diff --git a/client/src/App.jsx b/client/src/App.jsx index 044edc7..3f38b69 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,5 +1,5 @@ import { - useCallback, useEffect, useRef, useState, + useCallback, useEffect, useReducer, useState, } from 'react'; import { useSDK } from '@metamask/sdk-react'; import { Web3 } from 'web3'; @@ -19,6 +19,34 @@ const contracts = { }, }; +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 updateListItem = (list, item) => { + const newList = [...list]; + newList[Number(item.id)] = item; + return newList; +}; + function App() { const { sdk, connected, provider, chainId, account, balance, @@ -28,13 +56,12 @@ function App() { const [work1, setWork1] = useState(); const [work1Price, setWork1Price] = useState(); const [balanceEther, setBalanceEther] = useState(); - const reputation = useRef(); + const [reputation, setReputation] = useState(); const [totalReputation, setTotalReputation] = useState(); - const [posts, setPosts] = useState([]); - const [validationPools, setValidationPools] = useState([]); - const stakedPools = useRef([]); - const [availabilityStakes, setAvailabilityStakes] = useState([]); - const [workRequests, setWorkRequests] = useState([]); + const [posts, dispatchPost] = useReducer(updateListItem, []); + const [validationPools, dispatchValidationPool] = useReducer(updateListItem, []); + const [availabilityStakes, dispatchAvailabilityStake] = useReducer(updateListItem, []); + const [workRequests, dispatchWorkRequest] = useReducer(updateListItem, []); // const watchReputationToken = useCallback(async () => { // await provider.request({ @@ -48,73 +75,68 @@ function App() { // }); // }, [provider]); - const getPoolStatus = (pool) => { - if (pool.resolved) { - return pool.outcome ? 'Accepted' : 'Rejected'; - } - const endDate = new Date(Number(pool.endTime) * 1000); - return new Date() < endDate ? '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'; - } - }; - // In this effect, we initialize everything and add contract event listeners. // TODO: Refactor -- make separate, functional components? useEffect(() => { - if (!provider || !chainId || !account) return; + 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 () => { - reputation.current = Number(await DAOContract.methods.balanceOf(account).call()); - setTotalReputation(await DAOContract.methods.totalSupply().call()); + 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(p); + return p; }; const fetchPosts = async () => { const count = await DAOContract.methods.postCount().call(); const promises = []; for (let i = 0; i < count; i += 1) { - promises.push(DAOContract.methods.posts(i).call()); + promises.push(fetchPost(i)); } - const fetchedPosts = await Promise.all(promises); - setPosts(fetchedPosts); + await Promise.all(promises); }; - const stake = async (poolIndex, amount, inFavor) => { - console.log(`Attempting to stake ${amount} ${inFavor ? 'for' : 'against'} pool ${poolIndex}`); - await DAOContract.methods.stake(poolIndex, amount, inFavor).send({ - from: account, - gas: 1000000, - }); + const fetchValidationPool = async (poolIndex) => { + console.trace('fetchValidationPool'); + 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(pool); - // 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. - reputation.current = Number(reputation.current) - Number(stake); + // 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(pool); + }, pool.timeRemaining); + } }; - const stakeAllInFavor = (poolIndex) => stake(poolIndex, reputation.current, true); - const fetchValidationPools = async () => { // TODO: Pagination // TODO: Memoization @@ -122,71 +144,57 @@ function App() { const count = await DAOContract.methods.validationPoolCount().call(); const promises = []; for (let i = 0; i < count; i += 1) { - promises.push(DAOContract.methods.validationPools(i).call()); + // promises.push(DAOContract.methods.validationPools(i).call()); + promises.push(fetchValidationPool(i)); } - const pools = (await Promise.all(promises)).map((p) => { - const pool = p; - pool.status = getPoolStatus(pool); - const timeRemaining = new Date(Number(pool.endTime) * 1000) - new Date(); - if (timeRemaining > 0 && !pool.resolved && reputation.current - && !stakedPools.current.includes(pool.id)) { - // Naievely stake all reputation that this validation pool is valid. - // This is the greediest possible strategy. - // Staking reputation transfers it, thus it's important we update our reputation - // locally before hearing back from the server -- since blockchains are slow. - // Note that this means refresing the page will re-send any pending staking operations. - stakeAllInFavor(pool.id); - stakedPools.current = stakedPools.current.concat(pool.id); - } + await Promise.all(promises); + }; - // TODO: When remaing time expires, we want to update the status for this pool - // if (timeRemaining > 0) { - // setTimeout(() => { - // pool.status = getStatus(pool); - // setValidationPools((currentPools) => { - // const newPools = currentPools; - // newPools[pool.id] = pool; - // return newPools; - // }); - // console.log(`attepted to update pool status: ${pool.status}`); - // }, timeRemaining); - // } - - return pool; + 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(), }); - setValidationPools(pools); + dispatchAvailabilityStake(s); + return s; }; const fetchAvailabilityStakes = async () => { const count = await work1Contract.methods.stakeCount().call(); const promises = []; for (let i = 0; i < count; i += 1) { - promises.push(work1Contract.methods.stakes(i).call()); + promises.push(fetchAvailabilityStake(i)); } - const fetchedStakes = (await Promise.all(promises)).map((x, index) => { - Object.assign(x, { id: index }); - return x; + 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(), }); - setAvailabilityStakes(fetchedStakes); + dispatchWorkRequest(r); + return r; }; const fetchWorkRequests = async () => { const count = await work1Contract.methods.requestCount().call(); const promises = []; for (let i = 0; i < count; i += 1) { - promises.push(work1Contract.methods.requests(i).call()); + promises.push(fetchWorkRequest(i)); } - const fetchedRequests = (await Promise.all(promises)).map((x, index) => { - Object.assign(x, { - id: index, - statusString: getRequestStatus(x), - feeEther: web3.utils.fromWei(x.fee, 'ether'), - }); - return x; - }); - setWorkRequests(fetchedRequests); + await Promise.all(promises); }; + /* -------------------------------------------------------------------------------- */ + /* --------------------------- END FETCHERS --------------------------------------- */ + /* -------------------------------------------------------------------------------- */ + fetchPrice(); fetchReputation(); fetchPosts(); @@ -199,20 +207,36 @@ function App() { DAOContract.events.PostAdded({ fromBlock: 'latest' }).on('data', (event) => { console.log('event: post added', event); - fetchPosts(); + fetchPost(event.returnValues.postIndex); }); DAOContract.events.ValidationPoolInitiated({ fromBlock: 'latest' }).on('data', (event) => { console.log('event: validation pool initiated', event); - fetchValidationPools(); + fetchValidationPool(event.returnValues.poolIndex); }); DAOContract.events.ValidationPoolResolved({ fromBlock: 'latest' }).on('data', (event) => { console.log('event: validation pool resolved', event); fetchReputation(); - fetchValidationPools(); + fetchValidationPool(event.returnValues.poolIndex); }); - }, [provider, account, chainId, reputation]); + + 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; @@ -236,43 +260,63 @@ function App() { } }; - const addPost = async () => { + /* -------------------------------------------------------------------------------- */ + /* --------------------------- BEGIN UI ACTIONS ----------------------------------- */ + /* -------------------------------------------------------------------------------- */ + + const addPost = useCallback(async () => { await DAO.methods.addPost(account).send({ from: account, gas: 1000000, }); - }; + }, [DAO, account]); - const initiateValidationPool = async (postIndex, poolDuration) => { + const initiateValidationPool = useCallback(async (postIndex, poolDuration) => { await DAO.methods.initiateValidationPool(postIndex, poolDuration ?? 3600).send({ from: account, gas: 1000000, value: 100, }); - }; + }, [DAO, account]); - const evaluateOutcome = async (poolIndex) => { + 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.current, duration).send({ + 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 - reputation.current = 0; - }, [DAO, account, reputation, chainId]); + setReputation(0); + }, [DAO, account, chainId, reputation, setReputation]); const requestWork = useCallback(async () => { const web3 = new Web3(provider); const priceWei = BigInt(web3.utils.toWei(work1Price, 'ether')); - console.log('requestWork, '); await work1.methods.requestWork().send({ from: account, gas: 1000000, @@ -280,6 +324,31 @@ function App() { }); }, [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 && } @@ -302,7 +371,7 @@ function App() { {`Balance: ${balanceEther} ETH`}
- {`Your REP: ${reputation.current}`} + {`Your REP: ${reputation}`}
{`Total REP: ${totalReputation}`} @@ -322,7 +391,7 @@ function App() { - {posts.map((post) => ( + {posts.filter((x) => !!x).map((post) => ( {post.id.toString()} {post.author} @@ -371,7 +440,7 @@ function App() { - {validationPools.map((pool) => ( + {validationPools.filter((x) => !!x).map((pool) => ( {pool.id.toString()} {pool.postIndex.toString()} @@ -381,7 +450,15 @@ function App() { {pool.stakeCount.toString()} {pool.status} - {!pool.resolved && ( + {!pool.resolved && reputation > 0 && pool.timeRemaining > 0 && ( + <> + + {' '} + + )} + {!pool.resolved && pool.timeRemaining <= 0 && ( @@ -421,14 +498,14 @@ function App() { - {availabilityStakes.map((stake) => ( - - {stake.id.toString()} - {stake.worker.toString()} - {stake.amount.toString()} - {new Date(Number(stake.endTime) * 1000).toLocaleString()} - {stake.assigned.toString()} - {stake.reclaimed.toString()} + {availabilityStakes.filter((x) => !!x).map((s) => ( + + {s.id.toString()} + {s.worker.toString()} + {s.amount.toString()} + {new Date(Number(s.endTime) * 1000).toLocaleString()} + {s.assigned.toString()} + {s.reclaimed.toString()} ))} @@ -453,10 +530,11 @@ function App() { Stake ID Approval Pool ID + Actions - {workRequests.map((request) => ( + {workRequests.filter((x) => !!x).map((request) => ( {request.id.toString()} {request.customer.toString()} @@ -469,6 +547,26 @@ function App() { {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 && ( + <> + + + + )} + ))}