feat: implement new ux (#15)

* wip

* new pubkey input

* wip

* restyle upload to ipfs

* finish restyling
This commit is contained in:
tsukino
2024-04-16 09:17:07 -04:00
committed by GitHub
parent a44f2b46b7
commit 242729e1ae
35 changed files with 6348 additions and 429 deletions

View File

@@ -26,11 +26,10 @@
},
"ignorePatterns": [
"node_modules",
"zip",
"build",
"wasm",
"tlsn",
"util",
"webpack.config.js"
"rs",
"webpack.web.config.js",
"webpack.server.config.js",
"static"
]
}

5384
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,9 @@
"dev:server": "NODE_ENV=development concurrently npm:watch:server npm:nodemon:server",
"start": "node build/server/index.bundle.js",
"build": "concurrently npm:build:ui npm:build:server",
"dev": "concurrently npm:watch:ui npm:dev:server"
"dev": "concurrently npm:watch:ui npm:dev:server",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"repository": {
"type": "git",
@@ -32,6 +34,7 @@
"buffer": "^6.0.3",
"charwise": "^3.0.1",
"classnames": "^2.3.2",
"copy-to-clipboard": "^3.3.3",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"express-fileupload": "^1.4.3",
@@ -87,6 +90,7 @@
"fs-extra": "^11.1.0",
"html-loader": "^4.2.0",
"html-webpack-plugin": "^5.5.0",
"image-webpack-loader": "^8.1.0",
"node-loader": "^2.0.0",
"nodemon": "^3.0.3",
"postcss-loader": "^7.3.3",

View File

@@ -9,31 +9,40 @@ import { Provider } from 'react-redux';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom/server';
import configureAppStore from '../web/store';
import configureAppStore, { AppRootState } from '../web/store';
// @ts-ignore
import { verify } from '../rs/verifier/index.node';
import htmlToImage from 'node-html-to-image';
import { Proof } from 'tlsn-js/build/types';
const app = express();
const port = 3000;
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content, Accept, Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
res.setHeader(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content, Accept, Content-Type, Authorization',
);
res.setHeader(
'Access-Control-Allow-Methods',
'GET, POST, PUT, DELETE, PATCH, OPTIONS',
);
res.setHeader('Cross-origin-Embedder-Policy', 'require-corp');
res.setHeader('Cross-origin-Opener-Policy','same-origin');
res.setHeader('Cross-origin-Opener-Policy', 'same-origin');
if (req.method === 'OPTIONS') {
res.sendStatus(200)
res.sendStatus(200);
} else {
next()
next();
}
});
app.use(express.static('build/ui'));
app.use(fileUpload({
limits: { fileSize: 1024 * 1024 }, // 1mb file limit
}));
app.use(
fileUpload({
limits: { fileSize: 1024 * 1024 }, // 1mb file limit
}),
);
app.post('/api/upload', async (req, res) => {
for (const file of Object.values(req.files!)) {
@@ -58,44 +67,72 @@ app.get('/gateway/ipfs/:cid', async (req, res) => {
});
app.get('/ipfs/:cid', async (req, res) => {
const file = await getCID(req.params.cid);
const jsonProof = JSON.parse(file);
const proof = await verify(file, await fetchPublicKeyFromNotary(jsonProof.notaryUrl));
proof.notaryUrl = jsonProof.notaryUrl;
let file: string,
jsonProof: Proof,
proof: {
time: number;
sent: string;
recv: string;
notaryUrl: string;
};
const store = configureAppStore({
const storeConfig: AppRootState = {
notaryKey: { key: '' },
proofUpload: {
proofs: [],
selectedProof: null,
},
proofs: {
ipfs: {
[req.params.cid]: {
raw: jsonProof,
proof,
}
}
proofs: { ipfs: {} },
};
// If there is no file from CID or JSON cannot be parsed, redirect to root
try {
file = await getCID(req.params.cid);
jsonProof = JSON.parse(file);
} catch (e) {
res.redirect('/');
return;
}
storeConfig.proofs.ipfs[req.params.cid] = {
raw: jsonProof,
};
/**
* Verify the proof if notary url exist
* redirect to root if verification fails
*/
if (jsonProof.notaryUrl) {
try {
proof = await verify(
file,
await fetchPublicKeyFromNotary(jsonProof.notaryUrl),
);
proof.notaryUrl = jsonProof.notaryUrl;
storeConfig.proofs.ipfs[req.params.cid].proof = proof;
} catch (e) {
res.redirect('/');
return;
}
});
}
const store = configureAppStore(storeConfig);
const html = renderToString(
<Provider store={store}>
<StaticRouter location={req.url}>
<App />
</StaticRouter>
</Provider>
</Provider>,
);
const preloadedState = store.getState();
const img = await htmlToImage({
html: html,
});
const imgUrl= 'data:image/png;base64,' + img.toString('base64');
const imgUrl = 'data:image/png;base64,' + img.toString('base64');
console.log(imgUrl);
res.send(`
<!DOCTYPE html>
<html lang="en">
@@ -103,7 +140,7 @@ app.get('/ipfs/:cid', async (req, res) => {
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta property="og:image" content="${imgUrl}" />
<title>Popup</title>
<title>TLSNotary Explorer</title>
<script>
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState)};
</script>
@@ -115,7 +152,7 @@ app.get('/ipfs/:cid', async (req, res) => {
</body>
</html>
`);
})
});
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../ui', 'index.html'));

View File

@@ -4,15 +4,14 @@ const PINATA_API_KEY = process.env.PINATA_API_KEY;
const PINATA_API_SECRET = process.env.PINATA_API_SECRET;
const pinata = new pinataSDK(PINATA_API_KEY, PINATA_API_SECRET);
export async function addBytes(file: Buffer) {
const res = await pinata.pinFileToIPFS(Readable.from(file), {
pinataMetadata: {
name: 'proof.json',
},
pinataOptions: {
cidVersion: 1
}
cidVersion: 1,
},
});
if (res.IpfsHash) return res.IpfsHash;
@@ -23,8 +22,8 @@ export async function getCID(hash: string) {
const res = await fetch(process.env.PINATA_GATEWAY + '/ipfs/' + hash, {
headers: {
'x-pinata-gateway-token': process.env.PINATA_GATEWAY_KEY!,
}
},
});
return res.text();
}
}

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -3,7 +3,8 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Popup</title>
<link rel="icon" type="image/png" href="/static/favicon.png" />
<title>TLSNotary Explorer</title>
<script>
window.__PRELOADED_STATE__ = {};
</script>

6
static/logo.svg Normal file
View File

