import { useCallback, useEffect, useReducer, useState, } from 'react'; import { useSDK } from '@metamask/sdk-react'; import { Web3 } from 'web3'; import Button from 'react-bootstrap/Button'; import DAOArtifact from './assets/DAO.json'; import work1Artifact from './assets/Work1.json'; const contracts = { '0x539': { // Hardhat DAO: '0x8d914D38dD301FC4606f5aa9fEcF8A76389020d3', Work1: '0x050C420Cc4995B41217Eba1B54B82Fd5687e9139', }, '0xaa36a7': { // Sepolia DAO: '0x8F00038542C87A5eAf18d5938B7723bF2A04A4e4', Work1: '0x42b79f8d8408c36aD4347ab72f826684440a7a8F', }, }; const getPoolStatus = (pool) => { if (pool.resolved) { return pool.outcome ? 'Accepted' : 'Rejected'; } return pool.timeRemaining > 0 ? 'In Progress' : 'Ready to Evaluate'; }; const getRequestStatus = (request) => { switch (Number(request.status)) { case 0: return 'Requested'; case 1: return 'Evidence Submitted'; case 2: return 'Approval Submitted'; case 3: return 'Complete'; default: return 'Unknown'; } }; const updateList = (list, action) => { switch (action.type) { case 'update': { const newList = [...list]; newList[Number(action.item.id)] = action.item; return newList; } case 'refresh': default: return []; } }; function App() { const { sdk, connected, provider, chainId, account, balance, } = useSDK(); const [DAO, setDAO] = useState(); const [work1, setWork1] = useState(); const [work1Price, setWork1Price] = useState(); const [balanceEther, setBalanceEther] = useState(); const [reputation, setReputation] = useState(); const [totalReputation, setTotalReputation] = useState(); const [posts, dispatchPost] = useReducer(updateList, []); const [validationPools, dispatchValidationPool] = useReducer(updateList, []); const [availabilityStakes, dispatchAvailabilityStake] = useReducer(updateList, []); const [workRequests, dispatchWorkRequest] = useReducer(updateList, []); // const watchReputationToken = useCallback(async () => { // await provider.request({ // method: 'wallet_watchAsset', // params: { // type: 'ERC20', // options: { // address: DAOAddress, // }, // }, // }); // }, [provider]); // In this effect, we initialize everything and add contract event listeners. // TODO: Refactor -- make separate, functional components? useEffect(() => { if (!provider || !chainId || !account || balance === undefined) return; if (!contracts[chainId]) return; console.log('INITIALIZATION EFFECT', { provider, chainId, account, }); const web3 = new Web3(provider); const DAOContract = new web3.eth.Contract(DAOArtifact.abi, contracts[chainId].DAO); const work1Contract = new web3.eth.Contract(work1Artifact.abi, contracts[chainId].Work1); /* -------------------------------------------------------------------------------- */ /* --------------------------- BEGIN FETCHERS ------------------------------------- */ /* -------------------------------------------------------------------------------- */ const fetchPrice = async () => { const fetchedPrice = await work1Contract.methods.price().call(); setWork1Price(web3.utils.fromWei(fetchedPrice, 'ether')); }; const fetchReputation = async () => { setReputation(Number(await DAOContract.methods.balanceOf(account).call())); setTotalReputation(Number(await DAOContract.methods.totalSupply().call())); }; const fetchPost = async (postIndex) => { const p = await DAOContract.methods.posts(postIndex).call(); p.id = Number(p.id); dispatchPost({ type: 'update', item: p }); return p; }; const fetchPosts = async () => { const count = await DAOContract.methods.postCount().call(); const promises = []; dispatchPost({ type: 'refresh' }); for (let i = 0; i < count; i += 1) { promises.push(fetchPost(i)); } await Promise.all(promises); }; const fetchValidationPool = async (poolIndex) => { const pool = await DAOContract.methods.validationPools(poolIndex).call(); pool.id = Number(pool.id); pool.timeRemaining = new Date(Number(pool.endTime) * 1000) - new Date(); pool.status = getPoolStatus(pool); dispatchValidationPool({ type: 'update', item: 'pool' }); // When remaing time expires, we want to update the status for this pool if (pool.timeRemaining > 0) { setTimeout(() => { pool.timeRemaining = 0; pool.status = getPoolStatus(pool); dispatchValidationPool({ type: 'update', item: pool }); }, pool.timeRemaining); } }; const fetchValidationPools = async () => { // TODO: Pagination // TODO: Memoization // TODO: Caching const count = await DAOContract.methods.validationPoolCount().call(); const promises = []; dispatchValidationPool({ type: 'refresh' }); for (let i = 0; i < count; i += 1) { // promises.push(DAOContract.methods.validationPools(i).call()); promises.push(fetchValidationPool(i)); } await Promise.all(promises); }; const fetchAvailabilityStake = async (stakeIndex) => { const s = await work1Contract.methods.stakes(stakeIndex).call(); Object.assign(s, { id: Number(stakeIndex), currentUserIsWorker: () => s.worker.toLowerCase() === account.toString().toLowerCase(), }); dispatchAvailabilityStake({ type: 'update', item: s }); return s; }; const fetchAvailabilityStakes = async () => { const count = await work1Contract.methods.stakeCount().call(); const promises = []; dispatchAvailabilityStake({ type: 'refresh' }); for (let i = 0; i < count; i += 1) { promises.push(fetchAvailabilityStake(i)); } await Promise.all(promises); }; const fetchWorkRequest = async (requestIndex) => { const r = await work1Contract.methods.requests(requestIndex).call(); Object.assign(r, { id: Number(requestIndex), statusString: getRequestStatus(r), feeEther: web3.utils.fromWei(r.fee, 'ether'), currentUserIsCustomer: () => r.customer.toLowerCase() === account.toString().toLowerCase(), }); dispatchWorkRequest({ type: 'update', item: r }); return r; }; const fetchWorkRequests = async () => { const count = await work1Contract.methods.requestCount().call(); const promises = []; dispatchWorkRequest({ type: 'refresh' }); for (let i = 0; i < count; i += 1) { promises.push(fetchWorkRequest(i)); } await Promise.all(promises); }; /* -------------------------------------------------------------------------------- */ /* --------------------------- END FETCHERS --------------------------------------- */ /* -------------------------------------------------------------------------------- */ fetchPrice(); fetchReputation(); fetchPosts(); fetchValidationPools(); fetchAvailabilityStakes(); fetchWorkRequests(); setWork1(work1Contract); setDAO(DAOContract); DAOContract.events.PostAdded({ fromBlock: 'latest' }).on('data', (event) => { console.log('event: post added', event); fetchPost(event.returnValues.postIndex); }); DAOContract.events.ValidationPoolInitiated({ fromBlock: 'latest' }).on('data', (event) => { console.log('event: validation pool initiated', event); fetchValidationPool(event.returnValues.poolIndex); }); DAOContract.events.ValidationPoolResolved({ fromBlock: 'latest' }).on('data', (event) => { console.log('event: validation pool resolved', event); fetchReputation(); fetchValidationPool(event.returnValues.poolIndex); }); work1Contract.events.WorkAssigned({ fromBlock: 'latest' }).on('data', (event) => { console.log('event: work assigned', event); const r = fetchWorkRequest(event.returnValues.requestIndex); fetchAvailabilityStake(r.stakeIndex); }); work1Contract.events.WorkEvidenceSubmitted({ fromBlock: 'latest' }).on('data', (event) => { console.log('event: work evidence submitted', event); fetchWorkRequest(event.returnValues.requestIndex); }); }, [provider, account, chainId, balance, setReputation, dispatchAvailabilityStake, dispatchValidationPool, dispatchWorkRequest]); /* -------------------------------------------------------------------------------- */ /* --------------------------- END MAIN INITIALIZION EFFECT ----------------------- */ /* -------------------------------------------------------------------------------- */ useEffect(() => { if (!provider || balance === undefined) return; const web3 = new Web3(provider); setBalanceEther(web3.utils.fromWei(balance, 'ether')); }, [provider, balance]); const connect = async () => { try { await sdk?.connect(); } catch (err) { console.warn('failed to connect..', err); } }; const disconnect = async () => { try { sdk?.terminate(); } catch (err) { console.warn('failed to disconnect..', err); } }; /* -------------------------------------------------------------------------------- */ /* --------------------------- BEGIN UI ACTIONS ----------------------------------- */ /* -------------------------------------------------------------------------------- */ const addPost = useCallback(async () => { await DAO.methods.addPost(account).send({ from: account, gas: 1000000, }); }, [DAO, account]); const initiateValidationPool = useCallback(async (postIndex, poolDuration) => { await DAO.methods.initiateValidationPool(postIndex, poolDuration ?? 3600).send({ from: account, gas: 1000000, value: 100, }); }, [DAO, account]); const stake = useCallback(async (poolIndex, amount, inFavor) => { console.log(`Attempting to stake ${amount} ${inFavor ? 'for' : 'against'} pool ${poolIndex}`); await DAO.methods.stake(poolIndex, amount, inFavor).send({ from: account, gas: 999999, }); // Since this is the result we expect from the server, we preemptively set it here. // We can let this value be negative -- this would just mean we'll be getting // at least one error from the server, and a corrected reputation. setReputation((current) => current - stake); }, [DAO, account, setReputation]); const stakeAllInFavor = useCallback(async (poolIndex) => { await stake(poolIndex, reputation, true); }, [stake, reputation]); const evaluateOutcome = useCallback(async (poolIndex) => { await DAO.methods.evaluateOutcome(poolIndex).send({ from: account, gas: 1000000, }); }, [DAO, account]); const stakeAvailability = useCallback(async () => { const duration = 300; // 5 minutes const target = contracts[chainId].Work1; await DAO.methods.stakeAvailability(target, reputation, duration).send({ from: account, gas: 1000000, }); // Note that as with validation pool stakes, we should keep track locally of our reputation setReputation(0); }, [DAO, account, chainId, reputation, setReputation]); const requestWork = useCallback(async () => { const web3 = new Web3(provider); const priceWei = BigInt(web3.utils.toWei(work1Price, 'ether')); await work1.methods.requestWork().send({ from: account, gas: 1000000, value: priceWei, }); }, [provider, work1, account, work1Price]); const submitWorkEvidence = useCallback(async (requestIndex) => { await work1.methods.submitWorkEvidence(requestIndex).send({ from: account, gas: 1000000, }); }, [work1, account]); const submitWorkApproval = useCallback(async (requestIndex) => { await work1.methods.submitWorkApproval(requestIndex, true).send({ from: account, gas: 1000000, }); }, [work1, account]); const submitWorkDisapproval = useCallback(async (requestIndex) => { await work1.methods.submitWorkApproval(requestIndex, false).send({ from: account, gas: 1000000, }); }, [work1, account]); /* -------------------------------------------------------------------------------- */ /* --------------------------- END UI ACTIONS ------------------------------------- */ /* -------------------------------------------------------------------------------- */ return ( <> {!connected && } {connected && ( <>
{!contracts[chainId] && (
Please switch MetaMask to Sepolia testnet!
)}
{chainId && `Chain ID: ${chainId}`}
{`Account: ${account}`}
{`Balance: ${balanceEther} ETH`}
{`Your REP: ${reputation}`}
{`Total REP: ${totalReputation}`}
{`Posts count: ${posts.length}`}
{posts.filter((x) => !!x).map((post) => ( ))}
ID Author Actions
{post.id.toString()} {post.author} Initiate Validation Pool {' '} {' '} {' '}
{`Validation Pool Count: ${validationPools.length}`}
{validationPools.filter((x) => !!x).map((pool) => ( ))}
ID Post ID Fee Duration End Time Stake
Count
Status Actions
{pool.id.toString()} {pool.postIndex.toString()} {pool.fee.toString()} {pool.duration.toString()} {new Date(Number(pool.endTime) * 1000).toLocaleString()} {pool.stakeCount.toString()} {pool.status} {!pool.resolved && reputation > 0 && pool.timeRemaining > 0 && ( <> {' '} )} {!pool.resolved && pool.timeRemaining <= 0 && ( )}

Work Contract 1

Price: {work1Price} {' '} ETH
Availability Stake Count: {' '} {availabilityStakes.length}
{availabilityStakes.filter((x) => !!x).map((s) => ( ))}
ID Worker Amount End Time Assigned Reclaimed
{s.id.toString()} {s.worker.toString()} {s.amount.toString()} {new Date(Number(s.endTime) * 1000).toLocaleString()} {s.assigned.toString()} {s.reclaimed.toString()}
Work Request Count: {' '} {workRequests.length}
{workRequests.filter((x) => !!x).map((request) => ( ))}
ID Customer Fee Status Stake ID Approval Pool ID Actions
{request.id.toString()} {request.customer.toString()} {request.feeEther} {' '} ETH {request.statusString} {request.stakeIndex.toString()} {request.approval.toString()} {request.poolIndex.toString()} {availabilityStakes.length > 0 && availabilityStakes[Number(request.stakeIndex)]?.currentUserIsWorker() && Number(request.status) === 0 && ( )} {request.currentUserIsCustomer() && Number(request.status) === 1 && ( <> )}
)} ); } export default App;