dgf-prototype/client/src/App.jsx

485 lines
16 KiB
React
Raw Normal View History

2024-03-13 17:34:07 -05:00
import {
useCallback, useEffect, useRef, useState,
} 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-07 21:27:37 -06:00
import Button from 'react-bootstrap/Button';
2024-03-07 21:27:37 -06:00
import DAOArtifact from './assets/DAO.json';
2024-03-13 17:34:07 -05:00
import work1Artifact from './assets/Work1.json';
2024-03-10 19:39:15 -05:00
const contracts = {
'0x539': { // Hardhat
2024-03-13 12:02:08 -05:00
DAO: '0x8d914D38dD301FC4606f5aa9fEcF8A76389020d3',
Work1: '0x050C420Cc4995B41217Eba1B54B82Fd5687e9139',
2024-03-10 19:39:15 -05:00
},
'0xaa36a7': { // Sepolia
2024-03-12 18:02:07 -05:00
DAO: '0x8F00038542C87A5eAf18d5938B7723bF2A04A4e4',
Work1: '0x42b79f8d8408c36aD4347ab72f826684440a7a8F',
2024-03-10 19:39:15 -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();
const [DAO, setDAO] = useState();
2024-03-13 17:34:07 -05:00
const [work1, setWork1] = useState();
const [work1Price, setWork1Price] = useState();
2024-03-07 21:27:37 -06:00
const [balanceEther, setBalanceEther] = useState();
2024-03-13 12:02:08 -05:00
const reputation = useRef();
2024-03-10 19:39:15 -05:00
const [totalReputation, setTotalReputation] = useState();
2024-03-12 17:53:04 -05:00
const [posts, setPosts] = useState([]);
const [validationPools, setValidationPools] = useState([]);
2024-03-13 12:02:08 -05:00
const stakedPools = useRef([]);
2024-03-13 17:34:07 -05:00
const [availabilityStakes, setAvailabilityStakes] = useState([]);
2024-03-13 17:56:29 -05:00
const [workRequests, setWorkRequests] = useState([]);
2024-03-10 11:55:59 -05:00
2024-03-10 19:39:15 -05:00
// const watchReputationToken = useCallback(async () => {
// await provider.request({
// method: 'wallet_watchAsset',
// params: {
// type: 'ERC20',
// options: {
// address: DAOAddress,
// },
// },
// });
// }, [provider]);
2024-03-07 21:27:37 -06:00
2024-03-13 17:56:29 -05:00
const getPoolStatus = (pool) => {
2024-03-13 12:02:08 -05:00
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';
};
2024-03-13 17:56:29 -05:00
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';
}
};
2024-03-13 17:34:07 -05:00
// In this effect, we initialize everything and add contract event listeners.
// TODO: Refactor -- make separate, functional components?
2024-03-07 21:27:37 -06:00
useEffect(() => {
2024-03-10 19:39:15 -05:00
if (!provider || !chainId || !account) return;
if (!contracts[chainId]) return;
2024-03-07 21:27:37 -06:00
const web3 = new Web3(provider);
2024-03-10 19:39:15 -05:00
const DAOContract = new web3.eth.Contract(DAOArtifact.abi, contracts[chainId].DAO);
2024-03-13 17:34:07 -05:00
const work1Contract = new web3.eth.Contract(work1Artifact.abi, contracts[chainId].Work1);
2024-03-10 19:39:15 -05:00
2024-03-13 17:34:07 -05:00
const fetchPrice = async () => {
const fetchedPrice = await work1Contract.methods.price().call();
setWork1Price(web3.utils.fromWei(fetchedPrice, 'ether'));
};
2024-03-10 11:55:59 -05:00
const fetchReputation = async () => {
2024-03-13 12:02:08 -05:00
reputation.current = Number(await DAOContract.methods.balanceOf(account).call());
2024-03-10 19:39:15 -05:00
setTotalReputation(await DAOContract.methods.totalSupply().call());
2024-03-07 21:27:37 -06:00
};
2024-03-10 11:55:59 -05:00
2024-03-12 17:53:04 -05:00
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());
}
const fetchedPosts = await Promise.all(promises);
setPosts(fetchedPosts);
};
2024-03-13 12:02:08 -05:00
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,
});
// 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);
};
const stakeAllInFavor = (poolIndex) => stake(poolIndex, reputation.current, true);
2024-03-12 17:53:04 -05:00
const fetchValidationPools = async () => {
2024-03-13 12:02:08 -05:00
// TODO: Pagination
// TODO: Memoization
// TODO: Caching
2024-03-12 17:53:04 -05:00
const count = await DAOContract.methods.validationPoolCount().call();
const promises = [];
for (let i = 0; i < count; i += 1) {
promises.push(DAOContract.methods.validationPools(i).call());
}
2024-03-13 12:02:08 -05:00
const pools = (await Promise.all(promises)).map((p) => {
const pool = p;
2024-03-13 17:56:29 -05:00
pool.status = getPoolStatus(pool);
2024-03-13 12:02:08 -05:00
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);
}
// 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;
});
2024-03-12 17:53:04 -05:00
setValidationPools(pools);
2024-03-07 21:27:37 -06:00
};
2024-03-10 11:55:59 -05:00
2024-03-13 17:34:07 -05:00
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());
}
const fetchedStakes = (await Promise.all(promises)).map((x, index) => {
Object.assign(x, { id: index });
return x;
});
setAvailabilityStakes(fetchedStakes);
};
2024-03-13 17:56:29 -05:00
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());
}
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);
};
2024-03-13 17:34:07 -05:00
fetchPrice();
2024-03-10 11:55:59 -05:00
fetchReputation();
2024-03-12 17:53:04 -05:00
fetchPosts();
fetchValidationPools();
2024-03-13 17:34:07 -05:00
fetchAvailabilityStakes();
2024-03-13 17:56:29 -05:00
fetchWorkRequests();
2024-03-13 17:34:07 -05:00
setWork1(work1Contract);
2024-03-07 21:27:37 -06:00
setDAO(DAOContract);
2024-03-10 11:55:59 -05:00
2024-03-12 18:02:07 -05:00
DAOContract.events.PostAdded({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: post added', event);
fetchPosts();
});
2024-03-10 11:55:59 -05:00
DAOContract.events.ValidationPoolInitiated({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: validation pool initiated', event);
2024-03-12 17:53:04 -05:00
fetchValidationPools();
2024-03-10 11:55:59 -05:00
});
DAOContract.events.ValidationPoolResolved({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: validation pool resolved', event);
2024-03-10 19:39:15 -05:00
fetchReputation();
2024-03-12 17:53:04 -05:00
fetchValidationPools();
2024-03-10 11:55:59 -05:00
});
2024-03-13 12:02:08 -05:00
}, [provider, account, chainId, reputation]);
2024-03-07 21:27:37 -06:00
useEffect(() => {
2024-03-10 19:39:15 -05:00
if (!provider || balance === undefined) return;
const web3 = new Web3(provider);
setBalanceEther(web3.utils.fromWei(balance, 'ether'));
2024-03-07 21:27:37 -06:00
}, [provider, balance]);
const connect = async () => {
try {
await sdk?.connect();
} catch (err) {
console.warn('failed to connect..', err);
}
};
const disconnect = async () => {
try {
sdk?.terminate();
} catch (err) {
console.warn('failed to disconnect..', err);
}
};
2024-03-12 17:53:04 -05:00
const addPost = async () => {
await DAO.methods.addPost(account).send({
from: account,
gas: 1000000,
});
};
const initiateValidationPool = async (postIndex, poolDuration) => {
await DAO.methods.initiateValidationPool(postIndex, poolDuration ?? 3600).send({
2024-03-07 21:27:37 -06:00
from: account,
gas: 1000000,
value: 100,
2024-03-07 10:58:55 -06:00
});
2024-03-07 21:27:37 -06:00
};
2024-03-07 10:58:55 -06:00
2024-03-12 17:53:04 -05:00
const evaluateOutcome = async (poolIndex) => {
await DAO.methods.evaluateOutcome(poolIndex).send({
2024-03-07 21:27:37 -06:00
from: account,
gas: 1000000,
});
};
2024-03-13 17:34:07 -05:00
const stakeAvailability = useCallback(async () => {
const duration = 300; // 5 minutes
const target = contracts[chainId].Work1;
await DAO.methods.stakeAvailability(target, reputation.current, 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]);
2024-03-07 21:27:37 -06:00
2024-03-13 17:34:07 -05:00
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,
value: priceWei,
});
}, [provider, work1, account, work1Price]);
2024-03-07 10:58:55 -06:00
2024-02-21 18:01:41 -06:00
return (
<>
2024-03-07 21:27:37 -06:00
{!connected && <Button onClick={() => connect()}>Connect</Button>}
{connected && (
<>
<div>
2024-03-10 19:39:15 -05:00
{!contracts[chainId] && (
<div>
Please switch MetaMask to Sepolia testnet!
</div>
)}
2024-03-07 21:27:37 -06:00
<div>
{chainId && `Chain ID: ${chainId}`}
</div>
<div>
{`Account: ${account}`}
</div>
<div>
{`Balance: ${balanceEther} ETH`}
</div>
<div>
2024-03-13 12:02:08 -05:00
{`Your REP: ${reputation.current}`}
2024-03-10 19:39:15 -05:00
</div>
<div>
{`Total REP: ${totalReputation}`}
2024-03-07 21:27:37 -06:00
</div>
<Button onClick={() => disconnect()}>Disconnect</Button>
</div>
<div>
2024-03-12 17:53:04 -05:00
{`Posts count: ${posts.length}`}
</div>
<div>
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>Author</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{posts.map((post) => (
<tr key={post.id}>
<td>{post.id.toString()}</td>
<td>{post.author}</td>
<td>
Initiate Validation Pool
{' '}
<Button onClick={() => initiateValidationPool(post.id, 60)}>
1 Min.
</Button>
{' '}
<Button onClick={() => initiateValidationPool(post.id, 3600)}>
1 Hr.
</Button>
{' '}
<Button onClick={() => initiateValidationPool(post.id, 86400)}>
1 Day
2024-03-12 17:53:04 -05:00
</Button>
</td>
</tr>
))}
</tbody>
</table>
2024-03-07 21:27:37 -06:00
</div>
<div>
2024-03-12 17:53:04 -05:00
<Button onClick={() => addPost()}>Add Post</Button>
2024-03-07 21:27:37 -06:00
</div>
<div>
2024-03-12 17:53:04 -05:00
{`Validation Pool Count: ${validationPools.length}`}
2024-03-07 21:27:37 -06:00
</div>
2024-03-10 11:55:59 -05:00
<div>
2024-03-12 17:53:04 -05:00
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>Post ID</th>
<th>Fee</th>
<th>Duration</th>
<th>End Time</th>
2024-03-13 12:02:08 -05:00
<th>
Stake
<br />
Count
</th>
2024-03-12 17:53:04 -05:00
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{validationPools.map((pool) => (
<tr key={pool.id}>
<td>{pool.id.toString()}</td>
<td>{pool.postIndex.toString()}</td>
<td>{pool.fee.toString()}</td>
<td>{pool.duration.toString()}</td>
<td>{new Date(Number(pool.endTime) * 1000).toLocaleString()}</td>
2024-03-13 12:02:08 -05:00
<td>{pool.stakeCount.toString()}</td>
<td>{pool.status}</td>
2024-03-12 17:53:04 -05:00
<td>
{!pool.resolved && (
<Button onClick={() => evaluateOutcome(pool.id)}>
Evaluate Outcome
</Button>
)}
</td>
</tr>
))}
</tbody>
</table>
2024-03-10 11:55:59 -05:00
</div>
2024-03-07 21:27:37 -06:00
<div>
2024-03-13 17:34:07 -05:00
<h2>Work Contract 1</h2>
<div>
Price:
{work1Price}
{' '}
ETH
</div>
<div>
<Button onClick={() => stakeAvailability()}>Stake Availability</Button>
</div>
<div>
Availability Stake Count:
{' '}
{availabilityStakes.length}
</div>
2024-03-13 17:56:29 -05:00
<div>
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>Worker</th>
<th>Amount</th>
<th>End Time</th>
<th>Assigned</th>
<th>Reclaimed</th>
2024-03-13 17:34:07 -05:00
</tr>
2024-03-13 17:56:29 -05:00
</thead>
<tbody>
{availabilityStakes.map((stake) => (
<tr key={stake.id}>
<td>{stake.id.toString()}</td>
<td>{stake.worker.toString()}</td>
<td>{stake.amount.toString()}</td>
<td>{new Date(Number(stake.endTime) * 1000).toLocaleString()}</td>
<td>{stake.assigned.toString()}</td>
<td>{stake.reclaimed.toString()}</td>
</tr>
))}
</tbody>
</table>
</div>
2024-03-13 17:34:07 -05:00
<div>
<Button onClick={() => requestWork()}>Request Work</Button>
</div>
2024-03-13 17:56:29 -05:00
<div>
Work Request Count:
{' '}
{workRequests.length}
</div>
<div>
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>Customer</th>
<th>Fee</th>
<th>Status</th>
<th>Stake ID</th>
<th>Approval</th>
<th>Pool ID</th>
</tr>
</thead>
<tbody>
{workRequests.map((request) => (
<tr key={request.id}>
<td>{request.id.toString()}</td>
<td>{request.customer.toString()}</td>
<td>
{request.feeEther}
{' '}
ETH
</td>
<td>{request.statusString}</td>
<td>{request.stakeIndex.toString()}</td>
<td>{request.approval.toString()}</td>
<td>{request.poolIndex.toString()}</td>
</tr>
))}
</tbody>
</table>
</div>
2024-03-07 21:27:37 -06:00
</div>
</>
2024-03-07 10:58:55 -06:00
)}
2024-02-21 18:01:41 -06:00
</>
);
}
export default App;