@@ -0,0 +1,6 @@
<svg width="86" height="88" viewBox="0 0 86 88" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.5484 0.708986C25.5484 0.17436 26.1196 -0.167376 26.5923 0.0844205L33.6891 3.86446C33.9202 3.98756 34.0645 4.22766 34.0645 4.48902V9.44049H37.6129C38.0048 9.44049 38.3226 9.75747 38.3226 10.1485V21.4766L36.1936 20.0606V11.5645H34.0645V80.9919C34.0645 81.1134 34.0332 81.2328 33.9735 81.3388L30.4251 87.6388C30.1539 88.1204 29.459 88.1204 29.1878 87.6388L25.6394 81.3388C25.5797 81.2328 25.5484 81.1134 25.5484 80.9919V0.708986Z" fill="#243F5F"/>
<path d="M21.2903 25.7246V76.7012H12.7742V34.2207H0V25.7246H21.2903Z" fill="#243F5F"/>
<path d="M63.871 76.7012H72.3871V34.2207H76.6452V76.7012H85.1613V25.7246H63.871V76.7012Z" fill="#243F5F"/>
<path d="M38.3226 25.7246H59.6129V34.2207H46.8387V46.9649H59.6129V76.7012H38.3226V68.2051H51.0968V55.4609H38.3226V25.7246Z" fill="#243F5F"/>
</svg>

After

Width:  |  Height:  |  Size: 896 B

View File

@@ -30,12 +30,17 @@ export default function Button(props: Props): ReactElement {
'button--secondary': btnType === 'secondary',
'cursor-default': disabled || loading,
},
className
className,
)}
onClick={!disabled && !loading ? onClick : undefined}
disabled={disabled || loading}
{...btnProps}>
{loading ? <Icon className="animate-spin" fa="fa-solid fa-spinner" size={2} /> : children}
{...btnProps}
>
{loading ? (
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={2} />
) : (
children
)}
</button>
);
}
}

View File

@@ -0,0 +1,26 @@
import React, { ReactElement } from 'react';
import Icon from '../Icon';
export function FileDropdown(props: {
files: File[];
onDelete: (fileIndex: number) => void;
onChange: (fileIndex: number) => void;
}): ReactElement {
const file = props.files[0];
if (!file) return <></>;
return (
<div className="flex flew-row bg-slate-100 border border-slate-200 text-slate-700 gap-2 p-2 rounded max-w-80">
<Icon className="text-slate-500 flex-shrink-0" fa="fa-solid fa-file" />
<div className="select-none flex-grow flex-shrink text-ellipsis overflow-hidden whitespace-nowrap">
{file.name}
</div>
<Icon
fa="fa-solid fa-close flex-shrink-0"
className="text-slate-300 hover:text-red-500"
onClick={() => props.onDelete(0)}
/>
</div>
);
}

View File

@@ -1,99 +0,0 @@
import React, { ReactElement, useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import { uploadFile } from '../../store/proofupload';
import { readFileAsync } from '../../utils';
import NotaryKey from '../NotaryKey';
import ProofDetails from '../ProofDetails';
import type { Proof } from '../types/types';
import { useNotaryKey } from '../../store/notaryKey';
import Icon from '../Icon';
export default function FileDrop(): ReactElement {
const dispatch = useDispatch();
const notaryKey = useNotaryKey();
const [error, setError] = useState<string | null>(null);
const [verifiedProof, setVerifiedProof] = useState<any>(null);
const [file, setFile] = useState<File | null>(null);
const handleFileUpload = useCallback(async (file: any): Promise<void> => {
if (file.type !== 'application/json') {
setError('Please upload a valid JSON file.');
return;
}
if (file.size >= 1024 * 1024) {
setError('File size exceeds the maximum limit (1MB).');
return;
}
setFile(file);
setError(null);
let verifiedProof: Proof;
const proofContent = await readFileAsync(file);
const { verify } = await import('tlsn-js');
try {
let pubKey: any;
if (JSON.parse(proofContent).notaryUrl) {
const notaryFetch = await fetch(JSON.parse(proofContent).notaryUrl + '/info');
const notaryData = await notaryFetch.json();
pubKey = notaryData.publicKey;
}
verifiedProof = await verify(JSON.parse(proofContent), pubKey || notaryKey);
setVerifiedProof(verifiedProof);
} catch(e) {
setError(e as string);
return;
}
dispatch(uploadFile(file.name, verifiedProof));
}, [dispatch, notaryKey])
const handleFileDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFileUpload(files[0]);
}
}, [handleFileUpload]);
const handleFileInput = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
const files = e.target.files;
if (files && files.length > 0) {
handleFileUpload(files[0]);
}
}, [handleFileUpload]);
return (
<div className="h-screen w-4/5 m-auto ">
<label htmlFor="file-upload" className="flex flex-col items-center justify-start h-1/2 w-full">
<div
className="flex flex-col items-center justify-center w-full h-full border-dashed border-gray-400 rounded-lg border-2 cursor-pointer bg-gray-800"
onDrop={handleFileDrop}
onDragOver={(e) => e.preventDefault()}
>
<Icon className="text-white" fa="fa-solid fa-upload" size={6} />
<br></br>
<p className="font-bold font-medium text-white">Drop your "proof.json" file here or click to select</p>
{error && <p className="text-red-500 font-bold">{error}</p>}
</div>
<input
id="file-upload"
type="file"
onChange={handleFileInput}
accept=".json"
className="w-full h-full hidden" />
</label>
<br></br>
<NotaryKey />
<br></br>
<br></br>
{!error && <ProofDetails proof={verifiedProof} file={file} />}
</div>
)
}

View File

@@ -0,0 +1,51 @@
import Icon from '../Icon';
import React from 'react';
import classNames from 'classnames';
export default function FileUploadInput(props: {
onFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
className?: string;
disabled?: boolean;
loading?: boolean;
}) {
return (
<div
className={classNames(
'flex flex-col flex-nowrap overflow-y-auto',
props.className,
)}
>
<div className="flex flex-col items-center justify-center relative border-slate-400 hover:border-slate-600 border-2 text-slate-500 border-dashed flex-grow flex-shrink h-0 m-2 bg-slate-200 gap-2">
{props.loading ? (
<>
<Icon
className="animate-spin mb-4"
fa="fa-solid fa-spinner"
size={2}
/>
<div className="text-lg">Drop your proof here to continue</div>
</>
) : (
<>
<input
type="file"
className="absolute w-full h-full top-0 left-0 opacity-0 z-10 cursor-pointer"
onChange={props.onFileChange}
accept=".json"
disabled={props.disabled || props.loading}
/>
<Icon className="mb-4" fa="fa-solid fa-upload" size={2} />
<div className="text-lg">Drop your proof here to continue</div>
<div className="text-sm">or</div>
<button
className="button !bg-primary/[.8] !hover:bg-primary/[.7] !active:bg-primary !text-white"
onClick={() => null}
>
Browse Files
</button>
</>
)}
</div>
</div>
);
}

