import {
useCallback, useEffect, useReducer, useState, createContext, useContext, useMemo,
} 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 DAOArtifact from './assets/DAO.json';
import work1Artifact from './assets/Work1.json';
const contracts = {
'0x539': { // Hardhat
DAO: '0x76Dfe9F47f06112a1b78960bf37d87CfbB6D6133',
Work1: '0xd2845aE812Ee42cF024fB4C55c052365792aBd78',
},
'0xaa36a7': { // Sepolia
DAO: '0x39B7522Ee1A5B13aE5580C40114239D4cE0e7D29',
Work1: '0xC0Bb36820Ba891DE4ed6D60f75066805361dbeB8',
},
};
const updateList = (list, action) => {
switch (action.type) {
case 'update': {
const newList = [...list];
newList[Number(action.item.id)] = action.item;
return newList;
}
case 'refresh':
default:
return [];
}
};
const useList = (initialValue) => useReducer(updateList, initialValue ?? []);
const Web3Context = createContext({});
function App() {
const {
sdk, connected, provider, chainId, account, balance,
} = useSDK();
const [DAO, setDAO] = useState();
const [work1, setWork1] = useState();
const [work1Price, setWork1Price] = useState();
const [balanceEther, setBalanceEther] = useState();
const [reputation, setReputation] = useState();
const [totalReputation, setTotalReputation] = useState();
const [posts, dispatchPost] = useList();
const [validationPools, dispatchValidationPool] = useList();
const [availabilityStakes, dispatchAvailabilityStake] = useList();
const [workRequests, dispatchWorkRequest] = useList();
// In this effect, we initialize everything and add contract event listeners.
// TODO: Refactor -- make separate, functional components?
useEffect(() => {
if (!provider || !chainId || !account || balance === undefined) return;
if (!contracts[chainId]) return;
const web3 = new Web3(provider);
const DAOContract = new web3.eth.Contract(DAOArtifact.abi, contracts[chainId].DAO);
const work1Contract = new web3.eth.Contract(work1Artifact.abi, contracts[chainId].Work1);
/* -------------------------------------------------------------------------------- */
/* --------------------------- BEGIN FETCHERS ------------------------------------- */
/* -------------------------------------------------------------------------------- */
const fetchPrice = async () => {
const fetchedPrice = await work1Contract.methods.price().call();
setWork1Price(web3.utils.fromWei(fetchedPrice, 'ether'));
};
const fetchReputation = async () => {
setReputation(Number(await DAOContract.methods.balanceOf(account).call()));
setTotalReputation(Number(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));
}
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);
};
const fetchAvailabilityStake = async (stakeIndex) => {
const s = await work1Contract.methods.stakes(stakeIndex).call();
Object.assign(s, {
id: Number(stakeIndex),
currentUserIsWorker: () => s.worker.toLowerCase() === account.toString().toLowerCase(),
timeRemaining: new Date(Number(s.endTime) * 1000) - new Date(),
});
dispatchAvailabilityStake({ type: 'update', item: s });
if (s.timeRemaining > 0) {
setTimeout(() => {
s.timeRemaining = 0;
dispatchAvailabilityStake({ type: 'update', item: s });
}, s.timeRemaining);
}
return s;
};
const fetchAvailabilityStakes = async () => {
const count = await work1Contract.methods.stakeCount().call();
const promises = [];
dispatchAvailabilityStake({ type: 'refresh' });
for (let i = 0; i < count; i += 1) {
promises.push(fetchAvailabilityStake(i));
}
await Promise.all(promises);
};
const fetchWorkRequest = async (requestIndex) => {
const getRequestStatus = (request) => {
switch (Number(request.status)) {
case -1:
return 'Requested';
case 0:
return 'Evidence Submitted';
case 1:
return 'Approval Submitted';
case 2:
return 'Complete';
default:
return 'Unknown';
}
};
const r = await work1Contract.methods.requests(requestIndex).call();
Object.assign(r, {
id: Number(requestIndex),
statusString: getRequestStatus(r),
feeEther: web3.utils.fromWei(r.fee, 'ether'),
currentUserIsCustomer: () => r.customer.toLowerCase()
=== account.toString().toLowerCase(),
});
dispatchWorkRequest({ type: 'update', item: r });
return r;
};
const fetchWorkRequests = async () => {
const count = await work1Contract.methods.requestCount().call();
const promises = [];
dispatchWorkRequest({ type: 'refresh' });
for (let i = 0; i < count; i += 1) {
promises.push(fetchWorkRequest(i));
}
await Promise.all(promises);
};
/* -------------------------------------------------------------------------------- */
/* --------------------------- END FETCHERS --------------------------------------- */
/* -------------------------------------------------------------------------------- */
fetchPrice();
fetchReputation();
fetchPosts();
fetchValidationPools();
fetchAvailabilityStakes();
fetchWorkRequests();
setWork1(work1Contract);
setDAO(DAOContract);
/* -------------------------------------------------------------------------------- */
/* --------------------------- BEGIN EVENT HANDLERS ------------------------------- */
/* -------------------------------------------------------------------------------- */
DAOContract.events.PostAdded({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: post added', event);
fetchPost(event.returnValues.postIndex);
});
DAOContract.events.ValidationPoolInitiated({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: validation pool initiated', event);
fetchValidationPool(event.returnValues.poolIndex);
});
DAOContract.events.ValidationPoolResolved({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: validation pool resolved', event);
fetchReputation();
fetchValidationPool(event.returnValues.poolIndex);
});
work1Contract.events.AvailabilityStaked({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: availability staked', event);
fetchAvailabilityStake(event.returnValues.stakeIndex);
fetchReputation();
});
work1Contract.events.WorkAssigned({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: work assigned', event);
const r = fetchWorkRequest(event.returnValues.requestIndex);
fetchAvailabilityStake(r.stakeIndex);
});
work1Contract.events.WorkEvidenceSubmitted({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: work evidence submitted', event);
fetchWorkRequest(event.returnValues.requestIndex);
});
work1Contract.events.WorkApprovalSubmitted({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: work approval submitted', event);
fetchWorkRequest(event.returnValues.requestIndex);
});
}, [provider, account, chainId, balance, setReputation, dispatchAvailabilityStake,
dispatchValidationPool, dispatchWorkRequest, dispatchPost]);
/* -------------------------------------------------------------------------------- */
/* --------------------------- 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 ----------------------------------- */
/* -------------------------------------------------------------------------------- */
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: contracts[chainId].DAO,
},
},
});
}, [provider, chainId]);
const addPost = useCallback(async () => {
await DAO.methods.addPost(account).send({
from: account,
gas: 1000000,
});
}, [DAO, account]);
const initiateValidationPool = useCallback(async (postIndex, poolDuration) => {
await DAO.methods.initiateValidationPool(postIndex, poolDuration ?? 3600).send({
from: account,
gas: 1000000,
value: 100,
});
}, [DAO, account]);
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 - stake);
}, [DAO, account, setReputation]);
const stakeAllInFavor = useCallback(async (poolIndex) => {
await stake(poolIndex, reputation, true);
}, [stake, reputation]);
const evaluateOutcome = useCallback(async (poolIndex) => {
await DAO.methods.evaluateOutcome(poolIndex).send({
from: account,
gas: 1000000,
});
}, [DAO, account]);
const requestWork = useCallback(async () => {
const web3 = new Web3(provider);
const priceWei = BigInt(web3.utils.toWei(work1Price, 'ether'));
await work1.methods.requestWork().send({
from: account,
gas: 1000000,
value: priceWei,
});
}, [provider, work1, account, work1Price]);
const submitWorkEvidence = useCallback(async (requestIndex) => {
await work1.methods.submitWorkEvidence(requestIndex).send({
from: account,
gas: 1000000,
});
}, [work1, account]);
const submitWorkApproval = useCallback(async (requestIndex) => {
await work1.methods.submitWorkApproval(requestIndex, true).send({
from: account,
gas: 1000000,
});
}, [work1, account]);
const submitWorkDisapproval = useCallback(async (requestIndex) => {
await work1.methods.submitWorkApproval(requestIndex, false).send({
from: account,
gas: 1000000,
});
}, [work1, account]);
/* -------------------------------------------------------------------------------- */
/* --------------------------- END UI ACTIONS ------------------------------------- */
/* -------------------------------------------------------------------------------- */
const web3ProviderValue = useMemo(() => ({
DAO, work1, availabilityStakes, reputation, setReputation, account, chainId,
}), [
DAO, work1, availabilityStakes, reputation, setReputation, account, chainId,
]);
return (
{!connected && }
{connected && (
<>
{!contracts[chainId] && (
Please switch MetaMask to Sepolia testnet!
)}
{chainId && `Chain ID: ${chainId}`}
{`Account: ${account}`}
{`Balance: ${balanceEther} ETH`}
{`Your REP: ${reputation}`}
{`Total REP: ${totalReputation}`}
{`Posts count: ${posts.length}`}
ID |
Author |
Sender |
Actions |
{posts.filter((x) => !!x).map((post) => (
{post.id.toString()} |
{post.author} |
{post.sender} |
Initiate Validation Pool
{' '}
{' '}
{' '}
|
))}
{`Validation Pool Count: ${validationPools.length}`}
ID |
Post ID |
Fee |
Duration |
End Time |
Stake
Count
|
Status |
Actions |
{validationPools.filter((x) => !!x).map((pool) => (
{pool.id.toString()} |
{pool.postIndex.toString()} |
{pool.fee.toString()} |
{pool.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 && (
)}
|
))}
Work Contract 1
{`Price: ${work1Price} ETH`}
Work Request Count:
{' '}
{workRequests.length}
ID |
Customer |
Fee |
Status |
Stake ID |
Approval |
Pool ID |
Actions |
{workRequests.filter((x) => !!x).map((request) => (
{request.id.toString()} |
{request.customer.toString()} |
{request.feeEther}
{' '}
ETH
|
{request.statusString} |
{request.stakeIndex.toString()} |
{request.approval.toString()} |
{request.poolIndex.toString()} |
{availabilityStakes.length > 0
&& availabilityStakes[Number(request.stakeIndex)]?.currentUserIsWorker()
&& Number(request.status) === 0 && (
)}
{request.currentUserIsCustomer()
&& Number(request.status) === 1 && (
<>
>
)}
|
))}
Work Contract 1
{`Price: ${work1Price} ETH`}
TBD
>
)}
);
}
function AvailabilityStakes() {
const {
DAO, work1, availabilityStakes, reputation, setReputation, account, chainId,
} = useContext(Web3Context);
const stakeAvailability = useCallback(async (duration) => {
const target = contracts[chainId].Work1;
await DAO.methods.stakeAvailability(target, reputation, duration).send({
from: account,
gas: 999999,
});
// Note that as with validation pool stakes, we should keep track locally of our reputation
setReputation(0);
}, [DAO, account, chainId, reputation, setReputation]);
const reclaimAvailabilityStake = useCallback(async (stakeIndex) => {
await work1.methods.reclaimAvailability(stakeIndex).send({
from: account,
gas: 999999,
});
}, [work1, account]);
const extendAvailabilityStake = useCallback(async (stakeIndex, duration) => {
await work1.methods.extendAvailability(stakeIndex, duration).send({
from: account,
gas: 999999,
});
}, [work1, account]);
return (
<>
Stake Availability:
{' '}
{!reputation && <>No reputation available to stake>}
{reputation > 0 && (
<>
{' '}
>
)}
Availability Stake Count:
{' '}
{availabilityStakes?.length}
ID |
Worker |
Amount |
End Time |
Assigned |
Reclaimed |
Actions |
{availabilityStakes?.filter((x) => !!x).map((s) => (
{s.id.toString()} |
{s.worker.toString()} |
{s.amount.toString()} |
{new Date(Number(s.endTime) * 1000).toLocaleString()} |
{s.assigned.toString()} |
{s.reclaimed.toString()} |
{s.currentUserIsWorker() && (
)}
{s.currentUserIsWorker() && s.timeRemaining <= 0
&& !s.assigned && !s.reclaimed && (
<>
{' '}
>
)}
|
))}
>
);
}
export default App;