From 68de583281b23a83eb3dce14ca8ac21567f20c36 Mon Sep 17 00:00:00 2001 From: Tanner <88640606+Codetrauma@users.noreply.github.com> Date: Thu, 14 Mar 2024 23:56:25 -0700 Subject: [PATCH] feat: Upload/Get Proofs from IPFS (#8) * feature: adding cid to proof store * refactor: stringify cid on server side * feature: route for /:cid to display proof details of shared proofs * chore: removing clogs * refactor: update imports and added useNotaryKey() hook * refactor: adding store action to update selectedProof.IpfsCID * refactor: proof is only uploaded to IPFS after user accepts now * refactor: updated uploadFileToIpfs thunk * refactor: refactored modal component to use createPortal * feature: updated to alpha 4 * refactor: integrated modal component from tlsn-extension * chore: changing port * refactor: more styling * refactor: fixed error on proof share if initial notary key doesn't verify proof --------- Co-authored-by: John Shaw --- .gitignore | 1 + package-lock.json | 62 ++++++++++++-- package.json | 12 ++- server/index.ts | 10 ++- web/components/FileUpload/index.tsx | 18 +++-- web/components/Header/index.tsx | 2 +- web/components/Modal/index.tsx | 94 +++++++++++++++++++++ web/components/Modal/modal.scss | 72 +++++++++++++++++ web/components/NotaryKey/index.tsx | 41 +++++----- web/components/ProofDetails/index.scss | 45 +++++++++++ web/components/ProofDetails/index.tsx | 108 +++++++++++++++++++++++-- web/components/ProofSelect/index.tsx | 1 - web/components/SharedProof/index.tsx | 52 ++++++++++++ web/index.tsx | 3 +- web/pages/App/index.tsx | 5 +- web/store/notaryKey.ts | 5 ++ web/store/proofupload.ts | 33 +++++++- web/store/upload.ts | 28 +++++++ web/utils/index.tsx | 11 ++- 19 files changed, 552 insertions(+), 51 deletions(-) create mode 100644 web/components/Modal/index.tsx create mode 100644 web/components/Modal/modal.scss create mode 100644 web/components/ProofDetails/index.scss create mode 100644 web/components/SharedProof/index.tsx create mode 100644 web/store/upload.ts diff --git a/.gitignore b/.gitignore index f6e280d..6de3654 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ **/.DS_Store .idea build +.env diff --git a/package-lock.json b/package-lock.json index d602e6d..71b09e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "redux-thunk": "^2.4.2", "stream": "^0.0.2", "tailwindcss": "^3.3.3", - "tlsn-js": "^0.1.0-alpha.3" + "tlsn-js": "^0.1.0-alpha.4" }, "devDependencies": { "@babel/core": "^7.20.12", @@ -41,6 +41,7 @@ "@babel/preset-react": "^7.18.6", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", "@types/chrome": "^0.0.202", + "@types/express": "^4.17.21", "@types/express-fileupload": "^1.4.4", "@types/node": "^20.4.10", "@types/react": "^18.0.26", @@ -84,7 +85,12 @@ "typescript": "^4.9.4", "webpack": "^5.75.0", "webpack-cli": "^4.10.0", - "webpack-dev-server": "^4.11.1" + "webpack-dev-server": "^4.11.1", + "webpack-node-externals": "^3.0.0" + }, + "optionalDependencies": { + "bufferutil": "^4.0.8", + "utf-8-validate": "^5.0.10" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -8034,6 +8040,19 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/bufferutil": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -15116,6 +15135,17 @@ "node": ">= 6.13.0" } }, + "node_modules/node-gyp-build": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -19496,9 +19526,9 @@ } }, "node_modules/tlsn-js": { - "version": "0.1.0-alpha.3", - "resolved": "https://registry.npmjs.org/tlsn-js/-/tlsn-js-0.1.0-alpha.3.tgz", - "integrity": "sha512-P26JOq50UOeQgjznH/M5E4B2lHBuSHd36jvHly/c2mNt5N3TsH0Dmhk16Ll7t/Dt62YdVcYOEvg/GVA3Xdkh0A==", + "version": "0.1.0-alpha.4.1", + "resolved": "https://registry.npmjs.org/tlsn-js/-/tlsn-js-0.1.0-alpha.4.1.tgz", + "integrity": "sha512-vQdauBqRkB9i6EnHbuCP4JJyZ+/YT/r2WbGVJVqMbYT0BLYTOx7zI+zv9RkmMZ0a8urhPZT/bJPVWTrymyleWg==", "dependencies": { "comlink": "^4.4.1" }, @@ -20035,6 +20065,19 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", @@ -20469,6 +20512,15 @@ "node": ">=10.0.0" } }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/webpack-sources": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", diff --git a/package.json b/package.json index 6054487..c2af7e3 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "redux-thunk": "^2.4.2", "stream": "^0.0.2", "tailwindcss": "^3.3.3", - "tlsn-js": "^0.1.0-alpha.3" + "tlsn-js": "^0.1.0-alpha.4" }, "devDependencies": { "@babel/core": "^7.20.12", @@ -57,6 +57,7 @@ "@babel/preset-react": "^7.18.6", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", "@types/chrome": "^0.0.202", + "@types/express": "^4.17.21", "@types/express-fileupload": "^1.4.4", "@types/node": "^20.4.10", "@types/react": "^18.0.26", @@ -100,7 +101,12 @@ "typescript": "^4.9.4", "webpack": "^5.75.0", "webpack-cli": "^4.10.0", - "webpack-dev-server": "^4.11.1" + "webpack-dev-server": "^4.11.1", + "webpack-node-externals": "^3.0.0" }, - "homepage": "https://github.com/tlsnotary/explorer#readme" + "homepage": "https://github.com/tlsnotary/explorer#readme", + "optionalDependencies": { + "bufferutil": "^4.0.8", + "utf-8-validate": "^5.0.10" + } } diff --git a/server/index.ts b/server/index.ts index 7faf177..592c14e 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,6 +1,7 @@ import express from 'express'; import fileUpload from 'express-fileupload'; import stream from 'stream'; +import path from 'path'; import { addBytes, getCID } from './services/ipfs'; @@ -25,13 +26,14 @@ app.use(fileUpload({ limits: { fileSize: 1024 * 1024 }, // 1mb file limit })); + + app.post('/upload', async (req, res) => { for (const file of Object.values(req.files!)) { // @ts-ignore const data = file.data; const cid = await addBytes(data); - res.send(cid.toString()); - + res.send(JSON.stringify(cid.toString())); return; } @@ -48,6 +50,10 @@ app.get('/ipfs/:cid', async (req, res) => { readStream.pipe(res); }); +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, '../ui', 'index.html')); +}); + app.listen(port, () => { console.log(`explorer server listening on port ${port}`); }); diff --git a/web/components/FileUpload/index.tsx b/web/components/FileUpload/index.tsx index bbcb035..0d55ec6 100644 --- a/web/components/FileUpload/index.tsx +++ b/web/components/FileUpload/index.tsx @@ -1,20 +1,22 @@ import React, { ReactElement, useCallback, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { uploadFile } from '../../store/proofupload'; import { verify } from 'tlsn-js' 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(null); const [verifiedProof, setVerifiedProof] = useState(null); - - const notaryKey = useSelector((state: any) => state.notaryKey.key); - + const [file, setFile] = useState(null); const handleFileUpload = useCallback(async (file: any): Promise => { if (file.type !== 'application/json') { @@ -26,6 +28,7 @@ export default function FileDrop(): ReactElement { setError('File size exceeds the maximum limit (1MB).'); return; } + setFile(file); setError(null); let verifiedProof: Proof; const proofContent = await readFileAsync(file); @@ -37,8 +40,7 @@ export default function FileDrop(): ReactElement { return; } dispatch(uploadFile(file.name, verifiedProof)); - -}, [dispatch]) +}, [dispatch, notaryKey]) const handleFileDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); @@ -68,7 +70,7 @@ return ( onDrop={handleFileDrop} onDragOver={(e) => e.preventDefault()} > - +

Drop your "proof.json" file here or click to select

{error &&

{error}

} @@ -84,7 +86,7 @@ return (



- {!error && } + {!error && } ) } diff --git a/web/components/Header/index.tsx b/web/components/Header/index.tsx index dbd33ea..befdb5d 100644 --- a/web/components/Header/index.tsx +++ b/web/components/Header/index.tsx @@ -6,7 +6,7 @@ export default function Header(): ReactElement { return (
-
TLSN Explorer
+ TLSN Explorer
; + + return ReactDOM.createPortal( +
{ + e.stopPropagation(); + onClose && onClose(e); + }} + > +
e.stopPropagation()} + > + {children} +
+
, + modalRoot, + ); +} + +type HeaderProps = { + onClose?: () => void; + children: ReactNode; +}; + +export function ModalHeader(props: HeaderProps): ReactElement { + return ( +
+
{props.children}
+
+ {props.onClose && ( +
+ +
+ )} +
+
+ ); +} + +type ContentProps = { + children: ReactNode; + className?: string; +}; + +export function ModalContent(props: ContentProps): ReactElement { + return ( +
+ {props.children} +
+ ); +} + +type FooterProps = { + children: ReactNode; + className?: string; +}; + +export function ModalFooter(props: FooterProps): ReactElement { + return ( +
+ {props.children} +
+ ); +} diff --git a/web/components/Modal/modal.scss b/web/components/Modal/modal.scss new file mode 100644 index 0000000..132730d --- /dev/null +++ b/web/components/Modal/modal.scss @@ -0,0 +1,72 @@ +.modal { + display: flex; + flex-flow: column nowrap; + + &__overlay { + position: fixed; + width: 100vw; + height: 100vh; + top: 0; + left: 0; + z-index: 9999; + overflow: auto; + } + + &__wrapper { + margin: 3rem auto; + border-radius: 0.5rem; + z-index: 200; + overflow: hidden; + + @media only screen and (max-width: 768px) { + width: 100vw !important; + } + } + + &__header { + display: flex; + flex-flow: row nowrap; + flex: 0 0 auto; + align-items: center; + padding: 0.5rem 1rem; + + &__title { + font-weight: 500; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + + &__content { + display: flex; + flex-flow: row nowrap; + flex: 1 1 auto; + justify-content: flex-end; + } + } + + &__content { + flex: 1 1 auto; + max-height: calc(100vh - 20rem); + overflow-y: auto; + + p:nth-of-type(1) { + margin-top: 0; + } + + .error-message { + font-size: 0.8125rem; + text-align: center; + margin-top: 1rem; + } + } + + &__footer { + display: flex; + flex-flow: row nowrap; + align-content: center; + justify-content: flex-end; + flex: 0 0 auto; + padding: 1rem 1.25rem; + } +} diff --git a/web/components/NotaryKey/index.tsx b/web/components/NotaryKey/index.tsx index 050cc32..8fa3167 100644 --- a/web/components/NotaryKey/index.tsx +++ b/web/components/NotaryKey/index.tsx @@ -9,32 +9,26 @@ export default function NotaryKey(): ReactElement { const defaultKey: string = keys.defaultKey const notaryPseKey: string = keys.notaryPseKey - const [notaryKey, setNotaryKey] = useState(defaultKey); + const [notaryKey, setNotaryKey] = useState(notaryPseKey); const [errors, setError] = useState(null); 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 { - const decodedKeyContent = atob(keyContent); + atob(keyContent); } catch (err) { - console.log('here'); setError('Invalid Base64 encoding'); return false; } @@ -60,19 +54,30 @@ export default function NotaryKey(): ReactElement { } return ( -
+
Change Notary Public Key: - - {errors &&

{errors}

} - - +