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 <codetrauma@Johns-MacBook-Pro.local>
This commit is contained in:
Tanner
2024-03-14 23:56:25 -07:00
committed by GitHub
parent 4b90377cb3
commit 68de583281
19 changed files with 552 additions and 51 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
**/.DS_Store
.idea
build
.env

62
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -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}`);
});

View File

@@ -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<string | null>(null);
const [verifiedProof, setVerifiedProof] = useState<any>(null);
const notaryKey = useSelector((state: any) => state.notaryKey.key);
const [file, setFile] = useState<File | null>(null);
const handleFileUpload = useCallback(async (file: any): Promise<void> => {
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<HTMLDivElement>) => {
e.preventDefault();
@@ -68,7 +70,7 @@ return (
onDrop={handleFileDrop}
onDragOver={(e) => e.preventDefault()}
>
<i className="text-white fa-solid fa-upload text-6xl"></i>
<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>}
@@ -84,7 +86,7 @@ return (
<NotaryKey />
<br></br>
<br></br>
{!error && <ProofDetails proof={verifiedProof} /> }
{!error && <ProofDetails proof={verifiedProof} file={file} />}
</div>
)
}

View File

@@ -6,7 +6,7 @@ export default function Header(): ReactElement {
return (
<header className="flex flex-row items-center justify-between h-16 px-4 bg-gray-800 text-white">
<div className="text-xl font-bold">TLSN Explorer</div>
<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"

View File

@@ -0,0 +1,94 @@
import React, { MouseEventHandler, ReactElement, ReactNode } from 'react';
import ReactDOM from 'react-dom';
import './modal.scss';
import Icon from '../Icon';
import classNames from 'classnames';
type Props = {
className?: string;
onClose: MouseEventHandler;
children: ReactNode | ReactNode[];
};
export default function Modal(props: Props): ReactElement {
const { className, onClose, children } = props;
const modalRoot = document.querySelector('#modal-root');
if (!modalRoot) return <></>;
return ReactDOM.createPortal(
<div
className={classNames('bg-black bg-opacity-80', 'modal__overlay')}
onClick={(e) => {
e.stopPropagation();
onClose && onClose(e);
}}
>
<div
className={classNames(`modal__wrapper bg-white`, className)}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>,
modalRoot,
);
}
type HeaderProps = {
onClose?: () => void;
children: ReactNode;
};
export function ModalHeader(props: HeaderProps): ReactElement {
return (
<div className={classNames('border-b modal__header border-gray-100')}>
<div className="modal__header__title">{props.children}</div>
<div className="modal__header__content">
{props.onClose && (
<div
className={classNames(
'flex flex-row items-center justify-center',
'p-2 rounded-full opacity-50',
'hover:opacity-100 text-black',
)}
>
<Icon fa="fas fa-times" size={1} onClick={props.onClose} />
</div>
)}
</div>
</div>
);
}
type ContentProps = {
children: ReactNode;
className?: string;
};
export function ModalContent(props: ContentProps): ReactElement {
return (
<div className={classNames('modal__content', props.className)}>
{props.children}
</div>
);
}
type FooterProps = {
children: ReactNode;
className?: string;
};
export function ModalFooter(props: FooterProps): ReactElement {
return (
<div
className={classNames(
'border-t modal__footer border-gray-100',
props.className,
)}
>
{props.children}
</div>
);
}

View File

@@ -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;
}
}

View File

@@ -9,32 +9,26 @@ export default function NotaryKey(): ReactElement {
const defaultKey: string = keys.defaultKey
const notaryPseKey: string = keys.notaryPseKey
const [notaryKey, setNotaryKey] = useState<string>(defaultKey);
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-----')) {
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 (
<details className="w-3/4 m-auto">
<details className="w-3/4 mx-auto">
<summary className="text-2xl font-bold cursor-pointer">
Change Notary Public Key:
</summary>
<textarea className="w-full h-40 rounded bg-gray-800 text-white resize-none" value={notaryKey} onChange={(e) => handleInput(e)}>
</textarea>
{errors && <p className="text-red-500">{errors}</p>}
<button className="button" onClick={(e) => handleInput(e, notaryPseKey)}>
notary.pse.dev
</button>
<button className="button" onClick={(e) => handleInput(e, defaultKey)}>
Default
</button>
<textarea
className="w-full h-40 rounded bg-gray-800 text-white resize-none mt-4 p-4"
value={notaryKey}
onChange={(e) => handleInput(e)}
/>
{errors && <p className="text-red-500 mt-2">{errors}</p>}
<div className="flex mt-4">
<button
className="button"
onClick={(e) => handleInput(e, notaryPseKey)}
>
notary.pse.dev
</button>
<button
className="button"
onClick={(e) => handleInput(e, defaultKey)}
>
Default
</button>
</div>
</details>
)
}

View File

@@ -0,0 +1,45 @@
.button {
@apply bg-slate-100;
@apply text-slate-500;
@apply font-bold;
@apply px-2 py-0.5;
user-select: none;
&:hover {
@apply text-slate-600;
@apply bg-slate-200;
}
&:active {
@apply text-slate-700;
@apply bg-slate-300;
}
&--primary {
@apply bg-primary/[0.8];
@apply text-white;
&:hover {
@apply bg-primary/[0.9];
@apply text-white;
}
&:active {
@apply bg-primary;
@apply text-white;
}
}
&:disabled {
@apply opacity-50;
@apply select-none;
&:hover {
@apply text-slate-400;
}
&:active {
@apply text-slate-400;
}
}
}

View File

@@ -1,21 +1,115 @@
import React, { ReactElement } from 'react';
import React, { ReactElement, useState, useCallback, useEffect } from 'react';
import { formatStrings, formatTime, extractHTML } from '../../utils';
import { useSelector } from 'react-redux';
import { useSelector, useDispatch } from 'react-redux';
import ProofSelect from '../ProofSelect';
import Modal, { ModalContent, ModalHeader, ModalFooter } from '../Modal';
import { copyText } from '../../utils';
import { useSelectedProof } from '../../store/proofupload';
import { uploadFileToIpfs } from '../../store/upload';
import Icon from '../Icon';
interface ProofDetailsProps {
proof: any;
cid?: string;
file?: File | null;
}
const ProofDetails: React.FC<ProofDetailsProps> = ({proof, cid, file}): ReactElement => {
const dispatch = useDispatch();
const selectedProof = useSelectedProof();
const [isOpen, setIsOpen] = useState(false);
const [accepted, setAccepted] = useState(false);
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
const proofs = useSelector((state: any) => state.proofUpload.proofs);
useEffect(() => {
if (file) {
setFileToUpload(file)
}
}, [file])
export default function ProofDetails(proof: any): ReactElement {
const closeModal = useCallback(() => {
setIsOpen(false)
}, []);
const selectedProof = useSelector((state: any) => state.proofUpload.selectedProof);
const openModal = useCallback(() => {
setIsOpen(true)
}, []);
const proofToDisplay = selectedProof?.proof || proof?.proof;
const handleAccept = useCallback(async () => {
try {
if (!fileToUpload) {
console.error('No file to upload, state might be out of sync');
return;
}
const uploadedFile = fileToUpload;
await dispatch(uploadFileToIpfs(uploadedFile));
setAccepted(true);
} catch (e) {
console.error(e);
}
}, [fileToUpload]);
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}`;
// TODO - Format proof details for redacted data
return (
<div>
{proofToDisplay && (
<div className="flex flex-col gap-3 text-left items-center">
<ProofSelect />
<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>
{!accepted ? (
<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>
) : (
<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>
)}
</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>
<span className="font-bold text-2xl">Notarization Time:</span>
@@ -45,3 +139,5 @@ export default function ProofDetails(proof: any): ReactElement {
</div>
);
}
export default ProofDetails

View File

@@ -1,6 +1,5 @@
import React, { ReactElement } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { readFileAsync } from '../../utils';
import { selectProof } from '../../store/proofupload';
export default function ProofSelect(): ReactElement {

View File

@@ -0,0 +1,52 @@
import React, { ReactElement, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { verify } from 'tlsn-js';
import ProofDetails from '../ProofDetails';
import type { Proof } from '../types/types';
import { useNotaryKey } from '../../store/notaryKey';
import NotaryKey from '../NotaryKey';
export default function SharedProof(): ReactElement {
const { cid } = useParams();
const [verifiedProof, setVerifiedProof] = useState<Proof | null>(null);
const [errors, setErrors] = useState<string | null>(null);
const notaryKey = useNotaryKey();
useEffect(() => {
async function fetchFile() {
if (!cid) {
setErrors('No CID provided');
return;
}
const response = await fetch(`/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 {
const proof = await verify(data, notaryKey);
setVerifiedProof(proof);
} catch (e) {
setErrors('Provide a valid public key')
}
return data;
}
fetchFile();
}, [cid, notaryKey]);
return (
<div>
{<NotaryKey />}
<div className="flex flex-col items-center">
{!verifiedProof && errors && <div className="text-red-500 font-bold">{errors}</div>}
</div>
{verifiedProof && <ProofDetails proof={verifiedProof} cid={cid} />}
</div>
);
}

View File

@@ -3,6 +3,7 @@ import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import type {} from 'redux-thunk/extend-redux';
import store from './store';
import App from './pages/App';
@@ -19,4 +20,4 @@ import App from './pages/App';
if ((module as any).hot) {
(module as any).hot.accept();
}
}

View File

@@ -1,9 +1,9 @@
import React, { ReactElement } from 'react';
import './index.scss';
import Header from '../../components/Header';
import FileDrop from '../../components/FileUpload';
import { Routes, Route } from 'react-router-dom';
import ProofSelect from '../../components/ProofSelect';
import FileDrop from '../../components/FileUpload';
import SharedProof from '../../components/SharedProof';
export default function App(): ReactElement {
@@ -13,6 +13,7 @@ export default function App(): ReactElement {
<Header />
<Routes>
<Route path="/" element={<FileDrop />} />
<Route path="/:cid" element={<SharedProof />} />
</Routes>
</div>
);

View File

@@ -1,3 +1,5 @@
import { AppRootState } from '.';
import { useSelector } from 'react-redux';
import keys from '../utils/keys.json';
export enum ActionType {
@@ -34,6 +36,9 @@ function handleKey(state: State, action: Action): State {
}
}
export const useNotaryKey = () => {
return useSelector((state: AppRootState) => state.notaryKey.key);
}
export default function notaryKey(state = initState, action: Action): State {
switch (action.type) {

View File

@@ -1,8 +1,13 @@
import { AppRootState } from '.';
import type { Proof } from '../components/types/types'
import { useSelector } from 'react-redux';
export enum ActionType {
AddFile = 'proofupload/addFile',
SelectProof = 'proofupload/selectProof'
SelectProof = 'proofupload/selectProof',
UploadFileSuccess = 'proofupload/uploadFileSuccess',
}
export const uploadFile = (fileName: string, proof: Proof) => ({
@@ -15,6 +20,11 @@ export const selectProof = (proof: string) => ({
payload: proof
})
export const uploadFileSuccess = (cid: string) => ({
type: ActionType.UploadFileSuccess,
payload: cid
})
export type Action<payload = any> = {
type: ActionType;
payload: payload;
@@ -24,7 +34,7 @@ export type Action<payload = any> = {
type State = {
proofs: { fileName: string, proof: Proof }[];
selectedProof?: {fileName: string, proof: Proof} | null;
selectedProof?: { fileName: string, proof: Proof, ipfsCID?: string } | null;
}
const initState: State = {
@@ -34,7 +44,8 @@ const initState: State = {
function handleFile(state: State, action: Action): State {
return {
...state,
proofs: [...state.proofs, action.payload]
proofs: [...state.proofs, action.payload],
selectedProof: action.payload
}
}
@@ -43,7 +54,21 @@ function handleProofSelect(state: State, action: Action): State {
...state,
selectedProof: action.payload
}
}
function handleProofUpload(state: State, action: Action): State {
return {
...state,
// @ts-ignore
selectedProof: {
...state.selectedProof,
ipfsCID: action.payload
}
}
}
export const useSelectedProof = () => {
return useSelector((state: AppRootState) => state.proofUpload.selectedProof);
}
export default function proofUpload(state = initState, action: Action): State {
@@ -52,6 +77,8 @@ export default function proofUpload(state = initState, action: Action): State {
return handleFile(state, action);
case ActionType.SelectProof:
return handleProofSelect(state, action);
case ActionType.UploadFileSuccess:
return handleProofUpload(state, action);
default:
return state;
}

28
web/store/upload.ts Normal file
View File

@@ -0,0 +1,28 @@
import { ThunkDispatch } from 'redux-thunk';
import { Action, uploadFileSuccess } from './proofupload';
import { AppRootState } from './index';
import { ActionType
} from './proofupload';
export const uploadFileToIpfs = (file: File) => {
return async (dispatch: ThunkDispatch<AppRootState, ActionType, Action>) => {
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/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);
}
}
}

View File

@@ -1,4 +1,4 @@
import React, { ReactElement } from 'react';
import React, { ReactElement, useRef } from 'react';
export const readFileAsync = (file: File): Promise<string> => {
@@ -49,3 +49,12 @@ export const extractHTML = (receivedData: string): ReactElement => {
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);
}
}