View File

@@ -1,25 +1,35 @@
import React, { ReactElement } from 'react';
import Logo from '../../../static/logo.svg';
import './index.scss';
import Icon from '../Icon';
export default function Header(): ReactElement {
return (
<header className="flex flex-row items-center justify-between h-16 px-4 bg-gray-800 text-white">
<a href="/" className="text-xl font-bold">TLSN Explorer</a>
<div className="flex flex-row items-center gap-4">
<a href="https://tlsnotary.org/"
className="flex flex-row items-center justify-center w-40 h-10 rounded button"
target="_blank">
About TLSNotary
<header className="flex flex-row items-center justify-between h-16 px-4 bg-slate-200">
<img className="w-8 h-8" src={Logo} />
<div className="flex flex-row items-center">
<a
href="https://tlsnotary.org/"
className="flex flex-row items-center justify-center button !bg-transparent"
target="_blank"
>
<Icon fa="fa-solid fa-globe" />
</a>
<a href="https://github.com/tlsnotary/explorer" className="
flex flex-row items-center justify-center
w-16 h-10 button rounded
" target="_blank">
Source
<a
href="https://github.com/tlsnotary/explorer"
className="flex flex-row items-center justify-center button !bg-transparent"
target="_blank"
>
<Icon fa="fa-brands fa-github" />
</a>
<a
href="https://discord.gg/9XwESXtcN7"
className="flex flex-row items-center justify-center button !bg-transparent"
target="_blank"
>
<Icon fa="fa-brands fa-discord" />
</a>
</div>
</header>
)
);
}

View File

@@ -34,4 +34,4 @@ export default function Icon(props: Props): ReactElement {
{children}
</div>
);
}
}

View File

