import { useCallback, useEffect, useState, useMemo, useRef, } from 'react'; import { useSDK } from '@metamask/sdk-react'; import { Web3 } from 'web3'; import Button from 'react-bootstrap/Button'; import Tab from 'react-bootstrap/Tab'; import Tabs from 'react-bootstrap/Tabs'; import Container from 'react-bootstrap/Container'; import Row from 'react-bootstrap/Row'; import Col from 'react-bootstrap/Col'; import Stack from 'react-bootstrap/Stack'; import { WidgetApi } from 'matrix-widget-api'; import './App.css'; import useList from './utils/List'; import { getContractAddressByChainId } from './utils/contract-config'; import Web3Context from './contexts/Web3Context'; import DAOArtifact from '../contractArtifacts/DAO.json'; import Work1Artifact from '../contractArtifacts/Work1.json'; import OnboardingArtifact from '../contractArtifacts/Onboarding.json'; import WorkContract from './components/work-contracts/WorkContract'; import AddPostModal from './components/posts/AddPostModal'; import ViewPostModal from './components/posts/ViewPostModal'; import Post from './utils/Post'; import Proposals from './components/Proposals'; import ImportPaper from './components/ImportPaper'; import ImportPapersByAuthor from './components/ImportPapersByAuthor'; import getAddressName from './utils/get-address-name'; function App() { const { 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(); const [balanceEther, setBalanceEther] = useState(); const [reputation, setReputation] = useState(); const [totalReputation, setTotalReputation] = useState(); const [members, dispatchMember] = useList(); const [posts, dispatchPost] = useList(); const [validationPools, dispatchValidationPool] = useList(); const [showAddPost, setShowAddPost] = useState(false); const [showViewPost, setShowViewPost] = useState(false); const [viewPost, setViewPost] = useState({}); const [widgetId, setWidgetId] = useState(); const [matrixUserId, setMatrixUserId] = useState(); const widgetApi = useRef(); const web3ProviderValue = useMemo(() => ({ provider, DAO, work1, onboarding, reputation, setReputation, account, chainId, posts, DAORef, workRef, onboardingRef, }), [ 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 (postId) => { const post = await DAORef.current.methods.posts(postId).call(); post.authors = await DAORef.current.methods.getPostAuthors(postId).call(); dispatchPost({ type: 'updateById', item: post }); return post; }, [DAORef, dispatchPost]); const fetchPostId = useCallback(async (postIndex) => { const postId = await DAORef.current.methods.postIds(postIndex).call(); return postId; }, [DAORef]); const fetchPosts = useCallback(async () => { const count = await DAORef.current.methods.postCount().call(); let promises = []; dispatchPost({ type: 'refresh' }); for (let i = 0; i < count; i += 1) { promises.push(fetchPostId(i)); } const postIds = await Promise.all(promises); promises = []; postIds.forEach((postId) => { promises.push(fetchPost(postId)); }); }, [DAORef, dispatchPost, fetchPost, fetchPostId]); 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]); const fetchMember = useCallback(async (memberIndex) => { const id = await DAORef.current.methods.members(memberIndex).call(); const member = { id }; member.reputation = await DAORef.current.methods.balanceOf(id).call(); dispatchMember({ type: 'updateById', item: member }); return member; }, [DAORef, dispatchMember]); const fetchMembers = useCallback(async () => { const count = await DAORef.current.methods.memberCount().call(); const promises = []; dispatchMember({ type: 'refresh' }); for (let i = 0; i < count; i += 1) { promises.push(fetchMember(i)); } await Promise.all(promises); }, [DAORef, dispatchMember, fetchMember]); /* -------------------------------------------------------------------------------- */ /* --------------------------- END FETCHERS --------------------------------------- */ /* -------------------------------------------------------------------------------- */ // In this effect, we initialize everything and add contract event listeners. useEffect(() => { if (!provider || !chainId || !account || balance === undefined) return () => {}; const DAOAddress = getContractAddressByChainId(chainId, 'DAO'); const Work1Address = getContractAddressByChainId(chainId, 'Work1'); const OnboardingAddress = getContractAddressByChainId(chainId, 'Onboarding'); const web3 = new Web3(provider); 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(); fetchMembers(); fetchPosts(); fetchValidationPools(); setDAO(DAOContract); setWork1(Work1Contract); setOnboarding(OnboardingContract); // const fetchReputationInterval = setInterval(() => { // // console.log('reputation', reputation); // if (reputation !== undefined) { // clearInterval(fetchReputationInterval); // return; // } // fetchReputation(); // }, 1000); /* -------------------------------------------------------------------------------- */ /* --------------------------- BEGIN EVENT HANDLERS ------------------------------- */ /* -------------------------------------------------------------------------------- */ DAOContract.events.PostAdded({ fromBlock: 'latest' }).on('data', (event) => { console.log('event: post added'); fetchPost(event.returnValues.id); }); DAOContract.events.ValidationPoolInitiated({ fromBlock: 'latest' }).on('data', (event) => { console.log('event: validation pool initiated'); fetchValidationPool(event.returnValues.poolIndex); }); DAOContract.events.ValidationPoolResolved({ fromBlock: 'latest' }).on('data', (event) => { console.log('event: validation pool resolved'); fetchReputation(); fetchValidationPool(event.returnValues.poolIndex); fetchMembers(); }); Work1Contract.events.AvailabilityStaked({ fromBlock: 'latest' }).on('data', () => { fetchReputation(); }); OnboardingContract.events.AvailabilityStaked({ fromBlock: 'latest' }).on('data', () => { fetchReputation(); }); return () => { DAOContract.events.PostAdded().off(); DAOContract.events.ValidationPoolInitiated().off(); DAOContract.events.ValidationPoolResolved().off(); Work1Contract.events.AvailabilityStaked().off(); OnboardingContract.events.AvailabilityStaked().off(); }; }, [provider, account, chainId, balance, dispatchValidationPool, dispatchPost, reputation, DAORef, workRef, onboardingRef, fetchPost, fetchPosts, fetchReputation, fetchValidationPool, fetchValidationPools, fetchMembers, ]); /* -------------------------------------------------------------------------------- */ /* --------------------------- END MAIN INITIALIZION EFFECT ----------------------- */ /* -------------------------------------------------------------------------------- */ useEffect(() => { const params = new URL(document.location).searchParams; setWidgetId(params.get('widgetId')); setMatrixUserId(params.get('userId')); }, []); useEffect(() => { if (widgetId) { widgetApi.current = new WidgetApi(widgetId); } }, [widgetId]); /* -------------------------------------------------------------------------------- */ /* --------------------------- BEGIN UI ACTIONS ----------------------------------- */ /* -------------------------------------------------------------------------------- */ const connect = useCallback(async () => { try { await sdk?.connect(); } catch (err) { console.warn('failed to connect..', err); } }, [sdk]); const disconnect = useCallback(async () => { try { sdk?.terminate(); } catch (err) { console.warn('failed to disconnect..', err); } }, [sdk]); const watchReputationToken = useCallback(async () => { await provider.request({ method: 'wallet_watchAsset', params: { type: 'ERC20', options: { address: getContractAddressByChainId(chainId, 'DAO'), }, }, }); }, [provider, chainId]); const initiateValidationPool = useCallback(async (postId, poolDuration) => { const web3 = new Web3(provider); await DAO.methods.initiateValidationPool( postId, poolDuration ?? 3600, [1, 3], [1, 2], 100, true, false, web3.eth.abi.encodeParameter('bytes', '0x00'), ).send({ from: account, gas: 1000000, value: 10000, }); }, [provider, DAO, account]); const stake = useCallback(async (poolIndex, amount, inFavor) => { console.log(`Attempting to stake ${amount} ${inFavor ? 'for' : 'against'} pool ${poolIndex}`); await DAO.methods.stakeOnValidationPool(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 - BigInt(amount)); }, [DAO, account, setReputation]); const stakeHalfInFavor = useCallback(async (poolIndex) => { await stake(poolIndex, reputation / BigInt(2), true); }, [stake, reputation]); const evaluateOutcome = useCallback(async (poolIndex) => { await DAO.methods.evaluateOutcome(poolIndex).send({ from: account, gas: 10000000, }); }, [DAO, account]); const handleShowAddPost = () => setShowAddPost(true); const handleShowViewPost = useCallback(async ({ id }) => { const post = await Post.read(id); setViewPost(post); setShowViewPost(true); }, [setViewPost, setShowViewPost]); // TODO: Sign and send a message to the forum-api bot / to a room in matrix const registerMatrixIdentity = async () => { console.log('matrix user id', matrixUserId); }; /* -------------------------------------------------------------------------------- */ /* --------------------------- END UI ACTIONS ------------------------------------- */ /* -------------------------------------------------------------------------------- */ return ( {!connected && } {connected && ( <> {chainId !== '0xaa36a7' && (
Please switch MetaMask to Sepolia testnet!
)}
{chainId && `Chain ID: ${chainId}`}
{`Account: ${account}`}
{`Balance: ${balanceEther} ETH`}
{`Your REP: ${reputation?.toString()}`}
{`Total REP: ${totalReputation?.toString()}`}
{!!widgetId && ( )}

