From 18693ac474bd2f67bf0ae3f58d93d1019757eaaf Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Sat, 30 Mar 2024 17:21:26 -0500 Subject: [PATCH] Add callback for proposals --- client/src/App.jsx | 175 ++++++++++++++++------------ client/src/components/Proposals.jsx | 6 +- ethereum/contracts/Proposals.sol | 33 +++++- ethereum/test/Proposals.js | 3 +- 4 files changed, 135 insertions(+), 82 deletions(-) diff --git a/client/src/App.jsx b/client/src/App.jsx index 1fa59d3..15f03d2 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,5 +1,5 @@ import { - useCallback, useEffect, useState, useMemo, + useCallback, useEffect, useState, useMemo, useRef, } from 'react'; import { useSDK } from '@metamask/sdk-react'; import { Web3 } from 'web3'; @@ -31,6 +31,9 @@ function App() { sdk, connected, provider, chainId, account, balance, } = useSDK(); + const DAORef = useRef(); + const workRef = useRef(); + const onboardingRef = useRef(); const [DAO, setDAO] = useState(); const [work1, setWork1] = useState(); const [onboarding, setOnboarding] = useState(); @@ -54,9 +57,84 @@ function App() { account, chainId, posts, + DAORef, + workRef, + onboardingRef, }), [ - provider, DAO, work1, onboarding, reputation, setReputation, account, chainId, posts]); + provider, DAO, work1, onboarding, reputation, setReputation, account, chainId, posts, + DAORef, workRef, onboardingRef]); + useEffect(() => { + if (!provider || balance === undefined) return; + const web3 = new Web3(provider); + setBalanceEther(web3.utils.fromWei(balance, 'ether')); + }, [provider, balance]); + + /* -------------------------------------------------------------------------------- */ + /* --------------------------- BEGIN FETCHERS ------------------------------------- */ + /* -------------------------------------------------------------------------------- */ + + const fetchReputation = useCallback(async () => { + setReputation(await DAORef.current.methods.balanceOf(account).call()); + setTotalReputation(await DAORef.current.methods.totalSupply().call()); + }, [DAORef, account]); + + const fetchPost = useCallback(async (postIndex) => { + const p = await DAORef.current.methods.posts(postIndex).call(); + p.id = Number(p.id); + dispatchPost({ type: 'update', item: p }); + return p; + }, [DAORef, dispatchPost]); + + const fetchPosts = useCallback(async () => { + const count = await DAORef.current.methods.postCount().call(); + const promises = []; + dispatchPost({ type: 'refresh' }); + for (let i = 0; i < count; i += 1) { + promises.push(fetchPost(i)); + } + await Promise.all(promises); + }, [DAORef, dispatchPost, fetchPost]); + + const fetchValidationPool = useCallback(async (poolIndex) => { + const getPoolStatus = (pool) => { + if (pool.resolved) { + return pool.outcome ? 'Accepted' : 'Rejected'; + } + return pool.timeRemaining > 0 ? 'In Progress' : 'Ready to Evaluate'; + }; + const pool = await DAORef.current.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); + } + }, [DAORef, dispatchValidationPool]); + + const fetchValidationPools = useCallback(async () => { + // TODO: Pagination + // TODO: Memoization + // TODO: Caching + const count = await DAORef.current.methods.validationPoolCount().call(); + const promises = []; + dispatchValidationPool({ type: 'refresh' }); + for (let i = 0; i < count; i += 1) { + promises.push(fetchValidationPool(i)); + } + await Promise.all(promises); + }, [DAORef, dispatchValidationPool, fetchValidationPool]); + + /* -------------------------------------------------------------------------------- */ + /* --------------------------- END FETCHERS --------------------------------------- */ + /* -------------------------------------------------------------------------------- */ // In this effect, we initialize everything and add contract event listeners. useEffect(() => { if (!provider || !chainId || !account || balance === undefined) return () => {}; @@ -67,80 +145,26 @@ function App() { const DAOContract = new web3.eth.Contract(DAOArtifact.abi, DAOAddress); const Work1Contract = new web3.eth.Contract(Work1Artifact.abi, Work1Address); const OnboardingContract = new web3.eth.Contract(OnboardingArtifact.abi, OnboardingAddress); + DAORef.current = DAOContract; + workRef.current = Work1Contract; + onboardingRef.current = OnboardingContract; + + fetchReputation(); + fetchPosts(); + fetchValidationPools(); setDAO(DAOContract); setWork1(Work1Contract); setOnboarding(OnboardingContract); - /* -------------------------------------------------------------------------------- */ - /* --------------------------- BEGIN FETCHERS ------------------------------------- */ - /* -------------------------------------------------------------------------------- */ - - const fetchReputation = async () => { - setReputation(await DAOContract.methods.balanceOf(account).call()); - setTotalReputation(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)); + const fetchReputationInterval = setInterval(() => { + console.log('reputation', reputation); + if (reputation !== undefined) { + clearInterval(fetchReputationInterval); + return; } - await Promise.all(promises); - }; - - const fetchValidationPool = async (poolIndex) => { - const getPoolStatus = (pool) => { - if (pool.resolved) { - return pool.outcome ? 'Accepted' : 'Rejected'; - } - return pool.timeRemaining > 0 ? 'In Progress' : 'Ready to Evaluate'; - }; - 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(fetchValidationPool(i)); - } - await Promise.all(promises); - }; - - /* -------------------------------------------------------------------------------- */ - /* --------------------------- END FETCHERS --------------------------------------- */ - /* -------------------------------------------------------------------------------- */ - - fetchReputation(); - fetchPosts(); - fetchValidationPools(); + fetchReputation(); + }, 1000); /* -------------------------------------------------------------------------------- */ /* --------------------------- BEGIN EVENT HANDLERS ------------------------------- */ @@ -177,18 +201,15 @@ function App() { Work1Contract.events.AvailabilityStaked().off(); OnboardingContract.events.AvailabilityStaked().off(); }; - }, [provider, account, chainId, balance, dispatchValidationPool, dispatchPost]); + }, [provider, account, chainId, balance, dispatchValidationPool, dispatchPost, reputation, + DAORef, workRef, onboardingRef, + fetchPost, fetchPosts, fetchReputation, fetchValidationPool, fetchValidationPools, + ]); /* -------------------------------------------------------------------------------- */ /* --------------------------- END MAIN INITIALIZION EFFECT ----------------------- */ /* -------------------------------------------------------------------------------- */ - useEffect(() => { - if (!provider || balance === undefined) return; - const web3 = new Web3(provider); - setBalanceEther(web3.utils.fromWei(balance, 'ether')); - }, [provider, balance]); - /* -------------------------------------------------------------------------------- */ /* --------------------------- BEGIN UI ACTIONS ----------------------------------- */ /* -------------------------------------------------------------------------------- */ diff --git a/client/src/components/Proposals.jsx b/client/src/components/Proposals.jsx index 7df42a2..9d1cfe8 100644 --- a/client/src/components/Proposals.jsx +++ b/client/src/components/Proposals.jsx @@ -102,18 +102,22 @@ function Proposals() { }, [posts, setViewPost, setShowViewProposal]); const onSubmitProposal = useCallback(async (post) => { + const web3 = new Web3(provider); + const emptyCallbackData = web3.eth.abi.encodeParameter('bytes', '0x00'); // TODO: Make referenda durations configurable await proposalsContract.current.methods.propose( post.hash, durations[0], durations[1], durations[2], + false, + emptyCallbackData, ).send({ from: account, gas: 1000000, value: 10000, }); - }, [account, proposalsContract, durations]); + }, [provider, account, proposalsContract, durations]); const handleAttest = useCallback(async (proposalIndex) => { await proposalsContract.current.methods.attest(proposalIndex, reputation).send({ diff --git a/ethereum/contracts/Proposals.sol b/ethereum/contracts/Proposals.sol index 9748e98..ae2bd13 100644 --- a/ethereum/contracts/Proposals.sol +++ b/ethereum/contracts/Proposals.sol @@ -43,6 +43,8 @@ contract Proposals is DAOContract, IOnValidate { mapping(address => uint) attestations; uint attestationTotal; Referendum[3] referenda; + bool callbackOnValidate; + bytes callbackData; } mapping(uint => Proposal) public proposals; @@ -62,7 +64,9 @@ contract Proposals is DAOContract, IOnValidate { string calldata contentId, uint referendum0Duration, uint referendum1Duration, - uint referendum100Duration + uint referendum100Duration, + bool callbackOnValidate, + bytes calldata callbackData ) external payable returns (uint proposalIndex) { uint postIndex = dao.addPost(msg.sender, contentId); proposalIndex = proposalCount++; @@ -74,6 +78,8 @@ contract Proposals is DAOContract, IOnValidate { proposal.referenda[2].duration = referendum100Duration; proposal.fee = msg.value; proposal.remainingFee = proposal.fee; + proposal.callbackOnValidate = callbackOnValidate; + proposal.callbackData = callbackData; emit NewProposal(proposalIndex); } @@ -204,11 +210,32 @@ contract Proposals is DAOContract, IOnValidate { require(referendumIndex == 2, "Stage 2 index mismatch"); // Note that no retries are attempted for referendum 100% if (votePasses && participationAboveThreshold) { - // TODO: The proposal has passed all referenda and should become "law" + // The proposal has passed all referenda and should become "law" + proposal.stage = Stage.Accepted; // This is an opportunity for some actions to occur // We should at least emit an event - proposal.stage = Stage.Accepted; emit ProposalAccepted(proposalIndex); + // We also execute a callback, if requested + if (proposal.callbackOnValidate) { + try + // Note: We're directly reusing the onValidate hook we established for valdiation pools. + // if any contracts want to use both callbacks, distinct interfaces should be defined. + IOnValidate(proposal.sender).onValidate( + votePasses, + false, + stakedFor, + stakedAgainst, + proposal.callbackData + ) + { + console.log("proposal callbackOnValidate succeed"); + } catch Error(string memory reason) { + console.log( + "proposal callbackOnValidate failed:", + reason + ); + } + } } else if (referendum.retryCount >= 2) { proposal.stage = Stage.Failed; emit ProposalFailed(proposalIndex, "Retry count exceeded"); diff --git a/ethereum/test/Proposals.js b/ethereum/test/Proposals.js index b2a7bed..8dea35e 100644 --- a/ethereum/test/Proposals.js +++ b/ethereum/test/Proposals.js @@ -78,7 +78,8 @@ describe('Proposal', () => { account2, } = await loadFixture(deploy)); - await proposals.propose('proposal-content-id', 20, 20, 20, { value: 100 }); + const emptyCallbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []); + await proposals.propose('proposal-content-id', 20, 20, 20, false, emptyCallbackData, { value: 100 }); expect(await proposals.proposalCount()).to.equal(1); proposal = await proposals.proposals(0); expect(proposal.postIndex).to.equal(2);