@@ -13,7 +13,10 @@ type Props = {
export default function Modal(props: Props): ReactElement {
const { className, onClose, children } = props;
const modalRoot = typeof document !== 'undefined' ? document.querySelector('#modal-root') : null;
const modalRoot =
typeof document !== 'undefined'
? document.querySelector('#modal-root')
: null;
if (!modalRoot) return <></>;

View File

@@ -6,17 +6,19 @@ import keys from '../../utils/keys.json';
export default function NotaryKey(): ReactElement {
const dispatch = useDispatch();
const defaultKey: string = keys.defaultKey
const notaryPseKey: string = keys.notaryPseKey
const defaultKey: string = keys.defaultKey;
const notaryPseKey: string = keys.notaryPseKey;
const [notaryKey, setNotaryKey] = useState<string>(notaryPseKey);
const [errors, setError] = useState<string | null>(null);
const isValidPEMKey = (key: string): boolean => {
try {
const trimmedKey = key.trim();
if (!trimmedKey.startsWith('-----BEGIN PUBLIC KEY-----') || !trimmedKey.endsWith('-----END PUBLIC KEY-----')) {
if (
!trimmedKey.startsWith('-----BEGIN PUBLIC KEY-----') ||
!trimmedKey.endsWith('-----END PUBLIC KEY-----')
) {
setError('Invalid PEM format: header or footer missing');
return false;
}
@@ -27,7 +29,6 @@ export default function NotaryKey(): ReactElement {
try {
atob(keyContent);
} catch (err) {
setError('Invalid Base64 encoding');
return false;
@@ -35,23 +36,31 @@ export default function NotaryKey(): ReactElement {
return true;
} catch (err) {
console.error('Error validating key:', err);
return false;
}
};
const handleInput = (e: FormEvent<HTMLTextAreaElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>, key?: string | undefined) => {
const handleInput = (
e:
| FormEvent<HTMLTextAreaElement>
| React.MouseEvent<HTMLButtonElement, MouseEvent>,
key?: string | undefined,
) => {
setError(null);
const keyInput = key !== undefined ? key : (e.currentTarget instanceof HTMLTextAreaElement ? e.currentTarget.value : '');
const keyInput =
key !== undefined
? key
: e.currentTarget instanceof HTMLTextAreaElement
? e.currentTarget.value
: '';
if (isValidPEMKey(keyInput)) {
setNotaryKey(keyInput);
dispatch(setKey(keyInput));
} else {
setNotaryKey(keyInput);
}
}
};
return (
<details className="w-3/4 mx-auto">
@@ -71,13 +80,10 @@ export default function NotaryKey(): ReactElement {
>
notary.pse.dev
</button>
<button
className="button"
onClick={(e) => handleInput(e, defaultKey)}
>
<button className="button" onClick={(e) => handleInput(e, defaultKey)}>
Default
</button>
</div>
</details>
)
);
}

View File

@@ -14,7 +14,11 @@ interface ProofDetailsProps {
file?: File | null;
}
const ProofDetails: React.FC<ProofDetailsProps> = ({proof, cid, file}): ReactElement => {
const ProofDetails: React.FC<ProofDetailsProps> = ({
proof,
cid,
file,
}): ReactElement => {
const dispatch = useDispatch();
const selectedProof = useSelectedProof();
@@ -26,17 +30,16 @@ const ProofDetails: React.FC<ProofDetailsProps> = ({proof, cid, file}): ReactEle
useEffect(() => {
if (file) {
setFileToUpload(file)
setFileToUpload(file);
}
}, [file])
}, [file]);
const closeModal = useCallback(() => {
setIsOpen(false)
setIsOpen(false);
}, []);
const openModal = useCallback(() => {
setIsOpen(true)
setIsOpen(true);
}, []);
const handleAccept = useCallback(async () => {
@@ -55,16 +58,18 @@ const ProofDetails: React.FC<ProofDetailsProps> = ({proof, cid, file}): ReactEle
}
}, [fileToUpload]);
const handleCopyClick: React.MouseEventHandler<HTMLButtonElement> = async (e) => {
const handleCopyClick: React.MouseEventHandler<HTMLButtonElement> = async (
e,
) => {
e.preventDefault();
await copyText(inputValue);
};
const proofToDisplay = selectedProof?.proof || proof;
const inputValue = process.env.NODE_ENV === "development"
? `http://localhost:3000/${selectedProof?.ipfsCID ? selectedProof?.ipfsCID : cid}`
: `www.tlsnexplorer.com/${selectedProof?.ipfsCID ? selectedProof?.ipfsCID : cid}`;
const inputValue =
process.env.NODE_ENV === 'development'
? `http://localhost:3000/${selectedProof?.ipfsCID ? selectedProof?.ipfsCID : cid}`
: `www.tlsnexplorer.com/${selectedProof?.ipfsCID ? selectedProof?.ipfsCID : cid}`;
// TODO - Format proof details for redacted data
@@ -72,64 +77,79 @@ const ProofDetails: React.FC<ProofDetailsProps> = ({proof, cid, file}): ReactEle
<div>
{proofToDisplay && (
<div className="flex flex-col gap-3 text-left items-center">
<div className='flex flex-row gap-3'>
{proofs.length > 1 && <ProofSelect />}
<button onClick={openModal} className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
Share
</button>
{isOpen && (
<Modal
className="flex flex-col items-center justify-center w-11/12 md:w-3/4 lg:w-2/4 h-auto md:h-auto lg:h-auto bg-white rounded-lg p-4 gap-4"
onClose={closeModal}>
<ModalHeader onClose={closeModal}>
Share Proof
</ModalHeader>
<ModalContent className="flex flex-col items-center text-center gap-4">
<p className='text-red-500 font-bold'>This will make your proof publicly accessible by anyone with the CID</p>
{!isUploading && (
accepted ? (
<div className="relative w-11/12">
<input
readOnly
value={inputValue}
className="w-full h-12 bg-gray-800 text-white rounded px-3 pr-10" // Added pr-10 for padding on the right
/>
<div className="flex flex-row gap-3">
{proofs.length > 1 && <ProofSelect />}
<button
onClick={openModal}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
Share
</button>
{isOpen && (
<Modal
className="flex flex-col items-center justify-center w-11/12 md:w-3/4 lg:w-2/4 h-auto md:h-auto lg:h-auto bg-white rounded-lg p-4 gap-4"
onClose={closeModal}
>
<ModalHeader onClose={closeModal}>Share Proof</ModalHeader>
<ModalContent className="flex flex-col items-center text-center gap-4">
<p className="text-red-500 font-bold">
This will make your proof publicly accessible by anyone with
the CID
</p>
{!isUploading &&
(accepted ? (
<div className="relative w-11/12">
<input
readOnly
value={inputValue}
className="w-full h-12 bg-gray-800 text-white rounded px-3 pr-10" // Added pr-10 for padding on the right
/>
<button
className="absolute top-0 right-0 w-10 h-12 bg-gray-700 text-white flex items-center justify-center hover:bg-gray-500 rounded-r focus:outline-none focus:ring focus:border-blue-500"
onClick={handleCopyClick}
>
<Icon className="fas fa-copy" size={1} />
</button>
</div>
) : (
<button
className="m-0 w-32 bg-red-200 text-red-500 hover:bg-red-200 hover:text-red-500 hover:font-bold"
onClick={handleAccept}
>
I understand
</button>
))}
{isUploading && (
<Icon
className="animate-spin"
fa="fa-solid fa-spinner"
size={1}
/>
)}
</ModalContent>
<ModalFooter>
<button
className="absolute top-0 right-0 w-10 h-12 bg-gray-700 text-white flex items-center justify-center hover:bg-gray-500 rounded-r focus:outline-none focus:ring focus:border-blue-500"
onClick={handleCopyClick}
className="m-0 w-24 bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100 hover:font-bold"
onClick={closeModal}
>
<Icon className="fas fa-copy" size={1}/>
Close
</button>
</div>
) : (
<button className="m-0 w-32 bg-red-200 text-red-500 hover:bg-red-200 hover:text-red-500 hover:font-bold" onClick={handleAccept}>
I understand
</button>
)
</ModalFooter>
</Modal>
)}
{isUploading && (
<Icon
className="animate-spin"
fa="fa-solid fa-spinner"
size={1}
/>
)}
</ModalContent>
<ModalFooter>
<button className="m-0 w-24 bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100 hover:font-bold" onClick={closeModal}>
Close
</button>
</ModalFooter>
</Modal>
)}
</div>
<span className="font-bold text-2xl">Server Domain:</span>
<div className="flex items-center h-12 w-4/5 bg-gray-800 text-white rounded">{proofToDisplay.serverName}</div>
<div className="flex items-center h-12 w-4/5 bg-gray-800 text-white rounded">
{proofToDisplay.serverName}
</div>
<span className="font-bold text-2xl">Notarization Time:</span>
<div className="flex items-center h-12 w-4/5 bg-gray-800 text-white rounded">{formatTime(proofToDisplay.time)} UTC</div>
<div className="flex items-center h-12 w-4/5 bg-gray-800 text-white rounded">
{formatTime(proofToDisplay.time)} UTC
</div>
<span className="font-bold text-2xl">Proof:</span>
<div className="flex items-center h-12 w-4/5 bg-gray-800 text-white rounded"> Proof Successfully Verified </div>
<div className="flex items-center h-12 w-4/5 bg-gray-800 text-white rounded">
Proof Successfully Verified
</div>
<details open className="w-4/5 items-center">
<summary className="text-2xl font-bold cursor-pointer text-center">
Bytes Sent:
@@ -152,6 +172,6 @@ const ProofDetails: React.FC<ProofDetailsProps> = ({proof, cid, file}): ReactEle
)}
</div>
);
}
};
export default ProofDetails
export default ProofDetails;

View File

@@ -7,25 +7,30 @@ export default function ProofSelect(): ReactElement {
const proofs = useSelector((state: any) => state.proofUpload.proofs);
const handleChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedIndex = e.target.selectedIndex;
const proof = proofs[selectedIndex - 1];
dispatch(selectProof(proof));
}
};
return (
<div className='flex flex-row m-auto items-center h-10 bg-gray-800 rounded gap-4 text-black'>
<div className="flex flex-row m-auto items-center h-10 bg-gray-800 rounded gap-4 text-black">
{proofs && (
<select onChange={handleChange} className='bg-gray-800 text-white font-bold'>
<option disabled className='font-bold'>Select a proof</option>
{proofs && proofs.map((proof: any, index: number) => (
<option key={index} className='bg-gray-800 text-white font-bold'>
{proof.fileName}
<select
onChange={handleChange}
className="bg-gray-800 text-white font-bold"
>
<option disabled className="font-bold">
Select a proof
</option>
))}
</select>
)}
{proofs &&
proofs.map((proof: any, index: number) => (
<option key={index} className="bg-gray-800 text-white font-bold">
{proof.fileName}
</option>
))}
</select>
)}
</div>
);
}

View File

@@ -0,0 +1,187 @@
import React, {
ReactNode,
ReactElement,
useState,
MouseEventHandler,
useCallback,
} from 'react';
import c from 'classnames';
import classNames from 'classnames';
import { Proof as VerifiedProof } from '../../utils/types/types';
import { Proof } from 'tlsn-js/build/types';
import Modal, { ModalContent, ModalFooter, ModalHeader } from '../Modal';
import Icon from '../Icon';
import { useDispatch } from 'react-redux';
import { uploadFileToIpfs } from '../../store/upload';
import { setIPFSProof } from '../../store/proofs';
import { EXPLORER_URL } from '../../utils/constants';
import copy from 'copy-to-clipboard';
export default function ProofViewer(props: {
file: File;
verifiedProof: VerifiedProof;
proof: Proof;
className?: string;
}): ReactElement {
const [tab, setTab] = useState('sent');
const [showingShareWarning, showShareWarning] = useState(false);
const [showingIPFSLink, showIPFSLink] = useState(false);
const dispatch = useDispatch();
const [cid, setCID] = useState('');
const onClickShare = useCallback(() => {
if (!showingShareWarning) return showShareWarning(true);
}, [showingShareWarning]);
const onUpload = useCallback(async () => {
const cid = await dispatch(uploadFileToIpfs(props.file));
dispatch(
setIPFSProof({ cid, proof: props.verifiedProof, raw: props.proof }),
);
setCID(cid);
showShareWarning(false);
showIPFSLink(true);
}, [props.file, props.verifiedProof, props.proof]);
return (
<div className={classNames('flex flex-col py-2 gap-2', props.className)}>
{showingShareWarning && (
<ShareWarningModal
onClose={() => showShareWarning(false)}
onUpload={onUpload}
/>
)}
{showingIPFSLink && (
<IPFSLinkModal cid={cid} onClose={() => showIPFSLink(false)} />
)}
<div className="flex flex-col px-2">
<div className="flex flex-row gap-2 items-center">
<TabLabel onClick={() => setTab('sent')} active={tab === 'sent'}>
Sent
</TabLabel>
<TabLabel onClick={() => setTab('recv')} active={tab === 'recv'}>
Recv
</TabLabel>
<div className="flex flex-row flex-grow items-center justify-end">
<button className="button" onClick={onClickShare}>
Share
</button>
</div>
</div>
</div>
<div className="flex flex-col flex-grow px-2">
{tab === 'sent' && (
<textarea
className="w-full resize-none bg-slate-100 text-slate-800 border p-2 text-xs break-all h-full outline-none font-mono"
value={props.verifiedProof.sent}
></textarea>
)}
{tab === 'recv' && (
<textarea
className="w-full resize-none bg-slate-100 text-slate-800 border p-2 text-xs break-all h-full outline-none font-mono"
value={props.verifiedProof.recv}
></textarea>
)}
</div>
</div>
);
}
function TabLabel(props: {
children: ReactNode;
onClick: MouseEventHandler;
active?: boolean;
}): ReactElement {
return (
<button
className={c('px-1 select-none cursor-pointer font-bold', {
'text-slate-800 border-b-2 border-green-500': props.active,
'text-slate-400 border-b-2 border-transparent hover:text-slate-500':
!props.active,
})}
onClick={props.onClick}
>
{props.children}
</button>
);
}
function ShareWarningModal(props: {
onClose: () => void;
onUpload: () => Promise<void>;
}): ReactElement {
const [error, showError] = useState('');
const [uploading, setUploading] = useState(false);
const onUpload = useCallback(async () => {
try {
setUploading(true);
await props.onUpload();
} catch (e: any) {
showError(e?.message || 'Unknown upload error');
} finally {
setUploading(false);
}
}, []);
return (
<Modal className="w-2/3 max-w-[45rem]" onClose={props.onClose}>
<ModalHeader>Sharing a Proof</ModalHeader>
<ModalContent className="py-2 px-4">
This will upload your proof to IPFS. Anyone with the url will be able to
view your proof. Are you sure you want to proceed?
</ModalContent>
<ModalFooter className="flex flex-row items-center gap-2 !py-2 !px-4">
{!!error && <span className="text-red-500 text-sm">{error}</span>}
<button
className="button self-start"
onClick={props.onClose}
disabled={uploading}
>
Cancel
</button>
<button
className="button button--primary self-start"
disabled={uploading}
onClick={onUpload}
>
{uploading ? (
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={1} />
) : (
'Upload'
)}
</button>
</ModalFooter>
</Modal>
);
}
function IPFSLinkModal(props: {
onClose: () => void;
cid: string;
}): ReactElement {
const ipfsLink = `${EXPLORER_URL}/ipfs/${props.cid}`;
return (
<Modal className="w-2/3 max-w-[45rem]" onClose={props.onClose}>
<ModalHeader>Sharing a Proof</ModalHeader>
<ModalContent className="py-2 px-4">
<input
readOnly
value={ipfsLink}
className="w-full bg-slate-100 border border-slate-300 outline-0 p-2 cursor-pointer"
onFocus={(e) => e.target.select()}
/>
</ModalContent>
<ModalFooter className="flex flex-row items-center gap-2 !py-2 !px-4">
<button className="button self-start" onClick={props.onClose}>
Close
</button>
<button
className="button button--primary self-start"
onClick={() => copy(ipfsLink)}
>
Copy
</button>
</ModalFooter>
</Modal>
);
}

View File

@@ -1,62 +1,66 @@
import React, { ReactElement, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import ProofDetails from '../ProofDetails';
import { useNotaryKey } from '../../store/notaryKey';
import NotaryKey from '../NotaryKey';
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { fetchProofFromIPFS, useIPFSProof } from '../../store/proofs';
import { useDispatch } from 'react-redux';
import ProofViewer from '../ProofViewer';
import { FileDropdown } from '../FileDropdown';
import { PubkeyInput } from '../../pages/PubkeyInput';
import { Proof } from '../../utils/types/types';
export default function SharedProof(): ReactElement {
const { cid } = useParams();
const navigate = useNavigate();
const [errors, setErrors] = useState<string | null>(null);
const notaryKey = useNotaryKey();
const proofData = useIPFSProof(cid);
const dispatch = useDispatch();
const file = new File([JSON.stringify(proofData?.raw)], `${cid}.json`, {
type: 'text/plain',
});
const [verifiedProof, setVerifiedProof] = useState<Proof | null>(
proofData?.proof || null,
);
useEffect(() => {
if (!cid) return;
dispatch(fetchProofFromIPFS(cid)).catch((e) => {
console.error(e);
setErrors(e.message);
});
}, [cid]);
async function fetchFile() {
if (!cid) {
setErrors('No CID provided');
return;
}
const response = await fetch(`/gateway/ipfs/${cid}`);
if (!response.ok) {
setErrors('Failed to fetch file from IPFS');
throw new Error('Failed to fetch file from IPFS');
}
const data = await response.json();
try {
let pubKey: any;
if (data.notaryUrl) {
const notaryFetch = await fetch(data.notaryUrl + '/info');
const notaryData = await notaryFetch.json();
pubKey = notaryData.publicKey;
}
const onVerify = useCallback(
async (key = '') => {
if (!proofData?.raw) return;
const proof = await verify(data, pubKey || notaryKey);
const { verify } = await import('tlsn-js/src/index');
const resp = await verify(proofData?.raw, key);
setVerifiedProof(resp);
},
[proofData?.raw],
);
setVerifiedProof(proof);
} catch (e) {
setErrors('Provide a valid public key')
}
return data;
}
dispatch(fetchProofFromIPFS(cid, notaryKey))
.catch(e => {
console.error(e);
setErrors(e.message);
});
}, [cid, notaryKey]);
if (!proofData) return <></>;
return (
<div>
{<NotaryKey />}
<div className="flex flex-col items-center">
{!proofData && errors && <div className="text-red-500 font-bold">{errors}</div>}
</div>
{proofData && <ProofDetails proof={proofData.proof} cid={cid} />}
<div className="flex flex-col items-center w-full h-screen m-auto gap-2">
{!!file && (
<FileDropdown
files={[file]}
onChange={() => null}
onDelete={() => navigate('/')}
/>
)}
{!!proofData.raw && !verifiedProof && (
<PubkeyInput className="w-2/3 flex-shrink-0" onNext={onVerify} />
)}
{verifiedProof && (
<ProofViewer
className="h-4/5 w-2/3 flex-shrink-0"
file={file}
proof={proofData.raw}
verifiedProof={verifiedProof}
/>
)}
</div>
);
}

