Add callback for proposals
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 28s Details

This commit is contained in:
Ladd Hoffman 2024-03-30 17:21:26 -05:00
parent 2806eff0bb
commit 18693ac474
4 changed files with 135 additions and 82 deletions

View File

@ -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 ----------------------------------- */
/* -------------------------------------------------------------------------------- */

View File

@ -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({

View File

@ -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");

View File

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