mirror of
https://github.com/tlsnotary/explorer.git
synced 2026-01-08 04:23:52 -05:00
feat: implement new ux (#15)
* wip * new pubkey input * wip * restyle upload to ipfs * finish restyling
This commit is contained in:
@@ -26,11 +26,10 @@
|
|||||||
},
|
},
|
||||||
"ignorePatterns": [
|
"ignorePatterns": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"zip",
|
|
||||||
"build",
|
"build",
|
||||||
"wasm",
|
"rs",
|
||||||
"tlsn",
|
"webpack.web.config.js",
|
||||||
"util",
|
"webpack.server.config.js",
|
||||||
"webpack.config.js"
|
"static"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
5384
package-lock.json
generated
5384
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,9 @@
|
|||||||
"dev:server": "NODE_ENV=development concurrently npm:watch:server npm:nodemon:server",
|
"dev:server": "NODE_ENV=development concurrently npm:watch:server npm:nodemon:server",
|
||||||
"start": "node build/server/index.bundle.js",
|
"start": "node build/server/index.bundle.js",
|
||||||
"build": "concurrently npm:build:ui npm:build:server",
|
"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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -32,6 +34,7 @@
|
|||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"charwise": "^3.0.1",
|
"charwise": "^3.0.1",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-fileupload": "^1.4.3",
|
"express-fileupload": "^1.4.3",
|
||||||
@@ -87,6 +90,7 @@
|
|||||||
"fs-extra": "^11.1.0",
|
"fs-extra": "^11.1.0",
|
||||||
"html-loader": "^4.2.0",
|
"html-loader": "^4.2.0",
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
|
"image-webpack-loader": "^8.1.0",
|
||||||
"node-loader": "^2.0.0",
|
"node-loader": "^2.0.0",
|
||||||
"nodemon": "^3.0.3",
|
"nodemon": "^3.0.3",
|
||||||
"postcss-loader": "^7.3.3",
|
"postcss-loader": "^7.3.3",
|
||||||
|
|||||||
@@ -9,31 +9,40 @@ import { Provider } from 'react-redux';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { renderToString } from 'react-dom/server';
|
import { renderToString } from 'react-dom/server';
|
||||||
import { StaticRouter } from 'react-router-dom/server';
|
import { StaticRouter } from 'react-router-dom/server';
|
||||||
import configureAppStore from '../web/store';
|
import configureAppStore, { AppRootState } from '../web/store';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { verify } from '../rs/verifier/index.node';
|
import { verify } from '../rs/verifier/index.node';
|
||||||
import htmlToImage from 'node-html-to-image';
|
import htmlToImage from 'node-html-to-image';
|
||||||
|
import { Proof } from 'tlsn-js/build/types';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = 3000;
|
const port = 3000;
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content, Accept, Content-Type, Authorization');
|
res.setHeader(
|
||||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
|
'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-Embedder-Policy', 'require-corp');
|
||||||
res.setHeader('Cross-origin-Opener-Policy','same-origin');
|
res.setHeader('Cross-origin-Opener-Policy', 'same-origin');
|
||||||
|
|
||||||
if (req.method === 'OPTIONS') {
|
if (req.method === 'OPTIONS') {
|
||||||
res.sendStatus(200)
|
res.sendStatus(200);
|
||||||
} else {
|
} else {
|
||||||
next()
|
next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
app.use(express.static('build/ui'));
|
app.use(express.static('build/ui'));
|
||||||
app.use(fileUpload({
|
app.use(
|
||||||
limits: { fileSize: 1024 * 1024 }, // 1mb file limit
|
fileUpload({
|
||||||
}));
|
limits: { fileSize: 1024 * 1024 }, // 1mb file limit
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
app.post('/api/upload', async (req, res) => {
|
app.post('/api/upload', async (req, res) => {
|
||||||
for (const file of Object.values(req.files!)) {
|
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) => {
|
app.get('/ipfs/:cid', async (req, res) => {
|
||||||
const file = await getCID(req.params.cid);
|
let file: string,
|
||||||
const jsonProof = JSON.parse(file);
|
jsonProof: Proof,
|
||||||
const proof = await verify(file, await fetchPublicKeyFromNotary(jsonProof.notaryUrl));
|
proof: {
|
||||||
proof.notaryUrl = jsonProof.notaryUrl;
|
time: number;
|
||||||
|
sent: string;
|
||||||
|
recv: string;
|
||||||
|
notaryUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
const store = configureAppStore({
|
const storeConfig: AppRootState = {
|
||||||
notaryKey: { key: '' },
|
notaryKey: { key: '' },
|
||||||
proofUpload: {
|
proofUpload: {
|
||||||
proofs: [],
|
proofs: [],
|
||||||
selectedProof: null,
|
selectedProof: null,
|
||||||
},
|
},
|
||||||
proofs: {
|
proofs: { ipfs: {} },
|
||||||
ipfs: {
|
};
|
||||||
[req.params.cid]: {
|
|
||||||
raw: jsonProof,
|
// If there is no file from CID or JSON cannot be parsed, redirect to root
|
||||||
proof,
|
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(
|
const html = renderToString(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<StaticRouter location={req.url}>
|
<StaticRouter location={req.url}>
|
||||||
<App />
|
<App />
|
||||||
</StaticRouter>
|
</StaticRouter>
|
||||||
</Provider>
|
</Provider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const preloadedState = store.getState();
|
const preloadedState = store.getState();
|
||||||
|
|
||||||
const img = await htmlToImage({
|
const img = await htmlToImage({
|
||||||
html: html,
|
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(`
|
res.send(`
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@@ -103,7 +140,7 @@ app.get('/ipfs/:cid', async (req, res) => {
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta property="og:image" content="${imgUrl}" />
|
<meta property="og:image" content="${imgUrl}" />
|
||||||
<title>Popup</title>
|
<title>TLSNotary Explorer</title>
|
||||||
<script>
|
<script>
|
||||||
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState)};
|
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState)};
|
||||||
</script>
|
</script>
|
||||||
@@ -115,7 +152,7 @@ app.get('/ipfs/:cid', async (req, res) => {
|
|||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`);
|
||||||
})
|
});
|
||||||
|
|
||||||
app.get('*', (req, res) => {
|
app.get('*', (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, '../ui', 'index.html'));
|
res.sendFile(path.join(__dirname, '../ui', 'index.html'));
|
||||||
|
|||||||
@@ -4,15 +4,14 @@ const PINATA_API_KEY = process.env.PINATA_API_KEY;
|
|||||||
const PINATA_API_SECRET = process.env.PINATA_API_SECRET;
|
const PINATA_API_SECRET = process.env.PINATA_API_SECRET;
|
||||||
const pinata = new pinataSDK(PINATA_API_KEY, PINATA_API_SECRET);
|
const pinata = new pinataSDK(PINATA_API_KEY, PINATA_API_SECRET);
|
||||||
|
|
||||||
|
|
||||||
export async function addBytes(file: Buffer) {
|
export async function addBytes(file: Buffer) {
|
||||||
const res = await pinata.pinFileToIPFS(Readable.from(file), {
|
const res = await pinata.pinFileToIPFS(Readable.from(file), {
|
||||||
pinataMetadata: {
|
pinataMetadata: {
|
||||||
name: 'proof.json',
|
name: 'proof.json',
|
||||||
},
|
},
|
||||||
pinataOptions: {
|
pinataOptions: {
|
||||||
cidVersion: 1
|
cidVersion: 1,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.IpfsHash) return res.IpfsHash;
|
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, {
|
const res = await fetch(process.env.PINATA_GATEWAY + '/ipfs/' + hash, {
|
||||||
headers: {
|
headers: {
|
||||||
'x-pinata-gateway-token': process.env.PINATA_GATEWAY_KEY!,
|
'x-pinata-gateway-token': process.env.PINATA_GATEWAY_KEY!,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.text();
|
return res.text();
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -3,7 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<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>
|
<script>
|
||||||
window.__PRELOADED_STATE__ = {};
|
window.__PRELOADED_STATE__ = {};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
6
static/logo.svg
Normal file
6
static/logo.svg
Normal 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 |
@@ -30,12 +30,17 @@ export default function Button(props: Props): ReactElement {
|
|||||||
'button--secondary': btnType === 'secondary',
|
'button--secondary': btnType === 'secondary',
|
||||||
'cursor-default': disabled || loading,
|
'cursor-default': disabled || loading,
|
||||||
},
|
},
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
onClick={!disabled && !loading ? onClick : undefined}
|
onClick={!disabled && !loading ? onClick : undefined}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
{...btnProps}>
|
{...btnProps}
|
||||||
{loading ? <Icon className="animate-spin" fa="fa-solid fa-spinner" size={2} /> : children}
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={2} />
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
26
web/components/FileDropdown/index.tsx
Normal file
26
web/components/FileDropdown/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
51
web/components/FileUploadInput/index.tsx
Normal file
51
web/components/FileUploadInput/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,25 +1,35 @@
|
|||||||
import React, { ReactElement } from 'react';
|
import React, { ReactElement } from 'react';
|
||||||
|
import Logo from '../../../static/logo.svg';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
import Icon from '../Icon';
|
||||||
|
|
||||||
export default function Header(): ReactElement {
|
export default function Header(): ReactElement {
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="flex flex-row items-center justify-between h-16 px-4 bg-gray-800 text-white">
|
<header className="flex flex-row items-center justify-between h-16 px-4 bg-slate-200">
|
||||||
<a href="/" className="text-xl font-bold">TLSN Explorer</a>
|
<img className="w-8 h-8" src={Logo} />
|
||||||
<div className="flex flex-row items-center gap-4">
|
<div className="flex flex-row items-center">
|
||||||
<a href="https://tlsnotary.org/"
|
<a
|
||||||
className="flex flex-row items-center justify-center w-40 h-10 rounded button"
|
href="https://tlsnotary.org/"
|
||||||
target="_blank">
|
className="flex flex-row items-center justify-center button !bg-transparent"
|
||||||
About TLSNotary
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Icon fa="fa-solid fa-globe" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/tlsnotary/explorer" className="
|
<a
|
||||||
flex flex-row items-center justify-center
|
href="https://github.com/tlsnotary/explorer"
|
||||||
w-16 h-10 button rounded
|
className="flex flex-row items-center justify-center button !bg-transparent"
|
||||||
" target="_blank">
|
target="_blank"
|
||||||
Source
|
>
|
||||||
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,4 +34,4 @@ export default function Icon(props: Props): ReactElement {
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ type Props = {
|
|||||||
export default function Modal(props: Props): ReactElement {
|
export default function Modal(props: Props): ReactElement {
|
||||||
const { className, onClose, children } = props;
|
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 <></>;
|
if (!modalRoot) return <></>;
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,19 @@ import keys from '../../utils/keys.json';
|
|||||||
export default function NotaryKey(): ReactElement {
|
export default function NotaryKey(): ReactElement {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const defaultKey: string = keys.defaultKey
|
const defaultKey: string = keys.defaultKey;
|
||||||
const notaryPseKey: string = keys.notaryPseKey
|
const notaryPseKey: string = keys.notaryPseKey;
|
||||||
|
|
||||||
const [notaryKey, setNotaryKey] = useState<string>(notaryPseKey);
|
const [notaryKey, setNotaryKey] = useState<string>(notaryPseKey);
|
||||||
const [errors, setError] = useState<string | null>(null);
|
const [errors, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|
||||||
const isValidPEMKey = (key: string): boolean => {
|
const isValidPEMKey = (key: string): boolean => {
|
||||||
try {
|
try {
|
||||||
const trimmedKey = key.trim();
|
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');
|
setError('Invalid PEM format: header or footer missing');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -27,7 +29,6 @@ export default function NotaryKey(): ReactElement {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
atob(keyContent);
|
atob(keyContent);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('Invalid Base64 encoding');
|
setError('Invalid Base64 encoding');
|
||||||
return false;
|
return false;
|
||||||
@@ -35,23 +36,31 @@ export default function NotaryKey(): ReactElement {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
||||||
console.error('Error validating key:', err);
|
console.error('Error validating key:', err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInput = (
|
||||||
const handleInput = (e: FormEvent<HTMLTextAreaElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>, key?: string | undefined) => {
|
e:
|
||||||
|
| FormEvent<HTMLTextAreaElement>
|
||||||
|
| React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
||||||
|
key?: string | undefined,
|
||||||
|
) => {
|
||||||
setError(null);
|
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)) {
|
if (isValidPEMKey(keyInput)) {
|
||||||
setNotaryKey(keyInput);
|
setNotaryKey(keyInput);
|
||||||
dispatch(setKey(keyInput));
|
dispatch(setKey(keyInput));
|
||||||
} else {
|
} else {
|
||||||
setNotaryKey(keyInput);
|
setNotaryKey(keyInput);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<details className="w-3/4 mx-auto">
|
<details className="w-3/4 mx-auto">
|
||||||
@@ -71,13 +80,10 @@ export default function NotaryKey(): ReactElement {
|
|||||||
>
|
>
|
||||||
notary.pse.dev
|
notary.pse.dev
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button className="button" onClick={(e) => handleInput(e, defaultKey)}>
|
||||||
className="button"
|
|
||||||
onClick={(e) => handleInput(e, defaultKey)}
|
|
||||||
>
|
|
||||||
Default
|
Default
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ interface ProofDetailsProps {
|
|||||||
file?: File | null;
|
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 dispatch = useDispatch();
|
||||||
const selectedProof = useSelectedProof();
|
const selectedProof = useSelectedProof();
|
||||||
|
|
||||||
@@ -26,17 +30,16 @@ const ProofDetails: React.FC<ProofDetailsProps> = ({proof, cid, file}): ReactEle
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (file) {
|
if (file) {
|
||||||
setFileToUpload(file)
|
setFileToUpload(file);
|
||||||
}
|
}
|
||||||
}, [file])
|
}, [file]);
|
||||||
|
|
||||||
|
|
||||||
const closeModal = useCallback(() => {
|
const closeModal = useCallback(() => {
|
||||||
setIsOpen(false)
|
setIsOpen(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const openModal = useCallback(() => {
|
const openModal = useCallback(() => {
|
||||||
setIsOpen(true)
|
setIsOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleAccept = useCallback(async () => {
|
const handleAccept = useCallback(async () => {
|
||||||
@@ -55,16 +58,18 @@ const ProofDetails: React.FC<ProofDetailsProps> = ({proof, cid, file}): ReactEle
|
|||||||
}
|
}
|
||||||
}, [fileToUpload]);
|
}, [fileToUpload]);
|
||||||
|
|
||||||
|
const handleCopyClick: React.MouseEventHandler<HTMLButtonElement> = async (
|
||||||
const handleCopyClick: React.MouseEventHandler<HTMLButtonElement> = async (e) => {
|
e,
|
||||||
|
) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await copyText(inputValue);
|
await copyText(inputValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
const proofToDisplay = selectedProof?.proof || proof;
|
const proofToDisplay = selectedProof?.proof || proof;
|
||||||
const inputValue = process.env.NODE_ENV === "development"
|
const inputValue =
|
||||||
? `http://localhost:3000/${selectedProof?.ipfsCID ? selectedProof?.ipfsCID : cid}`
|
process.env.NODE_ENV === 'development'
|
||||||
: `www.tlsnexplorer.com/${selectedProof?.ipfsCID ? selectedProof?.ipfsCID : cid}`;
|
? `http://localhost:3000/${selectedProof?.ipfsCID ? selectedProof?.ipfsCID : cid}`
|
||||||
|
: `www.tlsnexplorer.com/${selectedProof?.ipfsCID ? selectedProof?.ipfsCID : cid}`;
|
||||||
|
|
||||||
// TODO - Format proof details for redacted data
|
// TODO - Format proof details for redacted data
|
||||||
|
|
||||||
@@ -72,64 +77,79 @@ const ProofDetails: React.FC<ProofDetailsProps> = ({proof, cid, file}): ReactEle
|
|||||||
<div>
|
<div>
|
||||||
{proofToDisplay && (
|
{proofToDisplay && (
|
||||||
<div className="flex flex-col gap-3 text-left items-center">
|
<div className="flex flex-col gap-3 text-left items-center">
|
||||||
<div className='flex flex-row gap-3'>
|
<div className="flex flex-row gap-3">
|
||||||
{proofs.length > 1 && <ProofSelect />}
|
{proofs.length > 1 && <ProofSelect />}
|
||||||
<button onClick={openModal} className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
|
<button
|
||||||
Share
|
onClick={openModal}
|
||||||
</button>
|
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
|
||||||
{isOpen && (
|
>
|
||||||
<Modal
|
Share
|
||||||
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"
|
</button>
|
||||||
onClose={closeModal}>
|
{isOpen && (
|
||||||
<ModalHeader onClose={closeModal}>
|
<Modal
|
||||||
Share Proof
|
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"
|
||||||
</ModalHeader>
|
onClose={closeModal}
|
||||||
<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>
|
<ModalHeader onClose={closeModal}>Share Proof</ModalHeader>
|
||||||
{!isUploading && (
|
<ModalContent className="flex flex-col items-center text-center gap-4">
|
||||||
accepted ? (
|
<p className="text-red-500 font-bold">
|
||||||
<div className="relative w-11/12">
|
This will make your proof publicly accessible by anyone with
|
||||||
<input
|
the CID
|
||||||
readOnly
|
</p>
|
||||||
value={inputValue}
|
{!isUploading &&
|
||||||
className="w-full h-12 bg-gray-800 text-white rounded px-3 pr-10" // Added pr-10 for padding on the right
|
(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
|
<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"
|
className="m-0 w-24 bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100 hover:font-bold"
|
||||||
onClick={handleCopyClick}
|
onClick={closeModal}
|
||||||
>
|
>
|
||||||
<Icon className="fas fa-copy" size={1}/>
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
) : (
|
|
||||||
<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="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>
|
</div>
|
||||||
<span className="font-bold text-2xl">Server Domain:</span>
|
<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>
|
<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>
|
<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">
|
<details open className="w-4/5 items-center">
|
||||||
<summary className="text-2xl font-bold cursor-pointer text-center">
|
<summary className="text-2xl font-bold cursor-pointer text-center">
|
||||||
Bytes Sent:
|
Bytes Sent:
|
||||||
@@ -152,6 +172,6 @@ const ProofDetails: React.FC<ProofDetailsProps> = ({proof, cid, file}): ReactEle
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ProofDetails
|
export default ProofDetails;
|
||||||
|
|||||||
@@ -7,25 +7,30 @@ export default function ProofSelect(): ReactElement {
|
|||||||
|
|
||||||
const proofs = useSelector((state: any) => state.proofUpload.proofs);
|
const proofs = useSelector((state: any) => state.proofUpload.proofs);
|
||||||
|
|
||||||
|
|
||||||
const handleChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
|
const handleChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
const selectedIndex = e.target.selectedIndex;
|
const selectedIndex = e.target.selectedIndex;
|
||||||
const proof = proofs[selectedIndex - 1];
|
const proof = proofs[selectedIndex - 1];
|
||||||
dispatch(selectProof(proof));
|
dispatch(selectProof(proof));
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
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 && (
|
{proofs && (
|
||||||
<select onChange={handleChange} className='bg-gray-800 text-white font-bold'>
|
<select
|
||||||
<option disabled className='font-bold'>Select a proof</option>
|
onChange={handleChange}
|
||||||
{proofs && proofs.map((proof: any, index: number) => (
|
className="bg-gray-800 text-white font-bold"
|
||||||
<option key={index} className='bg-gray-800 text-white font-bold'>
|
>
|
||||||
{proof.fileName}
|
<option disabled className="font-bold">
|
||||||
|
Select a proof
|
||||||
</option>
|
</option>
|
||||||
))}
|
{proofs &&
|
||||||
</select>
|
proofs.map((proof: any, index: number) => (
|
||||||
)}
|
<option key={index} className="bg-gray-800 text-white font-bold">
|
||||||
|
{proof.fileName}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
187
web/components/ProofViewer/index.tsx
Normal file
187
web/components/ProofViewer/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,62 +1,66 @@
|
|||||||
import React, { ReactElement, useEffect, useState } from 'react';
|
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import ProofDetails from '../ProofDetails';
|
|
||||||
import { useNotaryKey } from '../../store/notaryKey';
|
|
||||||
import NotaryKey from '../NotaryKey';
|
|
||||||
import { fetchProofFromIPFS, useIPFSProof } from '../../store/proofs';
|
import { fetchProofFromIPFS, useIPFSProof } from '../../store/proofs';
|
||||||
import { useDispatch } from 'react-redux';
|
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 {
|
export default function SharedProof(): ReactElement {
|
||||||
const { cid } = useParams();
|
const { cid } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [errors, setErrors] = useState<string | null>(null);
|
const [errors, setErrors] = useState<string | null>(null);
|
||||||
const notaryKey = useNotaryKey();
|
|
||||||
const proofData = useIPFSProof(cid);
|
const proofData = useIPFSProof(cid);
|
||||||
const dispatch = useDispatch();
|
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(() => {
|
useEffect(() => {
|
||||||
|
if (!cid) return;
|
||||||
|
dispatch(fetchProofFromIPFS(cid)).catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
setErrors(e.message);
|
||||||
|
});
|
||||||
|
}, [cid]);
|
||||||
|
|
||||||
async function fetchFile() {
|
const onVerify = useCallback(
|
||||||
if (!cid) {
|
async (key = '') => {
|
||||||
setErrors('No CID provided');
|
if (!proofData?.raw) return;
|
||||||
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 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);
|
if (!proofData) return <></>;
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
setErrors('Provide a valid public key')
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
dispatch(fetchProofFromIPFS(cid, notaryKey))
|
|
||||||
.catch(e => {
|
|
||||||
console.error(e);
|
|
||||||
setErrors(e.message);
|
|
||||||
});
|
|
||||||
}, [cid, notaryKey]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col items-center w-full h-screen m-auto gap-2">
|
||||||
{<NotaryKey />}
|
{!!file && (
|
||||||
<div className="flex flex-col items-center">
|
<FileDropdown
|
||||||
{!proofData && errors && <div className="text-red-500 font-bold">{errors}</div>}
|
files={[file]}
|
||||||
</div>
|
onChange={() => null}
|
||||||
{proofData && <ProofDetails proof={proofData.proof} cid={cid} />}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
|
|
||||||
export interface Proof {
|
|
||||||
time: number,
|
|
||||||
sent: string,
|
|
||||||
recv: string,
|
|
||||||
notaryUrl: string
|
|
||||||
}
|
|
||||||
14
web/custom.d.ts
vendored
Normal file
14
web/custom.d.ts
vendored
Normal 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;
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ delete window.__PRELOADED_STATE__;
|
|||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
document.getElementById('root')
|
document.getElementById('root'),
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import React, { ReactElement } from 'react';
|
import React, { ReactElement } from 'react';
|
||||||
import './index.scss';
|
|
||||||
import Header from '../../components/Header';
|
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router-dom';
|
||||||
import FileDrop from '../../components/FileUpload';
|
import Header from '../../components/Header';
|
||||||
import SharedProof from '../../components/SharedProof';
|
import SharedProof from '../../components/SharedProof';
|
||||||
|
import FileDrop from '../FileDrop';
|
||||||
|
import './index.scss';
|
||||||
|
|
||||||
export default function App(): ReactElement {
|
export default function App(): ReactElement {
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app flex flex-col gap-4">
|
<div className="app flex flex-col gap-4">
|
||||||
<Header />
|
<Header />
|
||||||
|
|||||||
145
web/pages/FileDrop/index.tsx
Normal file
145
web/pages/FileDrop/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
83
web/pages/PubkeyInput/index.tsx
Normal file
83
web/pages/PubkeyInput/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,25 +16,20 @@ export type AppRootState = ReturnType<typeof rootReducer>;
|
|||||||
const createStoreWithMiddleware =
|
const createStoreWithMiddleware =
|
||||||
process.env.NODE_ENV === 'development'
|
process.env.NODE_ENV === 'development'
|
||||||
? applyMiddleware(
|
? applyMiddleware(
|
||||||
thunk,
|
thunk,
|
||||||
createLogger({
|
createLogger({
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
}),
|
}),
|
||||||
)(createStore)
|
)(createStore)
|
||||||
: applyMiddleware(
|
: applyMiddleware(thunk)(createStore);
|
||||||
thunk,
|
|
||||||
)(createStore);
|
|
||||||
|
|
||||||
function configureAppStore(preloadedState?: AppRootState) {
|
function configureAppStore(preloadedState?: AppRootState) {
|
||||||
const { proofUpload, notaryKey, proofs } = preloadedState || {};
|
const { proofUpload, notaryKey, proofs } = preloadedState || {};
|
||||||
return createStoreWithMiddleware(
|
return createStoreWithMiddleware(rootReducer, {
|
||||||
rootReducer,
|
proofs,
|
||||||
{
|
proofUpload,
|
||||||
proofs,
|
notaryKey,
|
||||||
proofUpload,
|
});
|
||||||
notaryKey,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default configureAppStore;
|
export default configureAppStore;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { AppRootState } from './index';
|
|||||||
import type { Proof } from 'tlsn-js/build/types';
|
import type { Proof } from 'tlsn-js/build/types';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import deepEqual from 'fast-deep-equal';
|
import deepEqual from 'fast-deep-equal';
|
||||||
|
import { EXPLORER_URL } from '../utils/constants';
|
||||||
|
|
||||||
enum ActionType {
|
enum ActionType {
|
||||||
SetIPFSProof = 'proofs/setIPFSProof',
|
SetIPFSProof = 'proofs/setIPFSProof',
|
||||||
@@ -17,50 +18,61 @@ export type Action<payload = any> = {
|
|||||||
|
|
||||||
type ProofData = {
|
type ProofData = {
|
||||||
raw: Proof;
|
raw: Proof;
|
||||||
proof: {
|
proof?: {
|
||||||
time: number;
|
time: number;
|
||||||
sent: string;
|
sent: string;
|
||||||
recv: string;
|
recv: string;
|
||||||
notaryUrl: string;
|
notaryUrl: string;
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
ipfs: {
|
ipfs: {
|
||||||
[cid: string]: ProofData
|
[cid: string]: ProofData;
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const initState: State = {
|
const initState: State = {
|
||||||
ipfs: {},
|
ipfs: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchProofFromIPFS = (cid: string, notaryKey = '') => async (dispatch: ThunkDispatch<AppRootState, ActionType, Action>, getState: () => AppRootState) => {
|
export const fetchProofFromIPFS =
|
||||||
const old = getState().proofs.ipfs[cid];
|
(cid: string, notaryKey = '') =>
|
||||||
|
async (
|
||||||
|
dispatch: ThunkDispatch<AppRootState, ActionType, Action>,
|
||||||
|
getState: () => AppRootState,
|
||||||
|
) => {
|
||||||
|
const old = getState().proofs.ipfs[cid];
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
|
|
||||||
if (!old?.raw) {
|
if (!old?.raw) {
|
||||||
const response = await fetch(`/gateway/ipfs/${cid}`);
|
const response = await fetch(EXPLORER_URL + `/gateway/ipfs/${cid}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch file from IPFS');
|
throw new Error('Failed to fetch file from IPFS');
|
||||||
|
}
|
||||||
|
|
||||||
|
data = await response.json();
|
||||||
|
} else {
|
||||||
|
data = old.raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
data = await response.json();
|
const { verify } = await import('tlsn-js/src');
|
||||||
} else {
|
|
||||||
data = old.raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
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({
|
export const setIPFSProof = (
|
||||||
type: ActionType.SetIPFSProof,
|
payload: ProofData & {
|
||||||
payload: { cid, proof, raw: data },
|
cid: string;
|
||||||
});
|
},
|
||||||
}
|
) => ({
|
||||||
|
type: ActionType.SetIPFSProof,
|
||||||
|
payload: payload,
|
||||||
|
});
|
||||||
|
|
||||||
export default function proofs(
|
export default function proofs(
|
||||||
state = initState,
|
state = initState,
|
||||||
@@ -73,7 +85,7 @@ export default function proofs(
|
|||||||
recv: string;
|
recv: string;
|
||||||
notaryUrl: string;
|
notaryUrl: string;
|
||||||
};
|
};
|
||||||
}>
|
}>,
|
||||||
): State {
|
): State {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionType.SetIPFSProof:
|
case ActionType.SetIPFSProof:
|
||||||
@@ -85,8 +97,8 @@ export default function proofs(
|
|||||||
proof: action.payload.proof,
|
proof: action.payload.proof,
|
||||||
raw: action.payload.raw,
|
raw: action.payload.raw,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -97,4 +109,4 @@ export const useIPFSProof = (cid?: string): ProofData | null => {
|
|||||||
if (!cid) return null;
|
if (!cid) return null;
|
||||||
return state.proofs.ipfs[cid] || null;
|
return state.proofs.ipfs[cid] || null;
|
||||||
}, deepEqual);
|
}, deepEqual);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { AppRootState } from '.';
|
import { AppRootState } from '.';
|
||||||
import type { Proof } from '../components/types/types'
|
import type { Proof } from '../utils/types/types';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export enum ActionType {
|
export enum ActionType {
|
||||||
AddFile = 'proofupload/addFile',
|
AddFile = 'proofupload/addFile',
|
||||||
SelectProof = 'proofupload/selectProof',
|
SelectProof = 'proofupload/selectProof',
|
||||||
@@ -12,18 +10,18 @@ export enum ActionType {
|
|||||||
|
|
||||||
export const uploadFile = (fileName: string, proof: Proof) => ({
|
export const uploadFile = (fileName: string, proof: Proof) => ({
|
||||||
type: ActionType.AddFile,
|
type: ActionType.AddFile,
|
||||||
payload: { fileName, proof }
|
payload: { fileName, proof },
|
||||||
})
|
});
|
||||||
|
|
||||||
export const selectProof = (proof: string) => ({
|
export const selectProof = (proof: string) => ({
|
||||||
type: ActionType.SelectProof,
|
type: ActionType.SelectProof,
|
||||||
payload: proof
|
payload: proof,
|
||||||
})
|
});
|
||||||
|
|
||||||
export const uploadFileSuccess = (cid: string) => ({
|
export const uploadFileSuccess = (cid: string) => ({
|
||||||
type: ActionType.UploadFileSuccess,
|
type: ActionType.UploadFileSuccess,
|
||||||
payload: cid
|
payload: cid,
|
||||||
})
|
});
|
||||||
|
|
||||||
export type Action<payload = any> = {
|
export type Action<payload = any> = {
|
||||||
type: ActionType;
|
type: ActionType;
|
||||||
@@ -33,27 +31,27 @@ export type Action<payload = any> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
proofs: { fileName: string, proof: Proof }[];
|
proofs: { fileName: string; proof: Proof }[];
|
||||||
selectedProof?: { fileName: string, proof: Proof, ipfsCID?: string } | null;
|
selectedProof?: { fileName: string; proof: Proof; ipfsCID?: string } | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
const initState: State = {
|
const initState: State = {
|
||||||
proofs: []
|
proofs: [],
|
||||||
}
|
};
|
||||||
|
|
||||||
function handleFile(state: State, action: Action): State {
|
function handleFile(state: State, action: Action): State {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
proofs: [...state.proofs, action.payload],
|
proofs: [...state.proofs, action.payload],
|
||||||
selectedProof: action.payload
|
selectedProof: action.payload,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleProofSelect(state: State, action: Action): State {
|
function handleProofSelect(state: State, action: Action): State {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
selectedProof: action.payload
|
selectedProof: action.payload,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleProofUpload(state: State, action: Action): State {
|
function handleProofUpload(state: State, action: Action): State {
|
||||||
@@ -62,14 +60,14 @@ function handleProofUpload(state: State, action: Action): State {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
selectedProof: {
|
selectedProof: {
|
||||||
...state.selectedProof,
|
...state.selectedProof,
|
||||||
ipfsCID: action.payload
|
ipfsCID: action.payload,
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSelectedProof = () => {
|
export const useSelectedProof = () => {
|
||||||
return useSelector((state: AppRootState) => state.proofUpload.selectedProof);
|
return useSelector((state: AppRootState) => state.proofUpload.selectedProof);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function proofUpload(state = initState, action: Action): State {
|
export default function proofUpload(state = initState, action: Action): State {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
|||||||
@@ -2,26 +2,24 @@ import { ThunkDispatch } from 'redux-thunk';
|
|||||||
import { Action, uploadFileSuccess } from './proofupload';
|
import { Action, uploadFileSuccess } from './proofupload';
|
||||||
import { AppRootState } from './index';
|
import { AppRootState } from './index';
|
||||||
import { ActionType } from './proofupload';
|
import { ActionType } from './proofupload';
|
||||||
|
import { EXPLORER_URL } from '../utils/constants';
|
||||||
|
|
||||||
export const uploadFileToIpfs = (file: File) => {
|
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();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
try {
|
const response = await fetch(EXPLORER_URL + '/api/upload', {
|
||||||
const response = await fetch('/api/upload', {
|
method: 'POST',
|
||||||
method: 'POST',
|
body: formData,
|
||||||
body: formData
|
});
|
||||||
});
|
if (!response.ok) {
|
||||||
if (!response.ok) {
|
throw new Error('Failed to upload file to IPFS');
|
||||||
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 data = await response.json();
|
||||||
}
|
dispatch(uploadFileSuccess(data));
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
4
web/utils/constants.ts
Normal file
4
web/utils/constants.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export const EXPLORER_URL =
|
||||||
|
process.env.NODE_ENV === 'development'
|
||||||
|
? 'http://localhost:3000'
|
||||||
|
: 'https://explorer-tlsn.pse.dev/';
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { ReactElement, useRef } from 'react';
|
import React, { ReactElement, useRef } from 'react';
|
||||||
|
|
||||||
|
|
||||||
export const readFileAsync = (file: File): Promise<string> => {
|
export const readFileAsync = (file: File): Promise<string> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
@@ -19,42 +18,67 @@ export const readFileAsync = (file: File): Promise<string> => {
|
|||||||
|
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const formatTime = (time: number): string => {
|
export const formatTime = (time: number): string => {
|
||||||
const date = new Date(time * 1000);
|
const date = new Date(time * 1000);
|
||||||
return date.toLocaleString('en-US', { timeZone: 'UTC', hour12: false });
|
return date.toLocaleString('en-US', { timeZone: 'UTC', hour12: false });
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
export const formatStrings = (sentData: string): ReactElement => {
|
export const formatStrings = (sentData: string): ReactElement => {
|
||||||
return (
|
return (
|
||||||
<pre className='bg-gray-800 text-white h-fill overflow-x-scroll rounded'>
|
<pre className="bg-gray-800 text-white h-fill overflow-x-scroll rounded">
|
||||||
{sentData.split('\n').map((line, index) =>
|
{sentData.split('\n').map((line, index) => (
|
||||||
// TODO check for redactions
|
// TODO check for redactions
|
||||||
|
|
||||||
<React.Fragment key={index}>{line}<br />
|
<React.Fragment key={index}>
|
||||||
</React.Fragment>)}
|
{line}
|
||||||
|
<br />
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
</pre>
|
</pre>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const extractHTML = (receivedData: string): ReactElement => {
|
export const extractHTML = (receivedData: string): ReactElement => {
|
||||||
const startIndex = receivedData.indexOf('<!doctype html>');
|
const startIndex = receivedData.indexOf('<!doctype html>');
|
||||||
const endIndex = receivedData.lastIndexOf('</html>');
|
const endIndex = receivedData.lastIndexOf('</html>');
|
||||||
|
|
||||||
const html = receivedData.substring(startIndex, endIndex);
|
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> => {
|
export const copyText = async (text: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(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);
|
||||||
}
|
}
|
||||||
|
|||||||
6
web/utils/types/types.tsx
Normal file
6
web/utils/types/types.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface Proof {
|
||||||
|
time: number;
|
||||||
|
sent: string;
|
||||||
|
recv: string;
|
||||||
|
notaryUrl: string;
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ const options = {
|
|||||||
index: path.join(__dirname, "server", "index.tsx"),
|
index: path.join(__dirname, "server", "index.tsx"),
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.js', '.jsx', '.ts', '.tsx', '.css', '.scss'],
|
extensions: ['.js', '.jsx', '.ts', '.tsx', '.css', '.scss', '.png', '.svg'],
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: "[name].bundle.js",
|
filename: "[name].bundle.js",
|
||||||
@@ -26,6 +26,20 @@ const options = {
|
|||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
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
|
// look for .css or .scss files
|
||||||
test: /\.(css|scss)$/,
|
test: /\.(css|scss)$/,
|
||||||
|
|||||||
@@ -66,15 +66,15 @@ var options = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
test: new RegExp(".(" + fileExtensions.join("|") + ")$"),
|
// test: new RegExp(".(" + fileExtensions.join("|") + ")$"),
|
||||||
type: "asset/resource",
|
// type: "asset/resource",
|
||||||
exclude: /node_modules/,
|
// exclude: /node_modules/,
|
||||||
// loader: 'file-loader',
|
// // loader: 'file-loader',
|
||||||
// options: {
|
// // options: {
|
||||||
// name: '[name].[ext]',
|
// // name: '[name].[ext]',
|
||||||
// },
|
// // },
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
test: /\.html$/,
|
test: /\.html$/,
|
||||||
loader: "html-loader",
|
loader: "html-loader",
|
||||||
@@ -115,13 +115,27 @@ var options = {
|
|||||||
],
|
],
|
||||||
exclude: /node_modules/,
|
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: {
|
resolve: {
|
||||||
alias: alias,
|
alias: alias,
|
||||||
extensions: fileExtensions
|
extensions: fileExtensions
|
||||||
.map((extension) => "." + extension)
|
.map((extension) => "." + extension)
|
||||||
.concat([".js", ".jsx", ".ts", ".tsx", ".css"]),
|
.concat([".js", ".jsx", ".ts", ".tsx", ".css", ".png", ".svg"]),
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
isDevelopment && new ReactRefreshWebpackPlugin(),
|
isDevelopment && new ReactRefreshWebpackPlugin(),
|
||||||
@@ -136,6 +150,11 @@ var options = {
|
|||||||
to: path.join(__dirname, "build", "ui"),
|
to: path.join(__dirname, "build", "ui"),
|
||||||
force: true,
|
force: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
from: "static/favicon.png",
|
||||||
|
to: path.join(__dirname, "build", "ui"),
|
||||||
|
force: true,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
|
|||||||
Reference in New Issue
Block a user