View File

@@ -1,7 +0,0 @@
export interface Proof {
time: number,
sent: string,
recv: string,
notaryUrl: string
}

14
web/custom.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
declare module '*.svg' {
const content: string;
export default content;
}
declare module '*.png' {
const content: string;
export default content;
}
declare module '*.gif' {
const content: string;
export default content;
}

View File

@@ -19,7 +19,7 @@ delete window.__PRELOADED_STATE__;
<App />
</BrowserRouter>
</Provider>,
document.getElementById('root')
document.getElementById('root'),
);
})();

View File

@@ -1,13 +1,11 @@
import React, { ReactElement } from 'react';
import './index.scss';
import Header from '../../components/Header';
import { Routes, Route } from 'react-router-dom';
import FileDrop from '../../components/FileUpload';
import Header from '../../components/Header';
import SharedProof from '../../components/SharedProof';
import FileDrop from '../FileDrop';
import './index.scss';
export default function App(): ReactElement {
return (
<div className="app flex flex-col gap-4">
<Header />

View File

@@ -0,0 +1,145 @@
import React, { ReactElement, useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import { readFileAsync, safeParseJSON } from '../../utils';
import FileUploadInput from '../../components/FileUploadInput';
import ProofViewer from '../../components/ProofViewer';
import { Proof as VerifiedProof } from '../../utils/types/types';
import { FileDropdown } from '../../components/FileDropdown';
import { PubkeyInput } from '../PubkeyInput';
export default function FileDrop(): ReactElement {
const dispatch = useDispatch();
const [error, setError] = useState<string | null>(null);
const [verifiedProof, setVerifiedProof] = useState<VerifiedProof | null>(
null,
);
const [rawJson, setRawJson] = useState<any | null>(null);
const [file, setFile] = useState<File | null>(null);
const [step, setStep] = useState<'upload' | 'pubkey' | 'result'>('upload');
const [pubkey, setPubkey] = useState('');
const [uploading, setUploading] = useState(false);
const onVerify = useCallback(async (json: any, key = '') => {
const { verify } = await import('tlsn-js');
const resp = await verify(json, key);
setVerifiedProof(resp);
setStep('result');
}, []);
const handleFileUpload = useCallback(
async (file: any): Promise<void> => {
if (file.type !== 'application/json') {
setError('Please upload a valid JSON file.');
return;
}
if (file.size >= 1024 * 1024) {
setError('File size exceeds the maximum limit (1MB).');
return;
}
setError(null);
const proofContent = await readFileAsync(file);
const json = safeParseJSON(proofContent);
if (!json) {
setError(proofContent || 'Invalid proof');
return;
}
setRawJson(json);
if (!json?.notaryUrl) {
setStep('pubkey');
setFile(file);
return;
}
try {
await onVerify(json);
setFile(file);
} catch (e: any) {
setError(e?.message || 'Invalid proof');
}
},
[dispatch, onVerify],
);
const onFileChange = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
try {
setUploading(true);
const files = e.target.files;
if (files && files.length > 0) {
await handleFileUpload(files[0]);
}
} finally {
setUploading(false);
}
},
[handleFileUpload],
);
const onPubkeyChange = useCallback(
async (key: string) => {
setPubkey(key);
if (rawJson) {
await onVerify(rawJson, key);
}
},
[rawJson, onVerify],
);
return (
<div className="flex flex-col items-center w-full h-screen m-auto gap-2">
{!!file && (
<FileDropdown
files={[file]}
onChange={() => null}
onDelete={() => {
setFile(null);
setError('');
setPubkey('');
setVerifiedProof(null);
setRawJson(null);
setStep('upload');
}}
/>
)}
{(() => {
switch (step) {
case 'upload':
return (
<FileUploadInput
className="h-[24rem] w-2/3 flex-shrink-0"
onFileChange={onFileChange}
loading={uploading}
/>
);
case 'pubkey':
return (
<PubkeyInput
className="w-2/3 flex-shrink-0"
onNext={onPubkeyChange}
/>
);
case 'result':
return !!verifiedProof && !!file ? (
<ProofViewer
className="h-4/5 w-2/3 flex-shrink-0"
verifiedProof={verifiedProof}
proof={rawJson}
file={file}
/>
) : null;
default:
return null;
}
})()}
{error && <span className="text-red-500 text-sm">{error}</span>}
</div>
);
}

View File

@@ -0,0 +1,83 @@
import React, { ChangeEvent, useCallback, useState } from 'react';
import classNames from 'classnames';
export function PubkeyInput(props: {
onNext: (pubkey: string) => Promise<void>;
className?: string;
}) {
const [error, setError] = useState('');
const [pubkey, setPubkey] = useState('');
const isValidPEMKey = (key: string): boolean => {
try {
const trimmedKey = key.trim();
if (
!trimmedKey.startsWith('-----BEGIN PUBLIC KEY-----') ||
!trimmedKey.endsWith('-----END PUBLIC KEY-----')
) {
setError('Invalid PEM format: header or footer missing');
return false;
}
const keyContent = trimmedKey
.replace('-----BEGIN PUBLIC KEY-----', '')
.replace('-----END PUBLIC KEY-----', '')
.trim();
try {
atob(keyContent);
} catch (err) {
setError('Invalid Base64 encoding');
return false;
}
return true;
} catch (err) {
console.error('Error validating key:', err);
return false;
}
};
const onChange = useCallback(
async (e: ChangeEvent<HTMLTextAreaElement>) => {
setError('');
const pubkey = e.target.value;
setPubkey(pubkey);
},
[pubkey],
);
const onNext = useCallback(async () => {
if (isValidPEMKey(pubkey)) {
try {
await props.onNext(pubkey);
} catch (e: any) {
if (typeof e === 'string') {
setError(e);
} else {
setError(e?.message || 'Unable to verify proof');
}
}
}
}, [pubkey]);
return (
<div className={classNames('flex flex-col gap-2', props.className)}>
<div className="font-semibold">Please enter the notary key:</div>
<textarea
className="outline-0 flex-grow w-full bg-slate-100 rouned-xs !border border-slate-300 focus-within:border-slate-500 resize-none p-2 h-[24rem]"
onChange={onChange}
placeholder={`-----BEGIN PUBLIC KEY-----\n\n-----END PUBLIC KEY-----`}
/>
<div className="flex flex-row justify-end gap-2 items-center">
{error && <span className="text-red-500 text-sm">{error}</span>}
<button
className="button button--primary self-start"
onClick={onNext}
disabled={!pubkey || !!error}
>
Next
</button>
</div>
</div>
);
}

View File

@@ -16,25 +16,20 @@ export type AppRootState = ReturnType<typeof rootReducer>;
const createStoreWithMiddleware =
process.env.NODE_ENV === 'development'
? applyMiddleware(
thunk,
createLogger({
collapsed: true,
}),
)(createStore)
: applyMiddleware(
thunk,
)(createStore);
thunk,
createLogger({
collapsed: true,
}),
)(createStore)
: applyMiddleware(thunk)(createStore);
function configureAppStore(preloadedState?: AppRootState) {
const { proofUpload, notaryKey, proofs } = preloadedState || {};
return createStoreWithMiddleware(
rootReducer,
{
proofs,
proofUpload,
notaryKey,
},
);
return createStoreWithMiddleware(rootReducer, {
proofs,
proofUpload,
notaryKey,
});
}
export default configureAppStore;