Members

{`Members count: ${members.length}`}
{members.filter((x) => !!x).map((member) => ( ))}
ID Reputation
{member.id} {member.reputation.toString()}
{' '}

Posts

{`Posts count: ${posts.length}`}
{posts.filter((x) => !!x).map((post) => ( ))}
ID Authors Sender Reputation Actions
{post.id.toString()} {post.authors.map(({ authorAddress, weightPPM }) => (
{getAddressName(chainId, authorAddress)} {' '} {Number(weightPPM) / 10000} %
))}
{getAddressName(chainId, post.sender)} {post.reputation.toString()} {' '} Initiate Validation Pool {' '} {' '} {' '}

Validation Pools

{`Validation Pool Count: ${validationPools.length}`}
{validationPools.filter((x) => !!x).map((pool) => ( ))}
ID Post ID Sender Fee Binding Quorum WinRatio Redistribute
Losing Stakes
Duration End Time Stake
Count
Status Actions
{pool.id.toString()} {pool.postId} {getAddressName(chainId, pool.sender)} {pool.fee.toString()} {pool.params.bindingPercent.toString()} % {`${pool.params.quorum[0].toString()}/${pool.params.quorum[1].toString()}`} {`${pool.params.winRatio[0].toString()}/${pool.params.winRatio[1].toString()}`} {pool.params.redistributeLosingStakes.toString()} {pool.params.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 || !reputation) && ( )}
{work1 && ( )} {onboarding && ( )} {work1 && ( )}

Semantic Scholar Import

)}
); } export default App;