add onboarding contract
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 28s Details

This commit is contained in:
Ladd Hoffman 2024-03-18 14:03:53 -05:00
parent a1e28d254e
commit 8da5772d86
18 changed files with 712 additions and 250 deletions

View File

@ -1,5 +1,5 @@
import {
useCallback, useEffect, useReducer, useState, useMemo,
useCallback, useEffect, useState, useMemo,
} from 'react';
import { useSDK } from '@metamask/sdk-react';
import { Web3 } from 'web3';
@ -11,27 +11,13 @@ import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Stack from 'react-bootstrap/Stack';
import { getContractByChainId } from './contract-config';
import useList from './List';
import { getContractByChainId, getContractNameByAddress } from './contract-config';
import Web3Context from './Web3Context';
import DAOArtifact from './assets/DAO.json';
import work1Artifact from './assets/Work1.json';
import AvailabilityStakes from './AvailabilityStakes';
import WorkRequests from './WorkRequests';
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 ?? []);
import Work1Artifact from './assets/Work1.json';
import OnboardingArtifact from './assets/Onboarding.json';
import WorkContract from './WorkContract';
function App() {
const {
@ -40,49 +26,44 @@ function App() {
const [DAO, setDAO] = useState();
const [work1, setWork1] = useState();
const [work1Price, setWork1Price] = useState();
const [onboarding, setOnboarding] = 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();
const web3ProviderValue = useMemo(() => ({
provider,
DAO,
work1,
work1Price,
onboarding,
reputation,
setReputation,
account,
chainId,
availabilityStakes,
workRequests,
}), [
provider, DAO, work1, work1Price, reputation, setReputation, account, chainId,
availabilityStakes, workRequests,
]);
provider, DAO, work1, onboarding, reputation, setReputation, account, chainId]);
// In this effect, we initialize everything and add contract event listeners.
useEffect(() => {
if (!provider || !chainId || !account || balance === undefined) return;
const DAOAddress = getContractByChainId(chainId, 'DAO');
const Work1Address = getContractByChainId(chainId, 'Work1');
const OnboardingAddress = getContractByChainId(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 Work1Contract = new web3.eth.Contract(Work1Artifact.abi, Work1Address);
const OnboardingContract = new web3.eth.Contract(OnboardingArtifact.abi, OnboardingAddress);
setDAO(DAOContract);
setWork1(Work1Contract);
setOnboarding(OnboardingContract);
/* -------------------------------------------------------------------------------- */
/* --------------------------- BEGIN FETCHERS ------------------------------------- */
/* -------------------------------------------------------------------------------- */
const fetchPrice = async () => {
const fetchedPrice = await work1Contract.methods.price().call();
setWork1Price(web3.utils.fromWei(fetchedPrice, 'ether'));
};
const fetchReputation = async () => {
setReputation(await DAOContract.methods.balanceOf(account).call());
setTotalReputation(await DAOContract.methods.totalSupply().call());
@ -141,68 +122,13 @@ function App() {
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 r = await work1Contract.methods.requests(requestIndex).call();
Object.assign(r, {
id: Number(requestIndex),
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 ------------------------------- */
@ -224,30 +150,14 @@ function App() {
fetchValidationPool(event.returnValues.poolIndex);
});
work1Contract.events.AvailabilityStaked({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: availability staked', event);
fetchAvailabilityStake(event.returnValues.stakeIndex);
Work1Contract.events.AvailabilityStaked({ fromBlock: 'latest' }).on('data', () => {
fetchReputation();
});
work1Contract.events.WorkAssigned({ fromBlock: 'latest' }).on('data', async (event) => {
console.log('event: work assigned', event);
const r = await fetchWorkRequest(event.returnValues.requestIndex);
console.log('work request', r);
fetchAvailabilityStake(r.stakeIndex);
OnboardingContract.events.AvailabilityStaked({ fromBlock: 'latest' }).on('data', () => {
fetchReputation();
});
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]);
}, [provider, account, chainId, balance, dispatchValidationPool, dispatchPost]);
/* -------------------------------------------------------------------------------- */
/* --------------------------- END MAIN INITIALIZION EFFECT ----------------------- */
@ -299,12 +209,18 @@ function App() {
}, [DAO, account]);
const initiateValidationPool = useCallback(async (postIndex, poolDuration) => {
await DAO.methods.initiateValidationPool(postIndex, poolDuration ?? 3600).send({
const web3 = new Web3(provider);
await DAO.methods.initiateValidationPool(
postIndex,
poolDuration ?? 3600,
false,
web3.eth.abi.encodeParameter('bytes', '0x00'),
).send({
from: account,
gas: 1000000,
value: 100,
});
}, [DAO, account]);
}, [provider, DAO, account]);
const stake = useCallback(async (poolIndex, amount, inFavor) => {
console.log(`Attempting to stake ${amount} ${inFavor ? 'for' : 'against'} pool ${poolIndex}`);
@ -334,6 +250,12 @@ function App() {
/* --------------------------- END UI ACTIONS ------------------------------------- */
/* -------------------------------------------------------------------------------- */
const getAdressName = useCallback((address) => {
const contractName = getContractNameByAddress(chainId, address);
if (contractName) return `${contractName} Contract`;
return address;
}, [chainId]);
return (
<Web3Context.Provider value={web3ProviderValue}>
{!connected && <Button onClick={() => connect()}>Connect</Button>}
@ -401,7 +323,7 @@ function App() {
<tr key={post.id}>
<td>{post.id.toString()}</td>
<td>{post.author}</td>
<td>{post.sender}</td>
<td>{getAdressName(post.sender)}</td>
<td>
Initiate Validation Pool
{' '}
@ -434,6 +356,7 @@ function App() {
<tr>
<th>ID</th>
<th>Post ID</th>
<th>Sender</th>
<th>Fee</th>
<th>Duration</th>
<th>End Time</th>
@ -451,6 +374,7 @@ function App() {
<tr key={pool.id}>
<td>{pool.id.toString()}</td>
<td>{pool.postIndex.toString()}</td>
<td>{getAdressName(pool.sender)}</td>
<td>{pool.fee.toString()}</td>
<td>{pool.duration.toString()}</td>
<td>{new Date(Number(pool.endTime) * 1000).toLocaleString()}</td>
@ -476,30 +400,27 @@ function App() {
</tbody>
</table>
</div>
<div>
<h2>Work Contract 1</h2>
<div>
{`Price: ${work1Price} ETH`}
</div>
<AvailabilityStakes showActions={false} />
<WorkRequests />
</div>
</Tab>
<Tab eventKey="worker" title="Worker">
<h2>Work Contract 1</h2>
<div>
{`Price: ${work1Price} ETH`}
</div>
<AvailabilityStakes />
<WorkRequests />
{work1 && (
<WorkContract workContract={work1} title="Work Contract 1" verb="Work" />
)}
{onboarding && (
<WorkContract workContract={onboarding} title="Onboarding" verb="Onboarding" showRequestWork />
)}
</Tab>
<Tab eventKey="customer" title="Customer">
<h2>Work Contract 1</h2>
<div>
{`Price: ${work1Price} ETH`}
</div>
<AvailabilityStakes showActions={false} showAmount={false} onlyShowAvailable />
<WorkRequests showRequestWork />
{work1 && (
<WorkContract
workContract={work1}
showAvailabilityActions={false}
showAvailabilityAmount={false}
onlyShowAvailable
title="Work Contract 1"
verb="Work"
showRequestWork
/>
)}
</Tab>
</Tabs>
</>

View File

@ -1,9 +1,10 @@
import { useCallback, useContext } from 'react';
import { useCallback, useContext, useEffect } from 'react';
import { PropTypes } from 'prop-types';
import Button from 'react-bootstrap/Button';
import { getContractByChainId } from './contract-config';
import Web3Context from './Web3Context';
import WorkContractContext from './WorkContractContext';
const getAvailabilityStatus = (stake) => {
if (stake.reclaimed) return 'Reclaimed';
@ -12,13 +13,61 @@ const getAvailabilityStatus = (stake) => {
return 'Expired';
};
function AvailabilityStakes({ showActions, showAmount, onlyShowAvailable }) {
function AvailabilityStakes({
showActions, showAmount, onlyShowAvailable,
}) {
const {
DAO, work1, availabilityStakes, reputation, setReputation, account, chainId,
DAO, reputation, setReputation, account, chainId,
} = useContext(Web3Context);
const {
workContract,
availabilityStakes,
dispatchAvailabilityStake,
} = useContext(WorkContractContext);
const fetchAvailabilityStake = useCallback(async (stakeIndex) => {
const s = await workContract.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;
}, [workContract, account, dispatchAvailabilityStake]);
const fetchAvailabilityStakes = useCallback(async () => {
const count = await workContract.methods.stakeCount().call();
const promises = [];
dispatchAvailabilityStake({ type: 'refresh' });
for (let i = 0; i < count; i += 1) {
promises.push(fetchAvailabilityStake(i));
}
await Promise.all(promises);
}, [workContract, fetchAvailabilityStake, dispatchAvailabilityStake]);
useEffect(() => {
fetchAvailabilityStakes();
workContract.events.AvailabilityStaked({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: availability staked', event);
fetchAvailabilityStake(event.returnValues.stakeIndex);
});
workContract.events.WorkAssigned({ fromBlock: 'latest' }).on('data', (event) => {
fetchAvailabilityStake(event.returnValues.stakeIndex);
});
}, [workContract, fetchAvailabilityStakes, fetchAvailabilityStake]);
const stakeAvailability = useCallback(async (duration) => {
const target = getContractByChainId(chainId, 'Work1');
const target = getContractByChainId(chainId, 'Onboarding');
await DAO.methods.stakeAvailability(target, reputation / BigInt(2), duration).send({
from: account,
gas: 999999,
@ -28,18 +77,18 @@ function AvailabilityStakes({ showActions, showAmount, onlyShowAvailable }) {
}, [DAO, account, chainId, reputation, setReputation]);
const reclaimAvailabilityStake = useCallback(async (stakeIndex) => {
await work1.methods.reclaimAvailability(stakeIndex).send({
await workContract.methods.reclaimAvailability(stakeIndex).send({
from: account,
gas: 999999,
});
}, [work1, account]);
}, [workContract, account]);
const extendAvailabilityStake = useCallback(async (stakeIndex, duration) => {
await work1.methods.extendAvailability(stakeIndex, duration).send({
await workContract.methods.extendAvailability(stakeIndex, duration).send({
from: account,
gas: 999999,
});
}, [work1, account]);
}, [workContract, account]);
const displayData = availabilityStakes.filter((stake) => {
if (!stake) return false;

18
client/src/List.js Normal file
View File

@ -0,0 +1,18 @@
import { useReducer } from 'react';
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 ?? []);
export default useList;

View File

@ -0,0 +1,47 @@
import { useMemo } from 'react';
import { PropTypes } from 'prop-types';
import useList from './List';
import WorkContractContext from './WorkContractContext';
import AvailabilityStakes from './AvailabilityStakes';
import WorkRequests from './WorkRequests';
function WorkContract({
workContract,
showAvailabilityActions,
showAvailabilityAmount, onlyShowAvailable, title, verb, showRequestWork,
}) {
const [availabilityStakes, dispatchAvailabilityStake] = useList();
const workContractProviderValue = useMemo(() => ({
workContract, availabilityStakes, dispatchAvailabilityStake,
}), [workContract, availabilityStakes, dispatchAvailabilityStake]);
return (
<WorkContractContext.Provider value={workContractProviderValue}>
<h2>{title}</h2>
<AvailabilityStakes
showActions={showAvailabilityActions}
showAmount={showAvailabilityAmount}
onlyShowAvailable={onlyShowAvailable}
/>
<WorkRequests verb={verb} showRequestWork={showRequestWork} />
</WorkContractContext.Provider>
);
}
WorkContract.propTypes = {
workContract: PropTypes.any.isRequired, // eslint-disable-line react/forbid-prop-types
showRequestWork: PropTypes.bool,
title: PropTypes.string.isRequired,
verb: PropTypes.string.isRequired,
showAvailabilityActions: PropTypes.bool,
showAvailabilityAmount: PropTypes.bool,
onlyShowAvailable: PropTypes.bool,
};
WorkContract.defaultProps = {
showRequestWork: false,
showAvailabilityActions: true,
showAvailabilityAmount: true,
onlyShowAvailable: false,
};
export default WorkContract;

View File

@ -0,0 +1,5 @@
import { createContext } from 'react';
const WorkContractContext = createContext({});
export default WorkContractContext;

View File

@ -1,9 +1,13 @@
import { useCallback, useContext } from 'react';
import {
useCallback, useContext, useEffect, useState,
} from 'react';
import { PropTypes } from 'prop-types';
import Button from 'react-bootstrap/Button';
import Web3 from 'web3';
import Web3Context from './Web3Context';
import useList from './List';
import WorkContractContext from './WorkContractContext';
const getRequestStatus = (request) => {
switch (Number(request.status)) {
@ -20,51 +24,109 @@ const getRequestStatus = (request) => {
}
};
function WorkRequests({ showRequestWork }) {
function WorkRequests({
showRequestWork, verb,
}) {
const [workRequests, dispatchWorkRequest] = useList();
const [price, setPrice] = useState();
const { workContract, availabilityStakes } = useContext(WorkContractContext);
const {
provider, work1, work1Price, workRequests, availabilityStakes, account,
provider, account,
} = useContext(Web3Context);
const fetchPrice = useCallback(async () => {
const web3 = new Web3(provider);
const fetchedPrice = await workContract.methods.price().call();
setPrice(web3.utils.fromWei(fetchedPrice, 'ether'));
}, [workContract, provider]);
const fetchWorkRequest = useCallback(async (requestIndex) => {
const web3 = new Web3(provider);
const r = await workContract.methods.requests(requestIndex).call();
Object.assign(r, {
id: Number(requestIndex),
feeEther: web3.utils.fromWei(r.fee, 'ether'),
currentUserIsCustomer: () => r.customer.toLowerCase()
=== account.toString().toLowerCase(),
});
dispatchWorkRequest({ type: 'update', item: r });
return r;
}, [workContract, provider, account, dispatchWorkRequest]);
const fetchWorkRequests = useCallback(async () => {
const count = await workContract.methods.requestCount().call();
const promises = [];
dispatchWorkRequest({ type: 'refresh' });
for (let i = 0; i < count; i += 1) {
promises.push(fetchWorkRequest(i));
}
await Promise.all(promises);
}, [workContract, dispatchWorkRequest, fetchWorkRequest]);
useEffect(() => {
fetchPrice();
fetchWorkRequests();
workContract.events.WorkAssigned({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: work assigned', event);
fetchWorkRequest(event.returnValues.requestIndex);
});
workContract.events.WorkEvidenceSubmitted({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: work evidence submitted', event);
fetchWorkRequest(event.returnValues.requestIndex);
});
workContract.events.WorkApprovalSubmitted({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: work approval submitted', event);
fetchWorkRequest(event.returnValues.requestIndex);
});
}, [workContract, fetchWorkRequests, fetchPrice, fetchWorkRequest]);
const requestWork = useCallback(async () => {
const web3 = new Web3(provider);
const priceWei = BigInt(web3.utils.toWei(work1Price, 'ether'));
await work1.methods.requestWork().send({
const priceWei = BigInt(web3.utils.toWei(price, 'ether'));
await workContract.methods.requestWork().send({
from: account,
gas: 1000000,
value: priceWei,
});
}, [provider, work1, account, work1Price]);
}, [provider, workContract, account, price]);
const submitWorkEvidence = useCallback(async (requestIndex) => {
await work1.methods.submitWorkEvidence(requestIndex).send({
await workContract.methods.submitWorkEvidence(requestIndex).send({
from: account,
gas: 1000000,
});
}, [work1, account]);
}, [workContract, account]);
const submitWorkApproval = useCallback(async (requestIndex) => {
await work1.methods.submitWorkApproval(requestIndex, true).send({
await workContract.methods.submitWorkApproval(requestIndex, true).send({
from: account,
gas: 1000000,
});
}, [work1, account]);
}, [workContract, account]);
const submitWorkDisapproval = useCallback(async (requestIndex) => {
await work1.methods.submitWorkApproval(requestIndex, false).send({
await workContract.methods.submitWorkApproval(requestIndex, false).send({
from: account,
gas: 1000000,
});
}, [work1, account]);
}, [workContract, account]);
return (
<>
<div>
{`Price: ${price} ETH`}
</div>
{showRequestWork && (
<div>
<Button onClick={() => requestWork()}>Request Work</Button>
<Button onClick={() => requestWork()}>{`Request ${verb}`}</Button>
</div>
)}
<div>
Work Request Count:
Request Count:
{' '}
{workRequests.length}
</div>
@ -78,7 +140,7 @@ function WorkRequests({ showRequestWork }) {
<th>Status</th>
<th>Stake ID</th>
<th>Approval</th>
<th>Pool ID</th>
{/* <th>Pool ID</th> */}
<th>Actions</th>
</tr>
</thead>
@ -95,25 +157,24 @@ function WorkRequests({ showRequestWork }) {
<td>{getRequestStatus(request)}</td>
<td>{request.stakeIndex.toString()}</td>
<td>{request.approval.toString()}</td>
<td>{request.poolIndex.toString()}</td>
<td>
{availabilityStakes.length > 0
&& availabilityStakes[Number(request.stakeIndex)]?.currentUserIsWorker()
&& Number(request.status) === 0 && (
<Button onClick={() => submitWorkEvidence(request.id)}>
Submit Work Evidence
</Button>
&& availabilityStakes[Number(request.stakeIndex)]?.currentUserIsWorker()
&& Number(request.status) === 0 && (
<Button onClick={() => submitWorkEvidence(request.id)}>
Submit Work Evidence
</Button>
)}
{request.currentUserIsCustomer()
&& Number(request.status) === 1 && (
<>
<Button onClick={() => submitWorkApproval(request.id)}>
Submit Approval
</Button>
<Button onClick={() => submitWorkDisapproval(request.id)}>
Submit Dispproval
</Button>
</>
&& Number(request.status) === 1 && (
<>
<Button onClick={() => submitWorkApproval(request.id)}>
Submit Approval
</Button>
<Button onClick={() => submitWorkDisapproval(request.id)}>
Submit Dispproval
</Button>
</>
)}
</td>
</tr>
@ -127,6 +188,7 @@ function WorkRequests({ showRequestWork }) {
WorkRequests.propTypes = {
showRequestWork: PropTypes.bool,
verb: PropTypes.string.isRequired,
};
WorkRequests.defaultProps = {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,12 @@
{
"localhost": {
"DAO": "0x74DD88A13804003bDD1EBB41ebEE47EA145B8E46",
"Work1": "0x0bB2b52F9E4068E99038E0A9B86D337461926F17"
"DAO": "0x4CC737e8Ec8873abCBC98c8c4a401990d6Fc4F38",
"Work1": "0xa0A1c1e84F50f6786A6927b448903a37776D2e74",
"Onboarding": "0xA6bE53f1F25816c65CF3bc36d6F8793Eed14fd09"
},
"sepolia": {
"DAO": "0xA4Cb4d99be0Fef3e59dCaE909D6E8ef46EF07257",
"Work1": "0xdB3b37B555c980617140C8d8687a19B8381F374d"
"DAO": "0x8611676563EBcd69dC52E5829bF2914A957398C3",
"Work1": "0xCd5881DB1aa6b86283a9c5660FaB65C989cf2721",
"Onboarding": "0x215078c5cf21ffB79Ee14Cf09156B94a11b7340f"
}
}

View File

@ -16,3 +16,12 @@ export const getContractByChainId = (chainId, contractName) => {
if (!network) throw new Error(`Chain ID ${chainId} not recognized`);
return getContractByNetworkName(network, contractName);
};
export const getContractNameByAddress = (chainId, address) => {
const network = Object.entries(networks).find(([, id]) => id === chainId)[0];
if (!network) throw new Error(`Chain ID ${chainId} not recognized`);
const entries = Object.entries(contractAddresses[network]);
const entry = entries.find(([, addr]) => addr.toLowerCase() === address.toLowerCase());
if (entry) return entry[0];
return undefined;
};

View File

@ -1,10 +1,12 @@
{
"localhost": {
"DAO": "0x74DD88A13804003bDD1EBB41ebEE47EA145B8E46",
"Work1": "0x0bB2b52F9E4068E99038E0A9B86D337461926F17"
"DAO": "0x4CC737e8Ec8873abCBC98c8c4a401990d6Fc4F38",
"Work1": "0xa0A1c1e84F50f6786A6927b448903a37776D2e74",
"Onboarding": "0xA6bE53f1F25816c65CF3bc36d6F8793Eed14fd09"
},
"sepolia": {
"DAO": "0xA4Cb4d99be0Fef3e59dCaE909D6E8ef46EF07257",
"Work1": "0xdB3b37B555c980617140C8d8687a19B8381F374d"
"DAO": "0x8611676563EBcd69dC52E5829bF2914A957398C3",
"Work1": "0xCd5881DB1aa6b86283a9c5660FaB65C989cf2721",
"Onboarding": "0x215078c5cf21ffB79Ee14Cf09156B94a11b7340f"
}
}

View File

@ -25,12 +25,10 @@ contract Onboarding is WorkContract, IOnValidate {
uint postIndex = dao.addPost(stake.worker);
emit WorkApprovalSubmitted(requestIndex, approval);
// Initiate validation pool
dao.initiateValidationPool{value: request.fee - request.fee / 10}(
postIndex,
POOL_DURATION,
true,
abi.encode(requestIndex)
);
uint poolIndex = dao.initiateValidationPool{
value: request.fee - request.fee / 10
}(postIndex, POOL_DURATION, true, abi.encode(requestIndex));
dao.stake(poolIndex, stake.amount, true);
}
/// Callback to be executed when review pool completes
@ -39,9 +37,13 @@ contract Onboarding is WorkContract, IOnValidate {
msg.sender == address(dao),
"onValidate may only be called by the DAO contract"
);
if (!votePasses) return;
uint requestIndex = abi.decode(callbackData, (uint));
WorkRequest storage request = requests[requestIndex];
if (!votePasses) {
// refund the customer the remaining amount
payable(request.customer).transfer(request.fee / 10);
return;
}
uint postIndex = dao.addPost(request.customer);
dao.initiateValidationPool{value: request.fee / 10}(
postIndex,

View File

@ -39,7 +39,7 @@ abstract contract WorkContract is IAcceptAvailability {
uint constant POOL_DURATION = 1 days;
event AvailabilityStaked(uint stakeIndex);
event WorkAssigned(address worker, uint requestIndex);
event WorkAssigned(uint requestIndex, uint stakeIndex);
event WorkEvidenceSubmitted(uint requestIndex);
event WorkApprovalSubmitted(uint requestIndex, bool approval);
@ -123,7 +123,7 @@ abstract contract WorkContract is IAcceptAvailability {
stakeIndex = randomWeightedSelection();
AvailabilityStake storage stake = stakes[stakeIndex];
stake.assigned = true;
emit WorkAssigned(stake.worker, requestIndex);
emit WorkAssigned(requestIndex, stakeIndex);
}
/// Accept work request with fee
@ -169,11 +169,12 @@ abstract contract WorkContract is IAcceptAvailability {
uint postIndex = dao.addPost(stake.worker);
emit WorkApprovalSubmitted(requestIndex, approval);
// Initiate validation pool
dao.initiateValidationPool{value: request.fee}(
uint poolIndex = dao.initiateValidationPool{value: request.fee}(
postIndex,
POOL_DURATION,
true,
abi.encode(requestIndex)
false,
""
);
dao.stake(poolIndex, stake.amount, true);
}
}

View File

@ -46,13 +46,20 @@ const poolIsActive = (pool) => {
return true;
};
const poolIsValid = (pool) => {
const Work1Address = getContractByNetworkName(network, 'Work1');
return pool.sender === Work1Address;
const poolIsValid = (pool) => [
getContractByNetworkName(network, 'Work1'),
getContractByNetworkName(network, 'Onboarding'),
].includes(pool.sender);
const getPoolStatus = (pool) => {
if (poolIsActive(pool)) return 'Active';
if (!pool.resolved) return 'Ready to Evaluate';
if (pool.outcome) return 'Accepted';
return 'Rejected';
};
const stake = async (pool, amount) => {
console.log(`staking ${amount} in favor of pool ${pool.id.toString()}`);
const stake = async (pool, amount, inFavor) => {
console.log(`staking ${amount} ${inFavor ? 'in favor of' : 'against'} pool ${pool.id.toString()}`);
await dao.stake(pool.id, amount, true);
await fetchReputation();
};
@ -60,7 +67,7 @@ const stake = async (pool, amount) => {
const stakeEach = async (pools, amountPerPool) => {
const promises = [];
pools.forEach((pool) => {
promises.push(stake(pool, amountPerPool));
promises.push(stake(pool, amountPerPool, poolIsValid(pool)));
});
await Promise.all(promises);
};
@ -69,19 +76,14 @@ async function main() {
await initialize();
validationPools.forEach((pool) => {
let status;
if (poolIsActive(pool)) status = 'Active';
else if (!pool.resolved) status = 'Ready to Evaluate';
else if (pool.outcome) status = 'Accepted';
else status = 'Rejected';
console.log(`pool ${pool.id.toString()}, status: ${status}`);
console.log(`pool ${pool.id.toString()}, status: ${getPoolStatus(pool)}`);
});
// Stake half of available reputation on any active, valid pools
const activeValidPools = validationPools.filter(poolIsActive).filter(poolIsValid);
if (activeValidPools.length && reputation > 0) {
const amountPerPool = reputation / BigInt(2) / BigInt(activeValidPools.length);
await stakeEach(activeValidPools, amountPerPool);
// Stake half of available reputation on any active pools
const activePools = validationPools.filter(poolIsActive);
if (activePools.length && reputation > 0) {
const amountPerPool = reputation / BigInt(2) / BigInt(activePools.length);
await stakeEach(activePools, amountPerPool);
}
// Listen for new validation pools
@ -89,13 +91,14 @@ async function main() {
console.log(`pool ${poolIndex} started`);
const pool = await fetchValidationPool(poolIndex);
await fetchReputation();
if (!reputation) return;
const amountToStake = reputation / BigInt(2);
if (poolIsValid(pool)) {
// Stake half of available reputation on this validation pool
const amount = reputation / BigInt(2);
await stake(pool, amount, true);
await stake(pool, amountToStake, true);
} else {
console.log(`pool sender ${pool.sender} is not recognized`);
await stake(pool, amountToStake, false);
}
});

View File

@ -11,13 +11,18 @@ async function main() {
console.log(`DAO deployed to ${dao.target}`);
contractAddresses[network].DAO = dao.target;
fs.copyFileSync('./artifacts/contracts/DAO.sol/DAO.json', '../client/src/assets/DAO.json');
const WORK1_PRICE = ethers.parseEther('0.001');
const work1 = await ethers.deployContract('Work1', [dao.target, WORK1_PRICE]);
await work1.waitForDeployment();
const deployWorkContract = async (name, price) => {
const contract = await ethers.deployContract('Work1', [dao.target, price]);
await contract.waitForDeployment();
console.log(`${name} deployed to ${contract.target}`);
contractAddresses[network][name] = contract.target;
fs.copyFileSync(`./artifacts/contracts/${name}.sol/${name}.json`, `../client/src/assets/${name}.json`);
};
console.log(`Work1 deployed to ${work1.target}`);
contractAddresses[network].Work1 = work1.target;
await deployWorkContract('Work1', ethers.parseEther('0.001'));
await deployWorkContract('Onboarding', ethers.parseEther('0.001'));
fs.writeFileSync('../client/src/contract-addresses.json', JSON.stringify(contractAddresses, null, 2));
console.log('Wrote file', fs.realpathSync('../client/src/contract-addresses.json'));
@ -25,8 +30,6 @@ async function main() {
fs.writeFileSync('./contract-addresses.json', JSON.stringify(contractAddresses, null, 2));
console.log('Wrote file', fs.realpathSync('./contract-addresses.json'));
fs.copyFileSync('./artifacts/contracts/DAO.sol/DAO.json', '../client/src/assets/DAO.json');
fs.copyFileSync('./artifacts/contracts/Work1.sol/Work1.json', '../client/src/assets/Work1.json');
console.log('Copied ABIs to', fs.realpathSync('../client/src/assets'));
}

View File

@ -136,7 +136,13 @@ describe('DAO', () => {
});
it('should be able to evaluate outcome of second validation pool', async () => {
const init = () => dao.initiateValidationPool(0, POOL_DURATION, { value: POOL_FEE });
const init = () => dao.initiateValidationPool(
0,
POOL_DURATION,
false,
callbackData,
{ value: POOL_FEE },
);
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
expect(await dao.validationPoolCount()).to.equal(2);
time.increase(POOL_DURATION + 1);
@ -153,7 +159,13 @@ describe('DAO', () => {
await dao.evaluateOutcome(0);
expect(await dao.balanceOf(account1)).to.equal(100);
expect(await dao.balanceOf(dao.target)).to.equal(0);
await dao.initiateValidationPool(0, POOL_DURATION, { value: POOL_FEE });
await dao.initiateValidationPool(
0,
POOL_DURATION,
false,
callbackData,
{ value: POOL_FEE },
);
expect(await dao.balanceOf(dao.target)).to.equal(100);
});

View File

@ -129,11 +129,11 @@ describe('Work1', () => {
describe('Request and assign work', () => {
it('should be able to request work and assign to a worker', async () => {
const {
dao, work1, account1, account2,
dao, work1, account2,
} = await loadFixture(deploy);
await dao.stakeAvailability(work1.target, 50, STAKE_DURATION);
const requestWork = () => work1.connect(account2).requestWork({ value: WORK1_PRICE });
await expect(requestWork()).to.emit(work1, 'WorkAssigned').withArgs(account1, 0);
await expect(requestWork()).to.emit(work1, 'WorkAssigned').withArgs(0, 0);
expect(await work1.requestCount()).to.equal(1);
const request = await work1.requests(0);
expect(request.customer).to.equal(account2);
@ -141,11 +141,11 @@ describe('Work1', () => {
it('should not be able to reclaim stake after work is assigned', async () => {
const {
dao, work1, account1, account2,
dao, work1, account2,
} = await loadFixture(deploy);
await dao.stakeAvailability(work1.target, 50, STAKE_DURATION);
const requestWork = () => work1.connect(account2).requestWork({ value: WORK1_PRICE });
await expect(requestWork()).to.emit(work1, 'WorkAssigned').withArgs(account1, 0);
await expect(requestWork()).to.emit(work1, 'WorkAssigned').withArgs(0, 0);
await time.increase(STAKE_DURATION + 1);
await expect(work1.reclaimAvailability(0)).to.be.revertedWith('Stake has already been assigned work');
});
@ -177,11 +177,11 @@ describe('Work1', () => {
it('should not assign work to the same availability stake twice', async () => {
const {
dao, work1, account1, account2,
dao, work1, account2,
} = await loadFixture(deploy);
await dao.stakeAvailability(work1.target, 50, STAKE_DURATION);
const requestWork = () => work1.connect(account2).requestWork({ value: WORK1_PRICE });
await expect(requestWork()).to.emit(work1, 'WorkAssigned').withArgs(account1, 0);
await expect(requestWork()).to.emit(work1, 'WorkAssigned').withArgs(0, 0);
await expect(requestWork()).to.be.revertedWith('No available worker stakes');
});
@ -238,6 +238,9 @@ describe('Work1', () => {
expect(pool.fee).to.equal(WORK1_PRICE);
expect(pool.sender).to.equal(work1.target);
expect(pool.postIndex).to.equal(1);
expect(pool.stakeCount).to.equal(3);
await time.increase(86401);
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true);
});
it('should be able to submit work disapproval', async () => {