View File

@@ -3,6 +3,7 @@ import { AppRootState } from './index';
import type { Proof } from 'tlsn-js/build/types';
import { useSelector } from 'react-redux';
import deepEqual from 'fast-deep-equal';
import { EXPLORER_URL } from '../utils/constants';
enum ActionType {
SetIPFSProof = 'proofs/setIPFSProof',
@@ -17,50 +18,61 @@ export type Action<payload = any> = {
type ProofData = {
raw: Proof;
proof: {
proof?: {
time: number;
sent: string;
recv: string;
notaryUrl: string;
}
}
};
};
type State = {
ipfs: {
[cid: string]: ProofData
}
}
[cid: string]: ProofData;
};
};
const initState: State = {
ipfs: {},
};
export const fetchProofFromIPFS = (cid: string, notaryKey = '') => async (dispatch: ThunkDispatch<AppRootState, ActionType, Action>, getState: () => AppRootState) => {
const old = getState().proofs.ipfs[cid];
export const fetchProofFromIPFS =
(cid: string, notaryKey = '') =>
async (
dispatch: ThunkDispatch<AppRootState, ActionType, Action>,
getState: () => AppRootState,
) => {
const old = getState().proofs.ipfs[cid];
let data;
let data;
if (!old?.raw) {
const response = await fetch(`/gateway/ipfs/${cid}`);
if (!old?.raw) {
const response = await fetch(EXPLORER_URL + `/gateway/ipfs/${cid}`);
if (!response.ok) {
throw new Error('Failed to fetch file from IPFS');
if (!response.ok) {
throw new Error('Failed to fetch file from IPFS');
}
data = await response.json();
} else {
data = old.raw;
}
data = await response.json();
} else {
data = old.raw;
}
const { verify } = await import('tlsn-js/src');
const { verify } = await import('tlsn-js/src');
const proof = await verify(data, notaryKey);
const proof = await verify(data, notaryKey);
dispatch(setIPFSProof({ cid, proof, raw: data }));
};
dispatch({
type: ActionType.SetIPFSProof,
payload: { cid, proof, raw: data },
});
}
export const setIPFSProof = (
payload: ProofData & {
cid: string;
},
) => ({
type: ActionType.SetIPFSProof,
payload: payload,
});
export default function proofs(
state = initState,
@@ -73,7 +85,7 @@ export default function proofs(
recv: string;
notaryUrl: string;
};
}>
}>,
): State {
switch (action.type) {
case ActionType.SetIPFSProof:
@@ -85,8 +97,8 @@ export default function proofs(
proof: action.payload.proof,
raw: action.payload.raw,
},
}
}
},
};
default:
return state;
}
@@ -97,4 +109,4 @@ export const useIPFSProof = (cid?: string): ProofData | null => {
if (!cid) return null;
return state.proofs.ipfs[cid] || null;
}, deepEqual);
}
};

