frontend: refactor to consolidate main tabs as a component
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 30s Details

This commit is contained in:
Ladd Hoffman 2024-04-28 20:27:25 -05:00
parent 2c5fb00180
commit 0843a3279d
28 changed files with 1992 additions and 1068 deletions

View File

@ -1,9 +1,11 @@
{
"localhost": {
"DAO": "0x8d914D38dD301FC4606f5aa9fEcF8A76389020d3",
"Work1": "0xfe58B9EB03F75A603de1B286584f5E9532ab8fB5",
"Onboarding": "0x1d63FDe5B461106729fE1e5e38A02fc68C518Af5",
"Proposals": "0x050C420Cc4995B41217Eba1B54B82Fd5687e9139"
"DAO": "0x431c73b38B9eBe4a30C7A0ffa11188fa9429Bf38",
"Work1": "0x0173903444ed92F5e30f522d0f274162c57b4Cc0",
"Onboarding": "0x44df3b45C937c88267999a5f69D9BFBe83dEf447",
"Proposals": "0x663f119ffb7eD161A00C89c25046Afe8B40fff52",
"Rollup": "0xB61bB0defB5A0B59F0BAeBF2F4345d7c317c20B4",
"Work2": "0x4774670f82A590e7eD6072bd7098836B06FFd8ce"
},
"sepolia": {
"DAO": "0x8e5bd58B2ca8910C5F9be8de847d6883B15c60d2",

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

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,9 +1,11 @@
{
"localhost": {
"DAO": "0x8d914D38dD301FC4606f5aa9fEcF8A76389020d3",
"Work1": "0xfe58B9EB03F75A603de1B286584f5E9532ab8fB5",
"Onboarding": "0x1d63FDe5B461106729fE1e5e38A02fc68C518Af5",
"Proposals": "0x050C420Cc4995B41217Eba1B54B82Fd5687e9139"
"DAO": "0x431c73b38B9eBe4a30C7A0ffa11188fa9429Bf38",
"Work1": "0x0173903444ed92F5e30f522d0f274162c57b4Cc0",
"Onboarding": "0x44df3b45C937c88267999a5f69D9BFBe83dEf447",
"Proposals": "0x663f119ffb7eD161A00C89c25046Afe8B40fff52",
"Rollup": "0xB61bB0defB5A0B59F0BAeBF2F4345d7c317c20B4",
"Work2": "0x4774670f82A590e7eD6072bd7098836B06FFd8ce"
},
"sepolia": {
"DAO": "0x8e5bd58B2ca8910C5F9be8de847d6883B15c60d2",

View File

@ -10,8 +10,8 @@ abstract contract RollableWork is Work {
constructor(
DAO dao,
Proposals proposalsContract,
uint price,
Rollup rollupContract_
Rollup rollupContract_,
uint price
) Work(dao, proposalsContract, price) {
rollupContract = rollupContract_;
}

View File

@ -0,0 +1,16 @@
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.24;
import "./core/DAO.sol";
import "./RollableWork.sol";
import "./Proposals.sol";
import "./Rollup.sol";
contract Work2 is RollableWork {
constructor(
DAO dao,
Proposals proposals,
Rollup rollup,
uint price
) RollableWork(dao, proposals, rollup, price) {}
}

View File

@ -1,12 +1,15 @@
const deployWorkContract = require('./util/deploy-work-contract');
const deployContract = require('./util/deploy-contract');
const deployDAOContract = require('./util/deploy-dao-contract');
const deployWorkContract = require('./util/deploy-work-contract');
const deployRollableWorkContract = require('./util/deploy-rollable-work-contract');
async function main() {
await deployContract('DAO', [], true);
await deployDAOContract('Rollup');
await deployDAOContract('Proposals');
await deployWorkContract('Work1');
await deployWorkContract('Onboarding');
await deployRollableWorkContract('Work2');
}
main().catch((error) => {

View File

@ -0,0 +1,22 @@
const { ethers } = require('hardhat');
const deployContract = require('./deploy-contract');
const contractAddresses = require('../../contract-addresses.json');
require('dotenv').config();
const network = process.env.HARDHAT_NETWORK;
const deployRollableWorkContract = async (name) => {
const priceEnvVar = `${name.toUpperCase()}_PRICE`;
const price = ethers.parseEther(process.env[priceEnvVar] || '0.001');
await deployContract(name, [
contractAddresses[network].DAO,
contractAddresses[network].Proposals,
contractAddresses[network].Rollup,
price,
]);
};
module.exports = deployRollableWorkContract;

View File

@ -9,7 +9,7 @@ const network = process.env.HARDHAT_NETWORK;
const deployWorkContract = async (name) => {
const priceEnvVar = `${name.toUpperCase()}_PRICE`;
const price = ethers.parseEther(process.env[priceEnvVar] || 0.001);
const price = ethers.parseEther(process.env[priceEnvVar] || '0.001');
await deployContract(name, [
contractAddresses[network].DAO,

View File

@ -1,9 +1,11 @@
{
"localhost": {
"DAO": "0x8d914D38dD301FC4606f5aa9fEcF8A76389020d3",
"Work1": "0xfe58B9EB03F75A603de1B286584f5E9532ab8fB5",
"Onboarding": "0x1d63FDe5B461106729fE1e5e38A02fc68C518Af5",
"Proposals": "0x050C420Cc4995B41217Eba1B54B82Fd5687e9139"
"DAO": "0x431c73b38B9eBe4a30C7A0ffa11188fa9429Bf38",
"Work1": "0x0173903444ed92F5e30f522d0f274162c57b4Cc0",
"Onboarding": "0x44df3b45C937c88267999a5f69D9BFBe83dEf447",
"Proposals": "0x663f119ffb7eD161A00C89c25046Afe8B40fff52",
"Rollup": "0xB61bB0defB5A0B59F0BAeBF2F4345d7c317c20B4",
"Work2": "0x4774670f82A590e7eD6072bd7098836B06FFd8ce"
},
"sepolia": {
"DAO": "0x8e5bd58B2ca8910C5F9be8de847d6883B15c60d2",

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

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,263 +1,27 @@
import {
useCallback, useEffect, useState, useMemo, useRef,
} from 'react';
import { useSDK } from '@metamask/sdk-react';
import { Web3 } from 'web3';
import { useCallback } from 'react';
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 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';
import ImportMatrixEvent from './components/ImportMatrixEvent';
import MainTabs from './components/tabs';
import useMainContext from './contexts/useMainContext';
function WebApp() {
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 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,
DAORef, workRef, onboardingRef,
fetchPost, fetchPosts, fetchReputation, fetchValidationPool, fetchValidationPools, fetchMembers,
]);
/* -------------------------------------------------------------------------------- */
/* --------------------------- END MAIN INITIALIZION EFFECT ----------------------- */
/* -------------------------------------------------------------------------------- */
// useEffect(() => {
// // api.requestCapability(MatrixCapabilities.)
// api.on('action:m.message', (ev) => {
// console.log('action:m.message', ev);
// });
// api.start();
// }, []);
/* -------------------------------------------------------------------------------- */
/* --------------------------- BEGIN UI ACTIONS ----------------------------------- */
/* -------------------------------------------------------------------------------- */
sdk, connected, chainId, account, reputation, totalReputation, balanceEther,
} = useMainContext();
const connect = useCallback(async () => {
try {
await sdk?.connect();
console.log('connected');
console.log('connected', connected);
} catch (err) {
console.warn('failed to connect..', err);
}
}, [sdk]);
}, [sdk, connected]);
const disconnect = useCallback(async () => {
try {
@ -267,67 +31,8 @@ function WebApp() {
}
}, [sdk]);
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]);
/* -------------------------------------------------------------------------------- */
/* --------------------------- END UI ACTIONS ------------------------------------- */
/* -------------------------------------------------------------------------------- */
return (
<Web3Context.Provider value={web3ProviderValue}>
<AddPostModal show={showAddPost} setShow={setShowAddPost} postToBlockchain />
<ViewPostModal show={showViewPost} setShow={setShowViewPost} post={viewPost} />
<>
{!connected && <Button onClick={() => connect()}>Connect</Button>}
{connected && (
@ -372,211 +77,11 @@ function WebApp() {
</Col>
</Row>
</Container>
<Tabs>
<Tab eventKey="admin" title="Admin">
<h2>Members</h2>
<div>
{`Members count: ${members.length}`}
</div>
<div>
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>Reputation</th>
</tr>
</thead>
<tbody>
{members.filter((x) => !!x).map((member) => (
<tr key={member.id}>
<td>{member.id}</td>
<td>{member.reputation.toString()}</td>
</tr>
))}
</tbody>
</table>
</div>
{' '}
<h2>Posts</h2>
<div>
<Button onClick={handleShowAddPost}>Add Post</Button>
</div>
<div>
{`Posts count: ${posts.length}`}
</div>
<div>
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>Authors</th>
<th>Sender</th>
<th>Reputation</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{posts.filter((x) => !!x).map((post) => (
<tr key={post.id}>
<td>{post.id.toString()}</td>
<td>
<Stack>
{post.authors.map(({ authorAddress, weightPPM }) => (
<div key={authorAddress}>
{getAddressName(chainId, authorAddress)}
{' '}
{Number(weightPPM) / 10000}
%
</div>
))}
</Stack>
</td>
<td>{getAddressName(chainId, post.sender)}</td>
<td>{post.reputation.toString()}</td>
<td>
<Button onClick={() => handleShowViewPost(post)}>
View Post
</Button>
{' '}
Initiate Validation Pool
{' '}
<Button onClick={() => initiateValidationPool(post.id, 1)}>
1s
</Button>
{' '}
<Button onClick={() => initiateValidationPool(post.id, 20)}>
20s
</Button>
{' '}
<Button onClick={() => initiateValidationPool(post.id, 60)}>
60s
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<h2>Validation Pools</h2>
<div>
{`Validation Pool Count: ${validationPools.length}`}
</div>
<div>
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>Post ID</th>
<th>Sender</th>
<th>Fee</th>
<th>Binding</th>
<th>Quorum</th>
<th>WinRatio</th>
<th>
Redistribute
<br />
Losing Stakes
</th>
<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.postId}</td>
<td>{getAddressName(chainId, pool.sender)}</td>
<td>{pool.fee.toString()}</td>
<td>
{pool.params.bindingPercent.toString()}
%
</td>
<td>{`${pool.params.quorum[0].toString()}/${pool.params.quorum[1].toString()}`}</td>
<td>{`${pool.params.winRatio[0].toString()}/${pool.params.winRatio[1].toString()}`}</td>
<td>{pool.params.redistributeLosingStakes.toString()}</td>
<td>{pool.params.duration.toString()}</td>
<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
</Button>
{' '}
<Button onClick={() => stake(pool.id, reputation, true)}>
Stake All
</Button>
{' '}
</>
)}
{!pool.resolved && (pool.timeRemaining <= 0 || !reputation) && (
<Button onClick={() => evaluateOutcome(pool.id)}>
Evaluate Outcome
</Button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</Tab>
<Tab eventKey="worker" title="Worker">
{work1 && (
<WorkContract
workContract={work1}
title="Work Contract 1"
verb="Work"
showProposePriceChange
/>
)}
{onboarding && (
<WorkContract
workContract={onboarding}
title="Onboarding"
verb="Onboarding"
showRequestWork
showProposePriceChange
/>
)}
</Tab>
<Tab eventKey="customer" title="Customer">
{work1 && (
<WorkContract
workContract={work1}
showAvailabilityActions={false}
showAvailabilityAmount={false}
onlyShowAvailable
title="Work Contract 1"
verb="Work"
showRequestWork
/>
)}
</Tab>
<Tab eventKey="proposals" title="Proposals">
<Proposals />
</Tab>
<Tab eventKey="import" title="Import">
<h1>Semantic Scholar</h1>
<ImportPaper />
<ImportPapersByAuthor />
<h1>Matrix</h1>
<ImportMatrixEvent />
</Tab>
</Tabs>
<MainTabs />
</>
)}
</Web3Context.Provider>
</>
);
}

View File

@ -1,11 +1,7 @@
import {
useCallback, useEffect, useState, useMemo, useRef,
useCallback, useEffect, 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';
@ -14,234 +10,15 @@ import {
WidgetApi, EventDirection, WidgetEventCapability,
} from 'matrix-widget-api';
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';
import ImportMatrixEvent from './components/ImportMatrixEvent';
import MainTabs from './components/tabs';
import useMainContext from './contexts/useMainContext';
function Widget() {
const {
sdk, connected, provider, chainId, account, balance,
} = useSDK();
sdk, connected, provider, chainId, account, balanceEther, reputation, totalReputation,
} = useMainContext();
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 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,
DAORef, workRef, onboardingRef,
fetchPost, fetchPosts, fetchReputation, fetchValidationPool, fetchValidationPools, fetchMembers,
]);
/* -------------------------------------------------------------------------------- */
/* --------------------------- END MAIN INITIALIZION EFFECT ----------------------- */
/* -------------------------------------------------------------------------------- */
useEffect(() => {
const { searchParams } = new URL(window.location.href);
@ -286,56 +63,6 @@ function Widget() {
}
}, [sdk]);
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]);
// Sign and send a message
const registerMatrixIdentity = useCallback(async () => {
const message = new Date().toISOString();
@ -356,12 +83,7 @@ function Widget() {
/* -------------------------------------------------------------------------------- */
return (
<Web3Context.Provider value={web3ProviderValue}>
<AddPostModal show={showAddPost} setShow={setShowAddPost} postToBlockchain />
<ViewPostModal show={showViewPost} setShow={setShowViewPost} post={viewPost} />
<>
{!connected && <Button onClick={() => connect()}>Connect</Button>}
{connected && (
@ -409,210 +131,11 @@ function Widget() {
</Col>
</Row>
</Container>
<Tabs>
<Tab eventKey="admin" title="Admin">
<h2>Members</h2>
<div>
{`Members count: ${members.length}`}
</div>
<div>
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>Reputation</th>
</tr>
</thead>
<tbody>
{members.filter((x) => !!x).map((member) => (
<tr key={member.id}>
<td>{member.id}</td>
<td>{member.reputation.toString()}</td>
</tr>
))}
</tbody>
</table>
</div>
{' '}
<h2>Posts</h2>
<div>
<Button onClick={handleShowAddPost}>Add Post</Button>
</div>
<div>
{`Posts count: ${posts.length}`}
</div>
<div>
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>Authors</th>
<th>Sender</th>
<th>Reputation</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{posts.filter((x) => !!x).map((post) => (
<tr key={post.id}>
<td>{post.id.toString()}</td>
<td>
<Stack>
{post.authors.map(({ authorAddress, weightPPM }) => (
<div key={authorAddress}>
{getAddressName(chainId, authorAddress)}
{' '}
{Number(weightPPM) / 10000}
%
</div>
))}
</Stack>
</td>
<td>{getAddressName(chainId, post.sender)}</td>
<td>{post.reputation.toString()}</td>
<td>
<Button onClick={() => handleShowViewPost(post)}>
View Post
</Button>
{' '}
Initiate Validation Pool
{' '}
<Button onClick={() => initiateValidationPool(post.id, 1)}>
1s
</Button>
{' '}
<Button onClick={() => initiateValidationPool(post.id, 20)}>
20s
</Button>
{' '}
<Button onClick={() => initiateValidationPool(post.id, 60)}>
60s
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<h2>Validation Pools</h2>
<div>
{`Validation Pool Count: ${validationPools.length}`}
</div>
<div>
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>Post ID</th>
<th>Sender</th>
<th>Fee</th>
<th>Binding</th>
<th>Quorum</th>
<th>WinRatio</th>
<th>
Redistribute
<br />
Losing Stakes
</th>
<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.postId}</td>
<td>{getAddressName(chainId, pool.sender)}</td>
<td>{pool.fee.toString()}</td>
<td>
{pool.params.bindingPercent.toString()}
%
</td>
<td>{`${pool.params.quorum[0].toString()}/${pool.params.quorum[1].toString()}`}</td>
<td>{`${pool.params.winRatio[0].toString()}/${pool.params.winRatio[1].toString()}`}</td>
<td>{pool.params.redistributeLosingStakes.toString()}</td>
<td>{pool.params.duration.toString()}</td>
<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
</Button>
{' '}
<Button onClick={() => stake(pool.id, reputation, true)}>
Stake All
</Button>
{' '}
</>
)}
{!pool.resolved && (pool.timeRemaining <= 0 || !reputation) && (
<Button onClick={() => evaluateOutcome(pool.id)}>
Evaluate Outcome
</Button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</Tab>
<Tab eventKey="worker" title="Worker">
{work1 && (
<WorkContract
workContract={work1}
title="Work Contract 1"
verb="Work"
showProposePriceChange
/>
)}
{onboarding && (
<WorkContract
workContract={onboarding}
title="Onboarding"
verb="Onboarding"
showRequestWork
showProposePriceChange
/>
)}
</Tab>
<Tab eventKey="customer" title="Customer">
{work1 && (
<WorkContract
workContract={work1}
showAvailabilityActions={false}
showAvailabilityAmount={false}
onlyShowAvailable
title="Work Contract 1"
verb="Work"
showRequestWork
/>
)}
</Tab>
<Tab eventKey="proposals" title="Proposals">
<Proposals />
</Tab>
<Tab eventKey="import" title="Import">
<h1>Semantic Scholar Import</h1>
<ImportPaper />
<ImportPapersByAuthor />
<ImportMatrixEvent />
</Tab>
</Tabs>
<MainTabs />
</>
)}
</Web3Context.Provider>
</>
);
}

View File

@ -110,7 +110,10 @@ function Proposals() {
[{ authorAddress: account, weightPPM: 1000000 }],
post.hash,
[], // TODO: Proposal can cite posts from matrix, semantic scholar, etc
);
).send({
from: account,
gas: 1000000,
});
// Now submit the post as a proposal
await proposalsContract.current.methods.propose(
post.hash,

View File

@ -11,7 +11,7 @@ function AddPostModal({
show, setShow, title, postToBlockchain, onSubmit,
}) {
const {
provider, DAO, account,
provider, DAORef, account,
} = useContext(Web3Context);
const [content, setContent] = useState('');
@ -29,13 +29,13 @@ function AddPostModal({
await post.write();
// If requested, upload the hash to the blockchain
if (postToBlockchain) {
await post.publish(DAO, account);
await post.publish(DAORef.current, account);
}
// If requested, call callback
if (onSubmit) {
onSubmit(post);
}
}, [provider, DAO, account, content, setShow, postToBlockchain, onSubmit]);
}, [provider, DAORef, account, content, setShow, postToBlockchain, onSubmit]);
return (
<Modal className="modal" show={show} onHide={handleClose}>

View File

@ -0,0 +1,308 @@
import {
useCallback, useState,
} from 'react'; import Button from 'react-bootstrap/Button';
import { Web3 } from 'web3';
import Tab from 'react-bootstrap/Tab';
import Tabs from 'react-bootstrap/Tabs';
import Stack from 'react-bootstrap/Stack';
import WorkContract from '../work-contracts/WorkContract';
import Proposals from '../Proposals';
import ImportPaper from '../ImportPaper';
import ImportPapersByAuthor from '../ImportPapersByAuthor';
import getAddressName from '../../utils/get-address-name';
import ImportMatrixEvent from '../ImportMatrixEvent';
import AddPostModal from '../posts/AddPostModal';
import ViewPostModal from '../posts/ViewPostModal';
import Post from '../../utils/Post';
import useMainContext from '../../contexts/useMainContext';
function MainTabs() {
const {
provider, chainId, account, members, reputation, setReputation, posts, validationPools,
DAORef, work1, work2, onboarding,
} = useMainContext();
const [showAddPost, setShowAddPost] = useState(false);
const [showViewPost, setShowViewPost] = useState(false);
const [viewPost, setViewPost] = useState({});
/* -------------------------------------------------------------------------------- */
/* --------------------------- BEGIN UI ACTIONS ----------------------------------- */
/* -------------------------------------------------------------------------------- */
const initiateValidationPool = useCallback(async (postId, poolDuration) => {
const web3 = new Web3(provider);
await DAORef.current.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, DAORef, account]);
const stake = useCallback(async (poolIndex, amount, inFavor) => {
console.log(`Attempting to stake ${amount} ${inFavor ? 'for' : 'against'} pool ${poolIndex}`);
await DAORef.current.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));
}, [DAORef, account, setReputation]);
const stakeHalfInFavor = useCallback(async (poolIndex) => {
await stake(poolIndex, reputation / BigInt(2), true);
}, [stake, reputation]);
const evaluateOutcome = useCallback(async (poolIndex) => {
await DAORef.current.methods.evaluateOutcome(poolIndex).send({
from: account,
gas: 10000000,
});
}, [DAORef, account]);
const handleShowAddPost = () => setShowAddPost(true);
const handleShowViewPost = useCallback(async ({ id }) => {
const post = await Post.read(id);
setViewPost(post);
setShowViewPost(true);
}, [setViewPost, setShowViewPost]);
/* -------------------------------------------------------------------------------- */
/* --------------------------- END UI ACTIONS ------------------------------------- */
/* -------------------------------------------------------------------------------- */
return (
<>
<AddPostModal show={showAddPost} setShow={setShowAddPost} postToBlockchain />
<ViewPostModal show={showViewPost} setShow={setShowViewPost} post={viewPost} />
<Tabs>
<Tab eventKey="admin" title="Admin">
<h2>Members</h2>
<div>
{`Members count: ${members.length}`}
</div>
<div>
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>Reputation</th>
</tr>
</thead>
<tbody>
{members.filter((x) => !!x).map((member) => (
<tr key={member.id}>
<td>{member.id}</td>
<td>{member.reputation.toString()}</td>
</tr>
))}
</tbody>
</table>
</div>
{' '}
<h2>Posts</h2>
<div>
<Button onClick={handleShowAddPost}>Add Post</Button>
</div>
<div>
{`Posts count: ${posts.length}`}
</div>
<div>
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>Authors</th>
<th>Sender</th>
<th>Reputation</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{posts.filter((x) => !!x).map((post) => (
<tr key={post.id}>
<td>{post.id.toString()}</td>
<td>
<Stack>
{post.authors.map(({ authorAddress, weightPPM }) => (
<div key={authorAddress}>
{getAddressName(chainId, authorAddress)}
{' '}
{Number(weightPPM) / 10000}
%
</div>
))}
</Stack>
</td>
<td>{getAddressName(chainId, post.sender)}</td>
<td>{post.reputation.toString()}</td>
<td>
<Button onClick={() => handleShowViewPost(post)}>
View Post
</Button>
{' '}
Initiate Validation Pool
{' '}
<Button onClick={() => initiateValidationPool(post.id, 1)}>
1s
</Button>
{' '}
<Button onClick={() => initiateValidationPool(post.id, 20)}>
20s
</Button>
{' '}
<Button onClick={() => initiateValidationPool(post.id, 60)}>
60s
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<h2>Validation Pools</h2>
<div>
{`Validation Pool Count: ${validationPools.length}`}
</div>
<div>
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>Post ID</th>
<th>Sender</th>
<th>Fee</th>
<th>Binding</th>
<th>Quorum</th>
<th>WinRatio</th>
<th>
Redistribute
<br />
Losing Stakes
</th>
<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.postId}</td>
<td>{getAddressName(chainId, pool.sender)}</td>
<td>{pool.fee.toString()}</td>
<td>
{pool.params.bindingPercent.toString()}
%
</td>
<td>{`${pool.params.quorum[0].toString()}/${pool.params.quorum[1].toString()}`}</td>
<td>{`${pool.params.winRatio[0].toString()}/${pool.params.winRatio[1].toString()}`}</td>
<td>{pool.params.redistributeLosingStakes.toString()}</td>
<td>{pool.params.duration.toString()}</td>
<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
</Button>
{' '}
<Button onClick={() => stake(pool.id, reputation, true)}>
Stake All
</Button>
{' '}
</>
)}
{!pool.resolved && (pool.timeRemaining <= 0 || !reputation) && (
<Button onClick={() => evaluateOutcome(pool.id)}>
Evaluate Outcome
</Button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</Tab>
<Tab eventKey="worker" title="Worker">
{work1 && (
<WorkContract
workContract={work1}
title="Work Contract 1"
verb="Work"
showProposePriceChange
/>
)}
{onboarding && (
<WorkContract
workContract={onboarding}
title="Onboarding"
verb="Onboarding"
showRequestWork
showProposePriceChange
/>
)}
{work2 && (
<WorkContract
workContract={work2}
title="Work Contract 2"
verb="Work"
showProposePriceChange
/>
)}
</Tab>
<Tab eventKey="customer" title="Customer">
{work1 && (
<WorkContract
workContract={work1}
showAvailabilityActions={false}
showAvailabilityAmount={false}
onlyShowAvailable
title="Work Contract 1"
verb="Work"
showRequestWork
/>
)}
</Tab>
<Tab eventKey="proposals" title="Proposals">
<Proposals />
</Tab>
<Tab eventKey="import" title="Import">
<h1>Semantic Scholar</h1>
<ImportPaper />
<ImportPapersByAuthor />
<h1>Matrix</h1>
<ImportMatrixEvent />
</Tab>
</Tabs>
</>
);
}
export default MainTabs;

View File

@ -0,0 +1,238 @@
import {
useCallback, useEffect, useState, useMemo, useRef,
} from 'react';
import { useSDK } from '@metamask/sdk-react';
import { Web3 } from 'web3';
import PropTypes from 'prop-types';
import Web3Context from './Web3Context';
import useList from '../utils/List';
import { getContractAddressByChainId } from '../utils/contract-config';
import DAOArtifact from '../../contractArtifacts/DAO.json';
import Work1Artifact from '../../contractArtifacts/Work1.json';
import OnboardingArtifact from '../../contractArtifacts/Onboarding.json';
import Work2Artifact from '../../contractArtifacts/Work2.json';
function MainContextProvider({ children }) {
const {
sdk, connected, provider, chainId, account, balance,
} = useSDK();
const [DAO, setDAO] = useState();
const [work1, setWork1] = useState();
const [work2, setWork2] = useState();
const [onboarding, setOnboarding] = useState();
const DAORef = useRef();
const [balanceEther, setBalanceEther] = useState();
const [reputation, setReputation] = useState();
const [totalReputation, setTotalReputation] = useState();
const [posts, dispatchPost] = useList();
const [validationPools, dispatchValidationPool] = useList();
const [members, dispatchMember] = useList();
const web3ProviderValue = useMemo(() => ({
provider,
reputation,
setReputation,
totalReputation,
account,
chainId,
posts,
DAO,
DAORef,
work1,
work2,
onboarding,
members,
validationPools,
balanceEther,
sdk,
connected,
}), [
provider, reputation, setReputation, account, chainId, posts, balanceEther, totalReputation,
DAO, DAORef, work1, work2, onboarding, members, validationPools, sdk, connected]);
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 Work2Address = getContractAddressByChainId(chainId, 'Work2');
const web3 = new Web3(provider);
DAORef.current = new web3.eth.Contract(DAOArtifact.abi, DAOAddress);
setDAO(DAORef.current);
const work1Contract = new web3.eth.Contract(Work1Artifact.abi, Work1Address);
setWork1(work1Contract);
const onboardingContract = new web3.eth.Contract(OnboardingArtifact.abi, OnboardingAddress);
setOnboarding(onboardingContract);
const work2Contract = new web3.eth.Contract(Work2Artifact.abi, Work2Address);
setWork2(work2Contract);
fetchReputation();
fetchMembers();
fetchPosts();
fetchValidationPools();
// const fetchReputationInterval = setInterval(() => {
// // console.log('reputation', reputation);
// if (reputation !== undefined) {
// clearInterval(fetchReputationInterval);
// return;
// }
// fetchReputation();
// }, 1000);
/* -------------------------------------------------------------------------------- */
/* --------------------------- BEGIN EVENT HANDLERS ------------------------------- */
/* -------------------------------------------------------------------------------- */
DAORef.current.events.PostAdded({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: post added');
fetchPost(event.returnValues.id);
});
DAORef.current.events.ValidationPoolInitiated({ fromBlock: 'latest' }).on('data', (event) => {
console.log('event: validation pool initiated');
fetchValidationPool(event.returnValues.poolIndex);
});
DAORef.current.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 () => {
DAORef.current.events.PostAdded().off();
DAORef.current.events.ValidationPoolInitiated().off();
DAORef.current.events.ValidationPoolResolved().off();
work1Contract.events.AvailabilityStaked().off();
onboardingContract.events.AvailabilityStaked().off();
};
}, [provider, account, chainId, balance, dispatchValidationPool, dispatchPost,
DAORef, setDAO, setWork1, setOnboarding, setWork2,
fetchPost, fetchPosts, fetchReputation, fetchValidationPool, fetchValidationPools, fetchMembers,
]);
/* -------------------------------------------------------------------------------- */
/* --------------------------- END MAIN INITIALIZION EFFECT ----------------------- */
/* -------------------------------------------------------------------------------- */
return (
<Web3Context.Provider value={web3ProviderValue}>
{children}
</Web3Context.Provider>
);
}
MainContextProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export default MainContextProvider;

View File

@ -0,0 +1,9 @@
import { useContext } from 'react';
import Web3Context from './Web3Context';
function useMainContext() {
return useContext(Web3Context);
}
export default useMainContext;

View File

@ -10,6 +10,7 @@ import {
import WebApp from './WebApp';
import Widget from './Widget';
import MainContextProvider from './contexts/MainContextProvider';
const router = createBrowserRouter([
{
@ -37,7 +38,9 @@ ReactDOM.createRoot(document.getElementById('root')).render(
},
}}
>
<RouterProvider router={router} />
<MainContextProvider>
<RouterProvider router={router} />
</MainContextProvider>
</MetaMaskProvider>
</React.StrictMode>,
);