dgf-prototype/client/src/App.jsx

513 lines
18 KiB
React
Raw Normal View History

2024-03-13 17:34:07 -05:00
import {
2024-03-30 17:21:26 -05:00
useCallback, useEffect, useState, useMemo, useRef,
2024-03-13 17:34:07 -05:00
} from 'react';
2024-03-07 21:27:37 -06:00
import { useSDK } from '@metamask/sdk-react';
2024-03-07 10:58:55 -06:00
import { Web3 } from 'web3';
2024-03-19 22:22:36 -05:00
2024-03-07 21:27:37 -06:00
import Button from 'react-bootstrap/Button';
2024-03-14 18:38:54 -05:00
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';
2024-03-20 09:32:50 -05:00
import './App.css';
2024-03-19 22:22:36 -05:00
import useList from './utils/List';
2024-03-20 16:30:27 -05:00
import { getContractAddressByChainId, getContractNameByAddress } from './utils/contract-config';
2024-03-19 22:22:36 -05:00
import Web3Context from './contexts/Web3Context';
2024-03-17 13:24:49 -05:00
import DAOArtifact from './assets/DAO.json';
2024-03-18 14:03:53 -05:00
import Work1Artifact from './assets/Work1.json';
import OnboardingArtifact from './assets/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';
2024-03-14 18:08:17 -05:00
2024-02-21 18:01:41 -06:00
function App() {
2024-03-07 21:27:37 -06:00
const {
sdk, connected, provider, chainId, account, balance,
} = useSDK();
2024-03-30 17:21:26 -05:00
const DAORef = useRef();
const workRef = useRef();
const onboardingRef = useRef();
2024-03-07 21:27:37 -06:00
const [DAO, setDAO] = useState();
2024-03-13 17:34:07 -05:00
const [work1, setWork1] = useState();
2024-03-18 14:03:53 -05:00
const [onboarding, setOnboarding] = useState();
2024-03-07 21:27:37 -06:00
const [balanceEther, setBalanceEther] = useState();
2024-03-14 14:40:27 -05:00
const [reputation, setReputation] = useState();
2024-03-10 19:39:15 -05:00
const [totalReputation, setTotalReputation] = useState();
2024-03-14 18:08:17 -05:00
const [posts, dispatchPost] = useList();
const [validationPools, dispatchValidationPool] = useList();
2024-03-07 21:27:37 -06:00
2024-03-19 22:22:36 -05:00
const [showAddPost, setShowAddPost] = useState(false);
const [showViewPost, setShowViewPost] = useState(false);
2024-03-29 12:19:34 -05:00
const [viewPost, setViewPost] = useState({});
2024-03-19 22:22:36 -05:00
2024-03-16 21:42:22 -05:00
const web3ProviderValue = useMemo(() => ({
provider,
DAO,
work1,
2024-03-18 14:03:53 -05:00
onboarding,
2024-03-16 21:42:22 -05:00
reputation,
setReputation,
account,
chainId,
2024-03-29 19:15:23 -05:00
posts,
2024-03-30 17:21:26 -05:00
DAORef,
workRef,
onboardingRef,
2024-03-16 21:42:22 -05:00
}), [
2024-03-30 17:21:26 -05:00
provider, DAO, work1, onboarding, reputation, setReputation, account, chainId, posts,
DAORef, workRef, onboardingRef]);
2024-03-16 21:42:22 -05:00
2024-03-30 17:21:26 -05:00
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 --------------------------------------- */
/* -------------------------------------------------------------------------------- */
2024-03-13 17:34:07 -05:00
// In this effect, we initialize everything and add contract event listeners.
2024-03-07 21:27:37 -06:00
useEffect(() => {
2024-03-29 10:59:29 -05:00
if (!provider || !chainId || !account || balance === undefined) return () => {};
2024-03-20 16:30:27 -05:00
const DAOAddress = getContractAddressByChainId(chainId, 'DAO');
const Work1Address = getContractAddressByChainId(chainId, 'Work1');
const OnboardingAddress = getContractAddressByChainId(chainId, 'Onboarding');
2024-03-07 21:27:37 -06:00
const web3 = new Web3(provider);
const DAOContract = new web3.eth.Contract(DAOArtifact.abi, DAOAddress);
2024-03-18 14:03:53 -05:00
const Work1Contract = new web3.eth.Contract(Work1Artifact.abi, Work1Address);
const OnboardingContract = new web3.eth.Contract(OnboardingArtifact.abi, OnboardingAddress);
2024-03-30 17:21:26 -05:00
DAORef.current = DAOContract;
workRef.current = Work1Contract;
onboardingRef.current = OnboardingContract;
fetchReputation();
fetchPosts();
fetchValidationPools();
2024-03-18 14:03:53 -05:00
setDAO(DAOContract);
setWork1(Work1Contract);
setOnboarding(OnboardingContract);
2024-03-10 19:39:15 -05:00
2024-03-30 17:21:26 -05:00
const fetchReputationInterval = setInterval(() => {
console.log('reputation', reputation);
if (reputation !== undefined) {
clearInterval(fetchReputationInterval);
return;
2024-03-12 17:53:04 -05:00
}
2024-03-30 17:21:26 -05:00
fetchReputation();
}, 1000);
2024-03-10 11:55:59 -05:00
/* -------------------------------------------------------------------------------- */
/* --------------------------- BEGIN EVENT HANDLERS ------------------------------- */
/* -------------------------------------------------------------------------------- */
2024-03-12 18:02:07 -05:00
DAOContract.events.PostAdded({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: post added');
2024-03-14 14:40:27 -05:00
fetchPost(event.returnValues.postIndex);
2024-03-12 18:02:07 -05:00
});
2024-03-10 11:55:59 -05:00
DAOContract.events.ValidationPoolInitiated({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: validation pool initiated');
2024-03-14 14:40:27 -05:00
fetchValidationPool(event.returnValues.poolIndex);
2024-03-10 11:55:59 -05:00
});
DAOContract.events.ValidationPoolResolved({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: validation pool resolved');
2024-03-10 19:39:15 -05:00
fetchReputation();
2024-03-14 14:40:27 -05:00
fetchValidationPool(event.returnValues.poolIndex);
});
2024-03-18 14:03:53 -05:00
Work1Contract.events.AvailabilityStaked({ fromBlock: 'latest' }).on('data', () => {
fetchReputation();
});
2024-03-18 14:03:53 -05:00
OnboardingContract.events.AvailabilityStaked({ fromBlock: 'latest' }).on('data', () => {
fetchReputation();
});
2024-03-29 10:59:29 -05:00
return () => {
DAOContract.events.PostAdded().off();
DAOContract.events.ValidationPoolInitiated().off();
DAOContract.events.ValidationPoolResolved().off();
Work1Contract.events.AvailabilityStaked().off();
OnboardingContract.events.AvailabilityStaked().off();
};
2024-03-30 17:21:26 -05:00
}, [provider, account, chainId, balance, dispatchValidationPool, dispatchPost, reputation,
DAORef, workRef, onboardingRef,
fetchPost, fetchPosts, fetchReputation, fetchValidationPool, fetchValidationPools,
]);
2024-03-14 14:40:27 -05:00
/* -------------------------------------------------------------------------------- */
/* --------------------------- END MAIN INITIALIZION EFFECT ----------------------- */
/* -------------------------------------------------------------------------------- */
2024-03-07 21:27:37 -06:00
2024-03-14 18:08:17 -05:00
/* -------------------------------------------------------------------------------- */
/* --------------------------- BEGIN UI ACTIONS ----------------------------------- */
/* -------------------------------------------------------------------------------- */
const connect = useCallback(async () => {
2024-03-07 21:27:37 -06:00
try {
await sdk?.connect();
} catch (err) {
console.warn('failed to connect..', err);
}
2024-03-14 18:08:17 -05:00
}, [sdk]);
2024-03-07 21:27:37 -06:00
2024-03-14 18:08:17 -05:00
const disconnect = useCallback(async () => {
2024-03-07 21:27:37 -06:00
try {
sdk?.terminate();
} catch (err) {
console.warn('failed to disconnect..', err);
}
2024-03-14 18:08:17 -05:00
}, [sdk]);
2024-03-07 21:27:37 -06:00
const watchReputationToken = useCallback(async () => {
await provider.request({
method: 'wallet_watchAsset',
params: {
type: 'ERC20',
options: {
2024-03-20 16:30:27 -05:00
address: getContractAddressByChainId(chainId, 'DAO'),
},
},
});
}, [provider, chainId]);
2024-03-14 14:40:27 -05:00
const initiateValidationPool = useCallback(async (postIndex, poolDuration) => {
2024-03-18 14:03:53 -05:00
const web3 = new Web3(provider);
await DAO.methods.initiateValidationPool(
postIndex,
poolDuration ?? 3600,
2024-03-28 15:06:14 -05:00
[1, 3],
[1, 2],
100,
true,
2024-03-18 14:03:53 -05:00
false,
web3.eth.abi.encodeParameter('bytes', '0x00'),
).send({
2024-03-07 21:27:37 -06:00
from: account,
gas: 1000000,
2024-03-29 18:08:30 -05:00
value: 10000,
2024-03-07 10:58:55 -06:00
});
2024-03-18 14:03:53 -05:00
}, [provider, DAO, account]);
2024-03-07 10:58:55 -06:00
2024-03-14 14:40:27 -05:00
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 - BigInt(amount));
2024-03-14 14:40:27 -05:00
}, [DAO, account, setReputation]);
const stakeHalfInFavor = useCallback(async (poolIndex) => {
await stake(poolIndex, reputation / BigInt(2), true);
2024-03-14 14:40:27 -05:00
}, [stake, reputation]);
const evaluateOutcome = useCallback(async (poolIndex) => {
2024-03-12 17:53:04 -05:00
await DAO.methods.evaluateOutcome(poolIndex).send({
2024-03-07 21:27:37 -06:00
from: account,
gas: 1000000,
});
2024-03-14 14:40:27 -05:00
}, [DAO, account]);
2024-03-07 21:27:37 -06:00
2024-03-19 22:22:36 -05:00
const handleShowAddPost = () => setShowAddPost(true);
2024-03-20 14:53:34 -05:00
2024-03-29 12:19:34 -05:00
const handleShowViewPost = useCallback(async ({ contentId }) => {
const post = await Post.read(contentId);
setViewPost(post);
2024-03-19 22:22:36 -05:00
setShowViewPost(true);
2024-03-29 12:19:34 -05:00
}, [setViewPost, setShowViewPost]);
2024-03-19 22:22:36 -05:00
2024-03-14 14:40:27 -05:00
/* -------------------------------------------------------------------------------- */
/* --------------------------- END UI ACTIONS ------------------------------------- */
/* -------------------------------------------------------------------------------- */
2024-03-29 18:08:30 -05:00
const getAddressName = useCallback((address) => {
2024-03-18 14:03:53 -05:00
const contractName = getContractNameByAddress(chainId, address);
if (contractName) return `${contractName} Contract`;
2024-03-29 18:08:30 -05:00
const addressParts = [
address.slice(0, 7),
address.slice(address.length - 5),
];
return addressParts.join('...');
2024-03-18 14:03:53 -05:00
}, [chainId]);
2024-02-21 18:01:41 -06:00
return (
2024-03-16 20:34:36 -05:00
<Web3Context.Provider value={web3ProviderValue}>
2024-03-19 22:22:36 -05:00
<AddPostModal show={showAddPost} setShow={setShowAddPost} postToBlockchain />
2024-03-19 22:22:36 -05:00
2024-03-29 12:19:34 -05:00
<ViewPostModal show={showViewPost} setShow={setShowViewPost} post={viewPost} />
2024-03-19 22:22:36 -05:00
2024-03-07 21:27:37 -06:00
{!connected && <Button onClick={() => connect()}>Connect</Button>}
{connected && (
<>
2024-03-14 18:38:54 -05:00
<Container>
<Row>
<Col>
{chainId !== '0xaa36a7' && (
2024-03-14 18:38:54 -05:00
<div>
Please switch MetaMask to Sepolia testnet!
</div>
)}
</Col>
</Row>
<Row>
<Col>
<Stack>
<div>
{chainId && `Chain ID: ${chainId}`}
</div>
<div>
{`Account: ${account}`}
</div>
<div>
{`Balance: ${balanceEther} ETH`}
</div>
</Stack>
</Col>
<Col>
<Stack>
<div>
{`Your REP: ${reputation?.toString()}`}
2024-03-14 18:38:54 -05:00
</div>
<div>
{`Total REP: ${totalReputation?.toString()}`}
2024-03-14 18:38:54 -05:00
</div>
<div>
<Button onClick={() => disconnect()}>Disconnect</Button>
<Button onClick={() => watchReputationToken()}>Watch REP in MetaMask</Button>
2024-03-14 18:38:54 -05:00
</div>
</Stack>
</Col>
</Row>
</Container>
<Tabs>
<Tab eventKey="admin" title="Admin">
<h2>Posts</h2>
<div>
<Button onClick={handleShowAddPost}>Add Post</Button>
</div>
2024-03-14 18:38:54 -05:00
<div>
{`Posts count: ${posts.length}`}
</div>
<div>
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>Author</th>
<th>Sender</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{posts.filter((x) => !!x).map((post) => (
<tr key={post.id}>
<td>{post.id.toString()}</td>
2024-03-29 18:08:30 -05:00
<td>{getAddressName(post.author)}</td>
<td>{getAddressName(post.sender)}</td>
2024-03-14 18:38:54 -05:00
<td>
2024-03-19 22:22:36 -05:00
<Button onClick={() => handleShowViewPost(post)}>
View Post
</Button>
{' '}
2024-03-14 18:38:54 -05:00
Initiate Validation Pool
{' '}
<Button onClick={() => initiateValidationPool(post.id, 20)}>
20 s
2024-03-14 18:38:54 -05:00
</Button>
{' '}
<Button onClick={() => initiateValidationPool(post.id, 3600)}>
1 Hr.
2024-03-14 14:40:27 -05:00
</Button>
{' '}
2024-03-14 18:38:54 -05:00
<Button onClick={() => initiateValidationPool(post.id, 86400)}>
1 Day
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<h2>Validation Pools</h2>
2024-03-14 18:38:54 -05:00
<div>
{`Validation Pool Count: ${validationPools.length}`}
</div>
<div>
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>Post ID</th>
2024-03-18 14:03:53 -05:00
<th>Sender</th>
2024-03-14 18:38:54 -05:00
<th>Fee</th>
2024-03-29 18:08:30 -05:00
<th>Binding</th>
<th>Quorum</th>
<th>WinRatio</th>
<th>
Redistribute
<br />
Losing Stakes
</th>
2024-03-14 18:38:54 -05:00
<th>Duration</th>
<th>End Time</th>
<th>
Stake
<br />
Count
</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{validationPools.filter((x) => !!x).map((pool) => (
<tr key={pool.id}>
<td>{pool.id.toString()}</td>
<td>{pool.postIndex.toString()}</td>
2024-03-29 18:08:30 -05:00
<td>{getAddressName(pool.sender)}</td>
2024-03-14 18:38:54 -05:00
<td>{pool.fee.toString()}</td>
2024-03-29 18:08:30 -05:00
<td>
{pool.params.bindingPercent.toString()}
%
</td>
2024-03-30 15:59:02 -05:00
<td>{`${pool.params.quorum[0].toString()}/${pool.params.quorum[1].toString()}`}</td>
<td>{`${pool.params.winRatio[0].toString()}/${pool.params.winRatio[1].toString()}`}</td>
2024-03-29 18:08:30 -05:00
<td>{pool.params.redistributeLosingStakes.toString()}</td>
<td>{pool.params.duration.toString()}</td>
2024-03-14 18:38:54 -05:00
<td>{new Date(Number(pool.endTime) * 1000).toLocaleString()}</td>
<td>{pool.stakeCount.toString()}</td>
<td>{pool.status}</td>
<td>
{!pool.resolved && reputation > 0 && pool.timeRemaining > 0 && (
<>
<Button onClick={() => stakeHalfInFavor(pool.id)}>
Stake 1/2 REP
2024-03-14 18:38:54 -05:00
</Button>
{' '}
</>
)}
{!pool.resolved && pool.timeRemaining <= 0 && (
<Button onClick={() => evaluateOutcome(pool.id)}>
Evaluate Outcome
</Button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</Tab>
<Tab eventKey="worker" title="Worker">
2024-03-18 14:03:53 -05:00
{work1 && (
<WorkContract workContract={work1} title="Work Contract 1" verb="Work" />
)}
{onboarding && (
<WorkContract workContract={onboarding} title="Onboarding" verb="Onboarding" showRequestWork />
)}
2024-03-14 18:38:54 -05:00
</Tab>
<Tab eventKey="customer" title="Customer">
2024-03-18 14:03:53 -05:00
{work1 && (
<WorkContract
workContract={work1}
showAvailabilityActions={false}
showAvailabilityAmount={false}
onlyShowAvailable
title="Work Contract 1"
verb="Work"
showRequestWork
/>
)}
2024-03-14 18:38:54 -05:00
</Tab>
<Tab eventKey="proposals" title="Proposals">
<Proposals />
</Tab>
2024-03-14 18:38:54 -05:00
</Tabs>
2024-03-07 21:27:37 -06:00
</>
2024-03-07 10:58:55 -06:00
)}
2024-03-16 20:34:36 -05:00
</Web3Context.Provider>
);
}
2024-02-21 18:01:41 -06:00
export default App;