View File

@@ -1,9 +1,7 @@
import { AppRootState } from '.';
import type { Proof } from '../components/types/types'
import type { Proof } from '../utils/types/types';
import { useSelector } from 'react-redux';
export enum ActionType {
AddFile = 'proofupload/addFile',
SelectProof = 'proofupload/selectProof',
@@ -12,18 +10,18 @@ export enum ActionType {
export const uploadFile = (fileName: string, proof: Proof) => ({
type: ActionType.AddFile,
payload: { fileName, proof }
})
payload: { fileName, proof },
});
export const selectProof = (proof: string) => ({
type: ActionType.SelectProof,
payload: proof
})
payload: proof,
});
export const uploadFileSuccess = (cid: string) => ({
type: ActionType.UploadFileSuccess,
payload: cid
})
payload: cid,
});
export type Action<payload = any> = {
type: ActionType;
@@ -33,27 +31,27 @@ export type Action<payload = any> = {
};
type State = {
proofs: { fileName: string, proof: Proof }[];
selectedProof?: { fileName: string, proof: Proof, ipfsCID?: string } | null;
}
proofs: { fileName: string; proof: Proof }[];
selectedProof?: { fileName: string; proof: Proof; ipfsCID?: string } | null;
};
const initState: State = {
proofs: []
}
proofs: [],
};
function handleFile(state: State, action: Action): State {
return {
...state,
proofs: [...state.proofs, action.payload],
selectedProof: action.payload
}
selectedProof: action.payload,
};
}
function handleProofSelect(state: State, action: Action): State {
return {
...state,
selectedProof: action.payload
}
selectedProof: action.payload,
};
}
function handleProofUpload(state: State, action: Action): State {
@@ -62,14 +60,14 @@ function handleProofUpload(state: State, action: Action): State {
// @ts-ignore
selectedProof: {
...state.selectedProof,
ipfsCID: action.payload
}
}
ipfsCID: action.payload,
},
};
}
export const useSelectedProof = () => {
return useSelector((state: AppRootState) => state.proofUpload.selectedProof);
}
};
export default function proofUpload(state = initState, action: Action): State {
switch (action.type) {

View File

@@ -2,26 +2,24 @@ import { ThunkDispatch } from 'redux-thunk';
import { Action, uploadFileSuccess } from './proofupload';
import { AppRootState } from './index';
import { ActionType } from './proofupload';
import { EXPLORER_URL } from '../utils/constants';
export const uploadFileToIpfs = (file: File) => {
return async (dispatch: ThunkDispatch<AppRootState, ActionType, Action>) => {
return async (
dispatch: ThunkDispatch<AppRootState, ActionType, Action>,
): Promise<string> => {
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Failed to upload file to IPFS');
}
const data = await response.json();
dispatch(uploadFileSuccess(data))
return data;
} catch (error) {
console.error('Error uploading file to IPFS:', error);
const response = await fetch(EXPLORER_URL + '/api/upload', {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error('Failed to upload file to IPFS');
}
}
}
const data = await response.json();
dispatch(uploadFileSuccess(data));
return data;
};
};

4
web/utils/constants.ts Normal file
View File

@@ -0,0 +1,4 @@
export const EXPLORER_URL =
process.env.NODE_ENV === 'development'
? 'http://localhost:3000'
: 'https://explorer-tlsn.pse.dev/';

View File

@@ -1,6 +1,5 @@
import React, { ReactElement, useRef } from 'react';
export const readFileAsync = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
@@ -19,42 +18,67 @@ export const readFileAsync = (file: File): Promise<string> => {
reader.readAsText(file);
});
}
};
export const formatTime = (time: number): string => {
const date = new Date(time * 1000);
return date.toLocaleString('en-US', { timeZone: 'UTC', hour12: false });
}
};
export const formatStrings = (sentData: string): ReactElement => {
return (
<pre className='bg-gray-800 text-white h-fill overflow-x-scroll rounded'>
{sentData.split('\n').map((line, index) =>
// TODO check for redactions
<pre className="bg-gray-800 text-white h-fill overflow-x-scroll rounded">
{sentData.split('\n').map((line, index) => (
// TODO check for redactions
<React.Fragment key={index}>{line}<br />
</React.Fragment>)}
<React.Fragment key={index}>
{line}
<br />
</React.Fragment>
))}
</pre>
);
};
export const extractHTML = (receivedData: string): ReactElement => {
const startIndex = receivedData.indexOf('<!doctype html>');
const endIndex = receivedData.lastIndexOf('</html>');
const html = receivedData.substring(startIndex, endIndex);
return <iframe className="w-full h-auto" srcDoc={html}></iframe>
return <iframe className="w-full h-auto" srcDoc={html}></iframe>;
};
export const copyText = async (text: string): Promise<void> => {
try {
await navigator.clipboard.writeText(text);
} catch (e) {
console.error(e);
}
};
export function safeParseJSON(data: any) {
try {
return JSON.parse(data);
} catch (e) {
return null;
}
}
export function download(filename: string, content: string) {
if (typeof document === 'undefined') return;
const element = document.createElement('a');
element.setAttribute(
'href',
'data:text/plain;charset=utf-8,' + encodeURIComponent(content),
);
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}

View File

@@ -0,0 +1,6 @@
export interface Proof {
time: number;
sent: string;
recv: string;
notaryUrl: string;
}

View File

@@ -16,7 +16,7 @@ const options = {
index: path.join(__dirname, "server", "index.tsx"),
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.css', '.scss'],
extensions: ['.js', '.jsx', '.ts', '.tsx', '.css', '.scss', '.png', '.svg'],
},
output: {
filename: "[name].bundle.js",
@@ -26,6 +26,20 @@ const options = {
},
module: {
rules: [
{
test: /\.(gif|png|jpe?g|svg)$/i,
use: [
'file-loader',
{
loader: 'image-webpack-loader',
options: {
publicPath: 'assets',
bypassOnDebug: true, // webpack@1.x
disable: true, // webpack@2.x and newer
},
},
],
},
{
// look for .css or .scss files
test: /\.(css|scss)$/,

View File

@@ -66,15 +66,15 @@ var options = {
},
],
},
{
test: new RegExp(".(" + fileExtensions.join("|") + ")$"),
type: "asset/resource",
exclude: /node_modules/,
// loader: 'file-loader',
// options: {
// name: '[name].[ext]',
// },
},
// {
// test: new RegExp(".(" + fileExtensions.join("|") + ")$"),
// type: "asset/resource",
// exclude: /node_modules/,
// // loader: 'file-loader',
// // options: {
// // name: '[name].[ext]',
// // },
// },
{
test: /\.html$/,
loader: "html-loader",
@@ -115,13 +115,27 @@ var options = {
],
exclude: /node_modules/,
},
{
test: /\.(gif|png|jpe?g|svg)$/i,
use: [
'file-loader',
{
loader: 'image-webpack-loader',
options: {
publicPath: 'assets',
bypassOnDebug: true, // webpack@1.x
disable: true, // webpack@2.x and newer
},
},
],
},
],
},
resolve: {
alias: alias,
extensions: fileExtensions
.map((extension) => "." + extension)
.concat([".js", ".jsx", ".ts", ".tsx", ".css"]),
.concat([".js", ".jsx", ".ts", ".tsx", ".css", ".png", ".svg"]),
},
plugins: [
isDevelopment && new ReactRefreshWebpackPlugin(),
@@ -136,6 +150,11 @@ var options = {
to: path.join(__dirname, "build", "ui"),
force: true,
},
{
from: "static/favicon.png",
to: path.join(__dirname, "build", "ui"),
force: true,
},
]
}),
new HtmlWebpackPlugin({