Add backend API to house forum content
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 38s Details

This commit is contained in:
Ladd Hoffman 2024-03-19 22:22:36 -05:00
parent 8da5772d86
commit afe9da73c9
29 changed files with 14130 additions and 296 deletions

2
backend/.env.example Normal file
View File

@ -0,0 +1,2 @@
PORT=3000
DATA_DIR="./data"

20
backend/.eslintrc.cjs Normal file
View File

@ -0,0 +1,20 @@
module.exports = {
root: true,
env: { es2020: true },
extends: [
'eslint:recommended',
'airbnb',
],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
rules: {
'no-console': 'off',
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: false,
optionalDependencies: false,
peerDependencies: false,
},
],
},
};

3
backend/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/
.env
data/

9
backend/Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM node
WORKDIR /app
ADD package.json package-lock.json index.js /app/
RUN npm ci
ENTRYPOINT ["node", "index.js"]

View File

@ -0,0 +1,10 @@
services:
backend:
build: .
restart: always
ports:
- "3002:3000"
volumes:
- ./data:/data
environment:
- DATA_DIR=/data

45
backend/index.js Normal file
View File

@ -0,0 +1,45 @@
const express = require('express');
const { Level } = require('level');
const {
createHash,
} = require('node:crypto');
require('dotenv').config();
const app = express();
const port = process.env.PORT || 3000;
const dataDir = process.env.DATA_DIR || 'data';
const db = new Level(`${dataDir}/forum`, { valueEncoding: 'json' });
app.use(express.json());
app.post('/write', async (req, res) => {
const { body: data } = req;
// TODO: Require author signature
// Compute content hash
const hash = createHash('sha256').update(JSON.stringify(data)).digest('base64url');
console.log('write', hash);
// Store content
db.put(hash, data);
// Return hash
res.send(hash);
});
app.get('/read/:hash', async (req, res) => {
// Fetch content
const { hash } = req.params;
const data = await db.get(req.params.hash);
console.log('read', hash);
// Return content
res.json(data);
});
app.get('*', (req, res) => {
console.log(`req.path: ${req.path}`);
res.send('Hello World!');
});
app.listen(port, () => {
console.log(`Listening on port ${port}`);
});

4220
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
backend/package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^1.6.7",
"casper-js-sdk": "^2.15.4",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"level": "^8.0.1"
},
"devDependencies": {
"eslint": "^8.56.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0"
}
}

9565
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,11 +10,16 @@
"preview": "vite preview"
},
"dependencies": {
"@helia/dag-json": "^3.0.2",
"@libp2p/websockets": "^8.0.16",
"@metamask/sdk-react": "^0.16.0",
"@multiformats/multiaddr": "^12.2.1",
"@tanstack/react-table": "^8.13.2",
"axios": "^1.6.7",
"axios": "^1.6.8",
"bootstrap": "^5.3.3",
"bootswatch": "^5.3.3",
"helia": "^4.1.0",
"ipfs-core": "^0.18.1",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-bootstrap": "^2.10.1",

View File

@ -3,6 +3,8 @@ import {
} from 'react';
import { useSDK } from '@metamask/sdk-react';
import { Web3 } from 'web3';
import axios from 'axios';
import Button from 'react-bootstrap/Button';
import Tab from 'react-bootstrap/Tab';
import Tabs from 'react-bootstrap/Tabs';
@ -10,14 +12,16 @@ 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 Modal from 'react-bootstrap/Modal';
import Form from 'react-bootstrap/Form';
import useList from './List';
import { getContractByChainId, getContractNameByAddress } from './contract-config';
import Web3Context from './Web3Context';
import useList from './utils/List';
import { getContractByChainId, getContractNameByAddress } from './utils/contract-config';
import Web3Context from './contexts/Web3Context';
import DAOArtifact from './assets/DAO.json';
import Work1Artifact from './assets/Work1.json';
import OnboardingArtifact from './assets/Onboarding.json';
import WorkContract from './WorkContract';
import WorkContract from './components/WorkContract';
function App() {
const {
@ -33,6 +37,11 @@ function App() {
const [posts, dispatchPost] = useList();
const [validationPools, dispatchValidationPool] = useList();
const [showAddPost, setShowAddPost] = useState(false);
const [addPostContent, setAddPostContent] = useState('');
const [showViewPost, setShowViewPost] = useState(false);
const [viewPostContent, setViewPostContent] = useState('');
const web3ProviderValue = useMemo(() => ({
provider,
DAO,
@ -201,8 +210,8 @@ function App() {
});
}, [provider, chainId]);
const addPost = useCallback(async () => {
await DAO.methods.addPost(account).send({
const addPost = useCallback(async (contentId) => {
await DAO.methods.addPost(account, contentId).send({
from: account,
gas: 1000000,
});
@ -246,6 +255,30 @@ function App() {
});
}, [DAO, account]);
const handleCloseAddPost = () => setShowAddPost(false);
const handleShowAddPost = () => setShowAddPost(true);
const handleSubmitAddPost = async () => {
// Upload content to API
// TODO: include metamask signature
const data = { author: account, content: addPostContent };
const res = await axios.post('/api/write', data);
const hash = res.data;
// Upload hash to blockchain
await addPost(hash);
setShowAddPost(false);
setAddPostContent('');
};
const handleCloseViewPost = () => setShowViewPost(false);
const handleShowViewPost = async (post) => {
const res = await axios.get(`/api/read/${post.contentId}`);
const { data } = res;
// TODO: Verify base64url(sha256(JSON.stringify(data))) = contentId
// TODO: Verify data.author = post.author
setViewPostContent(data.content);
setShowViewPost(true);
};
/* -------------------------------------------------------------------------------- */
/* --------------------------- END UI ACTIONS ------------------------------------- */
/* -------------------------------------------------------------------------------- */
@ -258,6 +291,48 @@ function App() {
return (
<Web3Context.Provider value={web3ProviderValue}>
<Modal show={showAddPost} onHide={handleCloseAddPost}>
<Modal.Header closeButton>
<Modal.Title>Add Post</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form>
<Form.Group className="mb-3" controlId="addPost.content">
<Form.Label>Example textarea</Form.Label>
<Form.Control
as="textarea"
rows={3}
onChange={(e) => setAddPostContent(e.target.value)}
/>
</Form.Group>
</Form>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={handleCloseAddPost}>
Close
</Button>
<Button variant="primary" onClick={handleSubmitAddPost}>
Submit
</Button>
</Modal.Footer>
</Modal>
<Modal show={showViewPost} onHide={handleCloseViewPost}>
<Modal.Header closeButton>
<Modal.Title>View Post</Modal.Title>
</Modal.Header>
<Modal.Body>
{viewPostContent}
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={handleCloseViewPost}>
Close
</Button>
</Modal.Footer>
</Modal>
{!connected && <Button onClick={() => connect()}>Connect</Button>}
{connected && (
@ -325,6 +400,10 @@ function App() {
<td>{post.author}</td>
<td>{getAdressName(post.sender)}</td>
<td>
<Button onClick={() => handleShowViewPost(post)}>
View Post
</Button>
{' '}
Initiate Validation Pool
{' '}
<Button onClick={() => initiateValidationPool(post.id, 60)}>
@ -345,7 +424,7 @@ function App() {
</table>
</div>
<div>
<Button onClick={() => addPost()}>Add Post</Button>
<Button onClick={handleShowAddPost}>Add Post</Button>
</div>
<div>
{`Validation Pool Count: ${validationPools.length}`}

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

@ -2,9 +2,9 @@ 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';
import { getContractByChainId } from '../utils/contract-config';
import Web3Context from '../contexts/Web3Context';
import WorkContractContext from '../contexts/WorkContractContext';
const getAvailabilityStatus = (stake) => {
if (stake.reclaimed) return 'Reclaimed';

View File

@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { PropTypes } from 'prop-types';
import useList from './List';
import WorkContractContext from './WorkContractContext';
import useList from '../utils/List';
import WorkContractContext from '../contexts/WorkContractContext';
import AvailabilityStakes from './AvailabilityStakes';
import WorkRequests from './WorkRequests';

View File

@ -5,9 +5,9 @@ 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';
import Web3Context from '../contexts/Web3Context';
import useList from '../utils/List';
import WorkContractContext from '../contexts/WorkContractContext';
const getRequestStatus = (request) => {
switch (Number(request.status)) {
@ -85,9 +85,10 @@ function WorkRequests({
}, [workContract, fetchWorkRequests, fetchPrice, fetchWorkRequest]);
const requestWork = useCallback(async () => {
// TODO: Accept input, upload to API, include hash in contract call
const web3 = new Web3(provider);
const priceWei = BigInt(web3.utils.toWei(price, 'ether'));
await workContract.methods.requestWork().send({
await workContract.methods.requestWork('').send({
from: account,
gas: 1000000,
value: priceWei,
@ -95,7 +96,8 @@ function WorkRequests({
}, [provider, workContract, account, price]);
const submitWorkEvidence = useCallback(async (requestIndex) => {
await workContract.methods.submitWorkEvidence(requestIndex).send({
// TODO: Accept input, upload to API, include hash in contract call
await workContract.methods.submitWorkEvidence(requestIndex, '').send({
from: account,
gas: 1000000,
});

View File

@ -1,12 +1,12 @@
{
"localhost": {
"DAO": "0x4CC737e8Ec8873abCBC98c8c4a401990d6Fc4F38",
"Work1": "0xa0A1c1e84F50f6786A6927b448903a37776D2e74",
"Onboarding": "0xA6bE53f1F25816c65CF3bc36d6F8793Eed14fd09"
"DAO": "0x2D812555F4eF06267406D80E7fA01Ac3288f626c",
"Work1": "0x3CAB55d59af095618F2ee539463E33447cfc97BA",
"Onboarding": "0xaB0c7Cf9A436978F55831C8EdB67892419ABAE62"
},
"sepolia": {
"DAO": "0x8611676563EBcd69dC52E5829bF2914A957398C3",
"Work1": "0xCd5881DB1aa6b86283a9c5660FaB65C989cf2721",
"Onboarding": "0x215078c5cf21ffB79Ee14Cf09156B94a11b7340f"
"DAO": "0xa3b15aBD114C2332652A4fD5f9A43B86315E5078",
"Work1": "0x7C4F9cA684B875d21e121E9C1D718f89A2603d0d",
"Onboarding": "0x6C04aA0e984193A2A624B56F6407594f45d6554c"
}
}

View File

@ -1,4 +1,4 @@
import contractAddresses from './contract-addresses.json';
import contractAddresses from '../contract-addresses.json';
const networks = {
localhost: '0x539',

View File

@ -1,12 +1,12 @@
{
"localhost": {
"DAO": "0x4CC737e8Ec8873abCBC98c8c4a401990d6Fc4F38",
"Work1": "0xa0A1c1e84F50f6786A6927b448903a37776D2e74",
"Onboarding": "0xA6bE53f1F25816c65CF3bc36d6F8793Eed14fd09"
"DAO": "0x2D812555F4eF06267406D80E7fA01Ac3288f626c",
"Work1": "0x3CAB55d59af095618F2ee539463E33447cfc97BA",
"Onboarding": "0xaB0c7Cf9A436978F55831C8EdB67892419ABAE62"
},
"sepolia": {
"DAO": "0x8611676563EBcd69dC52E5829bF2914A957398C3",
"Work1": "0xCd5881DB1aa6b86283a9c5660FaB65C989cf2721",
"Onboarding": "0x215078c5cf21ffB79Ee14Cf09156B94a11b7340f"
"DAO": "0xa3b15aBD114C2332652A4fD5f9A43B86315E5078",
"Work1": "0x7C4F9cA684B875d21e121E9C1D718f89A2603d0d",
"Onboarding": "0x6C04aA0e984193A2A624B56F6407594f45d6554c"
}
}

View File

@ -11,6 +11,7 @@ struct Post {
uint id;
address sender;
address author;
string contentId;
}
struct Stake {
@ -61,12 +62,16 @@ contract DAO is ERC20("Reputation", "REP") {
event ValidationPoolInitiated(uint poolIndex);
event ValidationPoolResolved(uint poolIndex, bool votePasses);
function addPost(address author) public returns (uint postIndex) {
function addPost(
address author,
string calldata contentId
) external returns (uint postIndex) {
postIndex = postCount++;
Post storage post = posts[postIndex];
post.author = author;
post.sender = msg.sender;
post.id = postIndex;
post.contentId = contentId;
emit PostAdded(postIndex);
}
@ -78,7 +83,7 @@ contract DAO is ERC20("Reputation", "REP") {
uint duration,
bool callbackOnValidate,
bytes calldata callbackData
) public payable returns (uint poolIndex) {
) external payable returns (uint poolIndex) {
require(msg.value > 0, "Fee is required to initiate validation pool");
require(duration >= minDuration, "Duration is too short");
require(duration <= maxDuration, "Duration is too long");
@ -161,6 +166,12 @@ contract DAO is ERC20("Reputation", "REP") {
members[memberCount++] = post.author;
isMember[post.author] = true;
}
console.log(
"stakedFor: %d, stakedAgainst: %d, stakeCount: %d",
stakedFor,
stakedAgainst,
pool.stakeCount
);
pool.resolved = true;
pool.outcome = votePasses;
emit ValidationPoolResolved(poolIndex, votePasses);

View File

@ -22,7 +22,7 @@ contract Onboarding is WorkContract, IOnValidate {
request.status = WorkStatus.ApprovalSubmitted;
request.approval = approval;
// Make work evidence post
uint postIndex = dao.addPost(stake.worker);
uint postIndex = dao.addPost(stake.worker, request.evidenceContentId);
emit WorkApprovalSubmitted(requestIndex, approval);
// Initiate validation pool
uint poolIndex = dao.initiateValidationPool{
@ -44,7 +44,10 @@ contract Onboarding is WorkContract, IOnValidate {
payable(request.customer).transfer(request.fee / 10);
return;
}
uint postIndex = dao.addPost(request.customer);
uint postIndex = dao.addPost(
request.customer,
request.requestContentId
);
dao.initiateValidationPool{value: request.fee / 10}(
postIndex,
POOL_DURATION,

View File

@ -25,6 +25,8 @@ abstract contract WorkContract is IAcceptAvailability {
uint256 fee;
WorkStatus status;
uint stakeIndex;
string requestContentId;
string evidenceContentId;
bool approval;
}
@ -127,17 +129,21 @@ abstract contract WorkContract is IAcceptAvailability {
}
/// Accept work request with fee
function requestWork() external payable {
function requestWork(string calldata requestContentId) external payable {
require(msg.value >= price, "Insufficient fee");
uint requestIndex = requestCount++;
WorkRequest storage request = requests[requestIndex];
request.customer = msg.sender;
request.fee = msg.value;
request.stakeIndex = assignWork(requestIndex);
request.requestContentId = requestContentId;
}
/// Accept work evidence from worker
function submitWorkEvidence(uint requestIndex) external {
function submitWorkEvidence(
uint requestIndex,
string calldata evidenceContentId
) external {
WorkRequest storage request = requests[requestIndex];
require(
request.status == WorkStatus.Requested,
@ -149,6 +155,7 @@ abstract contract WorkContract is IAcceptAvailability {
"Worker can only submit evidence for work they are assigned"
);
request.status = WorkStatus.EvidenceSubmitted;
request.evidenceContentId = evidenceContentId;
emit WorkEvidenceSubmitted(requestIndex);
}
@ -166,7 +173,7 @@ abstract contract WorkContract is IAcceptAvailability {
request.status = WorkStatus.ApprovalSubmitted;
request.approval = approval;
// Make work evidence post
uint postIndex = dao.addPost(stake.worker);
uint postIndex = dao.addPost(stake.worker, request.evidenceContentId);
emit WorkApprovalSubmitted(requestIndex, approval);
// Initiate validation pool
uint poolIndex = dao.initiateValidationPool{value: request.fee}(

View File

@ -22,18 +22,22 @@ describe('DAO', () => {
describe('Post', () => {
it('should be able to add a post', async () => {
const { dao, account1 } = await loadFixture(deploy);
await expect(dao.addPost(account1)).to.emit(dao, 'PostAdded').withArgs(0);
const contentId = 'some-id';
await expect(dao.addPost(account1, contentId)).to.emit(dao, 'PostAdded').withArgs(0);
const post = await dao.posts(0);
expect(post.author).to.equal(account1);
expect(post.sender).to.equal(account1);
expect(post.contentId).to.equal(contentId);
});
it('should be able to add a post on behalf of another account', async () => {
const { dao, account1, account2 } = await loadFixture(deploy);
await dao.addPost(account2);
const contentId = 'some-id';
await dao.addPost(account2, contentId);
const post = await dao.posts(0);
expect(post.author).to.equal(account2);
expect(post.sender).to.equal(account1);
expect(post.contentId).to.equal(contentId);
});
});
@ -46,7 +50,7 @@ describe('DAO', () => {
beforeEach(async () => {
({ dao, account1 } = await loadFixture(deploy));
await dao.addPost(account1);
await dao.addPost(account1, 'content-id');
const init = () => dao.initiateValidationPool(
0,
POOL_DURATION,

View File

@ -17,7 +17,7 @@ describe('Onboarding', () => {
const Onboarding = await ethers.getContractFactory('Onboarding');
const onboarding = await Onboarding.deploy(dao.target, PRICE);
await dao.addPost(account1);
await dao.addPost(account1, 'content-id');
const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
await dao.initiateValidationPool(0, 60, false, callbackData, { value: 100 });
await time.increase(61);
@ -39,165 +39,7 @@ describe('Onboarding', () => {
expect(await onboarding.stakeCount()).to.equal(0);
});
describe('Stake availability', () => {
let dao;
let onboarding;
let account1;
let account2;
beforeEach(async () => {
({
dao, onboarding, account1, account2,
} = await loadFixture(deploy));
await expect(dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION)).to.emit(onboarding, 'AvailabilityStaked').withArgs(0);
});
it('Should be able to stake availability', async () => {
expect(await dao.balanceOf(account1)).to.equal(50);
expect(await dao.balanceOf(onboarding.target)).to.equal(50);
expect(await onboarding.stakeCount()).to.equal(1);
const stake = await onboarding.stakes(0);
expect(stake.worker).to.equal(account1);
expect(stake.amount).to.equal(50);
expect(stake.endTime).to.equal(await time.latest() + STAKE_DURATION);
});
it('should not be able to stake availability without reputation value', async () => {
await expect(dao.stakeAvailability(onboarding.target, 0, STAKE_DURATION)).to.be.revertedWith('No stake provided');
});
it('should be able to reclaim staked availability after duration elapses', async () => {
expect(await dao.balanceOf(account1)).to.equal(50);
time.increase(STAKE_DURATION + 1);
await expect(onboarding.reclaimAvailability(0)).to.emit(onboarding, 'AvailabilityStaked').withArgs(0);
expect(await dao.balanceOf(account1)).to.equal(100);
});
it('should not be able to reclaim staked availability twice', async () => {
expect(await dao.balanceOf(account1)).to.equal(50);
time.increase(STAKE_DURATION + 1);
await onboarding.reclaimAvailability(0);
await expect(onboarding.reclaimAvailability(0)).to.be.revertedWith('Stake has already been reclaimed');
});
it('should not be able to reclaim staked availability before duration elapses', async () => {
await expect(onboarding.reclaimAvailability(0)).to.be.revertedWith('Stake duration has not yet elapsed');
});
it('should not be able to reclaim availability staked by another account', async () => {
time.increase(STAKE_DURATION + 1);
await expect(onboarding.connect(account2).reclaimAvailability(0)).to.be.revertedWith('Worker can only reclaim their own availability stake');
});
it('should be able to extend the duration of an availability stake before it expires', async () => {
await time.increase(STAKE_DURATION / 2);
await expect(onboarding.extendAvailability(0, STAKE_DURATION)).to.emit(onboarding, 'AvailabilityStaked').withArgs(0);
});
it('should be able to extend the duration of an availability stake after it expires', async () => {
await time.increase(STAKE_DURATION * 2);
await onboarding.extendAvailability(0, STAKE_DURATION);
});
it('should not be able to extend the duration of another worker\'s availability stake', async () => {
await time.increase(STAKE_DURATION * 2);
await expect(onboarding.connect(account2).extendAvailability(0, STAKE_DURATION)).to.be.revertedWith('Worker can only extend their own availability stake');
});
it('should not be able to extend a stake that has been reclaimed', async () => {
await time.increase(STAKE_DURATION * 2);
await onboarding.reclaimAvailability(0);
await expect(onboarding.extendAvailability(0, STAKE_DURATION)).to.be.revertedWith('Stake has already been reclaimed');
});
it('extending a stake before expiration should increase the end time by the given duration', async () => {
await time.increase(STAKE_DURATION / 2);
await onboarding.extendAvailability(0, STAKE_DURATION * 2);
const expectedEndTime = await time.latest() + 2.5 * STAKE_DURATION;
const stake = await onboarding.stakes(0);
expect(stake.endTime).to.be.within(expectedEndTime - 1, expectedEndTime);
});
it('extending a stake after expiration should restart the stake for the given duration', async () => {
await time.increase(STAKE_DURATION * 2);
await onboarding.extendAvailability(0, STAKE_DURATION * 2);
const expectedEndTime = await time.latest() + STAKE_DURATION * 2;
const stake = await onboarding.stakes(0);
expect(stake.endTime).to.be.within(expectedEndTime - 1, expectedEndTime);
});
});
describe('Request and assign work', () => {
it('should be able to request work and assign to a worker', async () => {
const {
dao, onboarding, account1, account2,
} = await loadFixture(deploy);
await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION);
const requestWork = () => onboarding.connect(account2).requestWork({ value: PRICE });
await expect(requestWork()).to.emit(onboarding, 'WorkAssigned').withArgs(account1, 0);
expect(await onboarding.requestCount()).to.equal(1);
const request = await onboarding.requests(0);
expect(request.customer).to.equal(account2);
});
it('should not be able to reclaim stake after work is assigned', async () => {
const {
dao, onboarding, account1, account2,
} = await loadFixture(deploy);
await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION);
const requestWork = () => onboarding.connect(account2).requestWork({ value: PRICE });
await expect(requestWork()).to.emit(onboarding, 'WorkAssigned').withArgs(account1, 0);
await time.increase(STAKE_DURATION + 1);
await expect(onboarding.reclaimAvailability(0)).to.be.revertedWith('Stake has already been assigned work');
});
it('should not be able to request work if there are no availability stakes', async () => {
const {
onboarding, account2,
} = await loadFixture(deploy);
const requestWork = () => onboarding.connect(account2).requestWork({ value: PRICE });
await expect(requestWork()).to.be.revertedWith('No available worker stakes');
});
it('should not be able to request work if fee is insufficient', async () => {
const {
onboarding, account2,
} = await loadFixture(deploy);
const requestWork = () => onboarding.connect(account2).requestWork({ value: PRICE / 2 });
await expect(requestWork()).to.be.revertedWith('Insufficient fee');
});
it('should not assign work to an expired availability stake', async () => {
const {
dao, onboarding, account2,
} = await loadFixture(deploy);
await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION);
const requestWork = () => onboarding.connect(account2).requestWork({ value: PRICE });
await time.increase(STAKE_DURATION + 1);
await expect(requestWork()).to.be.revertedWith('No available worker stakes');
});
it('should not assign work to the same availability stake twice', async () => {
const {
dao, onboarding, account1, account2,
} = await loadFixture(deploy);
await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION);
const requestWork = () => onboarding.connect(account2).requestWork({ value: PRICE });
await expect(requestWork()).to.emit(onboarding, 'WorkAssigned').withArgs(account1, 0);
await expect(requestWork()).to.be.revertedWith('No available worker stakes');
});
it('should not be able to extend a stake that has been assigned work', async () => {
const {
dao, onboarding, account2,
} = await loadFixture(deploy);
await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION);
await onboarding.connect(account2).requestWork({ value: PRICE });
await time.increase(STAKE_DURATION * 2);
await expect(onboarding.extendAvailability(0, STAKE_DURATION)).to.be.revertedWith('Stake has already been assigned work');
});
});
describe('Work evidence and approval/disapproval', () => {
describe('Work approval/disapproval', () => {
let dao;
let onboarding;
let account1;
@ -210,31 +52,16 @@ describe('Onboarding', () => {
await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION);
});
it('should be able to submit work evidence', async () => {
await onboarding.connect(account2).requestWork({ value: PRICE });
await expect(onboarding.submitWorkEvidence(0)).to.emit(onboarding, 'WorkEvidenceSubmitted').withArgs(0);
});
it('should not be able to submit work evidence twice', async () => {
await onboarding.connect(account2).requestWork({ value: PRICE });
await expect(onboarding.submitWorkEvidence(0)).to.emit(onboarding, 'WorkEvidenceSubmitted').withArgs(0);
await expect(onboarding.submitWorkEvidence(0)).to.be.revertedWith('Status must be Requested');
});
it('should not be able to submit work evidence for a different worker', async () => {
await onboarding.connect(account2).requestWork({ value: PRICE });
await expect(onboarding.connect(account2).submitWorkEvidence(0)).to.be.revertedWith('Worker can only submit evidence for work they are assigned');
});
it('should be able to submit work approval', async () => {
await onboarding.connect(account2).requestWork({ value: PRICE });
await onboarding.submitWorkEvidence(0);
await onboarding.connect(account2).requestWork('req-content-id', { value: PRICE });
await onboarding.submitWorkEvidence(0, 'evidence-content-id');
await expect(onboarding.submitWorkApproval(0, true))
.to.emit(dao, 'ValidationPoolInitiated').withArgs(1)
.to.emit(onboarding, 'WorkApprovalSubmitted').withArgs(0, true);
const post = await dao.posts(1);
expect(post.author).to.equal(account1);
expect(post.sender).to.equal(onboarding.target);
expect(post.contentId).to.equal('evidence-content-id');
const pool = await dao.validationPools(1);
expect(pool.postIndex).to.equal(1);
expect(pool.fee).to.equal(PRICE * 0.9);
@ -242,29 +69,29 @@ describe('Onboarding', () => {
});
it('should be able to submit work disapproval', async () => {
await onboarding.connect(account2).requestWork({ value: PRICE });
await onboarding.submitWorkEvidence(0);
await onboarding.connect(account2).requestWork('req-content-id', { value: PRICE });
await onboarding.submitWorkEvidence(0, 'evidence-content-id');
await expect(onboarding.submitWorkApproval(0, false))
.to.emit(dao, 'ValidationPoolInitiated').withArgs(1)
.to.emit(onboarding, 'WorkApprovalSubmitted').withArgs(0, false);
});
it('should not be able to submit work approval/disapproval twice', async () => {
await onboarding.connect(account2).requestWork({ value: PRICE });
await onboarding.submitWorkEvidence(0);
await onboarding.connect(account2).requestWork('req-content-id', { value: PRICE });
await onboarding.submitWorkEvidence(0, 'evidence-content-id');
await expect(onboarding.submitWorkApproval(0, true)).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
await expect(onboarding.submitWorkApproval(0, true)).to.be.revertedWith('Status must be EvidenceSubmitted');
});
it('should not be able to submit work evidence after work approval', async () => {
await onboarding.connect(account2).requestWork({ value: PRICE });
await onboarding.submitWorkEvidence(0);
await onboarding.connect(account2).requestWork('req-content-id', { value: PRICE });
await onboarding.submitWorkEvidence(0, 'evidence-content-id');
await expect(onboarding.submitWorkApproval(0, true)).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
await expect(onboarding.submitWorkEvidence(0)).to.be.revertedWith('Status must be Requested');
await expect(onboarding.submitWorkEvidence(0, 'evidence-content-id')).to.be.revertedWith('Status must be Requested');
});
it('should not be able to submit work approval/disapproval before work evidence', async () => {
await onboarding.connect(account2).requestWork({ value: PRICE });
await onboarding.connect(account2).requestWork('req-content-id', { value: PRICE });
await expect(onboarding.submitWorkApproval(0, true)).to.be.revertedWith('Status must be EvidenceSubmitted');
});
});
@ -275,8 +102,8 @@ describe('Onboarding', () => {
dao, onboarding, account2,
} = await loadFixture(deploy);
await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION);
await onboarding.connect(account2).requestWork({ value: PRICE });
await onboarding.submitWorkEvidence(0);
await onboarding.connect(account2).requestWork('req-content-id', { value: PRICE });
await onboarding.submitWorkEvidence(0, 'evidence-content-id');
await expect(onboarding.submitWorkApproval(0, true)).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
await time.increase(86401);
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
@ -284,6 +111,7 @@ describe('Onboarding', () => {
const post = await dao.posts(2);
expect(post.author).to.equal(account2);
expect(post.sender).to.equal(onboarding.target);
expect(post.contentId).to.equal('req-content-id');
const pool = await dao.validationPools(2);
expect(pool.postIndex).to.equal(2);
expect(pool.fee).to.equal(PRICE * 0.1);
@ -295,11 +123,11 @@ describe('Onboarding', () => {
const {
dao, onboarding, account2,
} = await loadFixture(deploy);
await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION);
await onboarding.connect(account2).requestWork({ value: PRICE });
await onboarding.submitWorkEvidence(0);
await dao.stakeAvailability(onboarding.target, 40, STAKE_DURATION);
await onboarding.connect(account2).requestWork('req-content-id', { value: PRICE });
await onboarding.submitWorkEvidence(0, 'evidence-content-id');
await expect(onboarding.submitWorkApproval(0, true)).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
await dao.stake(1, 50, false);
await dao.stake(1, 60, false);
await time.increase(86401);
await expect(dao.evaluateOutcome(1)).not.to.emit(dao, 'ValidationPoolInitiated');
expect(await dao.postCount()).to.equal(2);

View File

@ -17,7 +17,7 @@ describe('Work1', () => {
const Work1 = await ethers.getContractFactory('Work1');
const work1 = await Work1.deploy(dao.target, WORK1_PRICE);
await dao.addPost(account1);
await dao.addPost(account1, 'some-content-id');
const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
await dao.initiateValidationPool(0, 60, false, callbackData, { value: 100 });
await time.increase(61);
@ -132,11 +132,12 @@ describe('Work1', () => {
dao, work1, account2,
} = await loadFixture(deploy);
await dao.stakeAvailability(work1.target, 50, STAKE_DURATION);
const requestWork = () => work1.connect(account2).requestWork({ value: WORK1_PRICE });
const requestWork = () => work1.connect(account2).requestWork('req-content-id', { value: WORK1_PRICE });
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);
expect(request.requestContentId).to.equal('req-content-id');
});
it('should not be able to reclaim stake after work is assigned', async () => {
@ -144,7 +145,7 @@ describe('Work1', () => {
dao, work1, account2,
} = await loadFixture(deploy);
await dao.stakeAvailability(work1.target, 50, STAKE_DURATION);
const requestWork = () => work1.connect(account2).requestWork({ value: WORK1_PRICE });
const requestWork = () => work1.connect(account2).requestWork('req-content-id', { value: WORK1_PRICE });
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');
@ -153,7 +154,7 @@ describe('Work1', () => {
const {
work1, account2,
} = await loadFixture(deploy);
const requestWork = () => work1.connect(account2).requestWork({ value: WORK1_PRICE });
const requestWork = () => work1.connect(account2).requestWork('req-content-id', { value: WORK1_PRICE });
await expect(requestWork()).to.be.revertedWith('No available worker stakes');
});
@ -161,7 +162,7 @@ describe('Work1', () => {
const {
work1, account2,
} = await loadFixture(deploy);
const requestWork = () => work1.connect(account2).requestWork({ value: WORK1_PRICE / 2 });
const requestWork = () => work1.connect(account2).requestWork('req-content-id', { value: WORK1_PRICE / 2 });
await expect(requestWork()).to.be.revertedWith('Insufficient fee');
});
@ -170,7 +171,7 @@ describe('Work1', () => {
dao, work1, account2,
} = await loadFixture(deploy);
await dao.stakeAvailability(work1.target, 50, STAKE_DURATION);
const requestWork = () => work1.connect(account2).requestWork({ value: WORK1_PRICE });
const requestWork = () => work1.connect(account2).requestWork('req-content-id', { value: WORK1_PRICE });
await time.increase(STAKE_DURATION + 1);
await expect(requestWork()).to.be.revertedWith('No available worker stakes');
});
@ -180,7 +181,7 @@ describe('Work1', () => {
dao, work1, account2,
} = await loadFixture(deploy);
await dao.stakeAvailability(work1.target, 50, STAKE_DURATION);
const requestWork = () => work1.connect(account2).requestWork({ value: WORK1_PRICE });
const requestWork = () => work1.connect(account2).requestWork('req-content-id', { value: WORK1_PRICE });
await expect(requestWork()).to.emit(work1, 'WorkAssigned').withArgs(0, 0);
await expect(requestWork()).to.be.revertedWith('No available worker stakes');
});
@ -190,7 +191,7 @@ describe('Work1', () => {
dao, work1, account2,
} = await loadFixture(deploy);
await dao.stakeAvailability(work1.target, 50, STAKE_DURATION);
await work1.connect(account2).requestWork({ value: WORK1_PRICE });
await work1.connect(account2).requestWork('req-content-id', { value: WORK1_PRICE });
await time.increase(STAKE_DURATION * 2);
await expect(work1.extendAvailability(0, STAKE_DURATION)).to.be.revertedWith('Stake has already been assigned work');
});
@ -210,30 +211,32 @@ describe('Work1', () => {
});
it('should be able to submit work evidence', async () => {
await work1.connect(account2).requestWork({ value: WORK1_PRICE });
await expect(work1.submitWorkEvidence(0)).to.emit(work1, 'WorkEvidenceSubmitted').withArgs(0);
await work1.connect(account2).requestWork('req-content-id', { value: WORK1_PRICE });
await expect(work1.submitWorkEvidence(0, 'evidence-content-id')).to.emit(work1, 'WorkEvidenceSubmitted').withArgs(0);
});
it('should not be able to submit work evidence twice', async () => {
await work1.connect(account2).requestWork({ value: WORK1_PRICE });
await expect(work1.submitWorkEvidence(0)).to.emit(work1, 'WorkEvidenceSubmitted').withArgs(0);
await expect(work1.submitWorkEvidence(0)).to.be.revertedWith('Status must be Requested');
await work1.connect(account2).requestWork('req-content-id', { value: WORK1_PRICE });
await expect(work1.submitWorkEvidence(0, 'evidence-content-id')).to.emit(work1, 'WorkEvidenceSubmitted').withArgs(0);
await expect(work1.submitWorkEvidence(0, 'evidence-content-id')).to.be.revertedWith('Status must be Requested');
});
it('should not be able to submit work evidence for a different worker', async () => {
await work1.connect(account2).requestWork({ value: WORK1_PRICE });
await expect(work1.connect(account2).submitWorkEvidence(0)).to.be.revertedWith('Worker can only submit evidence for work they are assigned');
await work1.connect(account2).requestWork('req-content-id', { value: WORK1_PRICE });
await expect(work1.connect(account2).submitWorkEvidence(0, 'evidence-content-id'))
.to.be.revertedWith('Worker can only submit evidence for work they are assigned');
});
it('should be able to submit work approval', async () => {
await work1.connect(account2).requestWork({ value: WORK1_PRICE });
await work1.submitWorkEvidence(0);
await work1.connect(account2).requestWork('req-content-id', { value: WORK1_PRICE });
await work1.submitWorkEvidence(0, 'evidence-content-id');
await expect(work1.submitWorkApproval(0, true))
.to.emit(dao, 'ValidationPoolInitiated').withArgs(1)
.to.emit(work1, 'WorkApprovalSubmitted').withArgs(0, true);
const post = await dao.posts(1);
expect(post.author).to.equal(account1);
expect(post.sender).to.equal(work1.target);
expect(post.contentId).to.equal('evidence-content-id');
const pool = await dao.validationPools(1);
expect(pool.fee).to.equal(WORK1_PRICE);
expect(pool.sender).to.equal(work1.target);
@ -244,29 +247,29 @@ describe('Work1', () => {
});
it('should be able to submit work disapproval', async () => {
await work1.connect(account2).requestWork({ value: WORK1_PRICE });
await work1.submitWorkEvidence(0);
await work1.connect(account2).requestWork('req-content-id', { value: WORK1_PRICE });
await work1.submitWorkEvidence(0, 'evidence-content-id');
await expect(work1.submitWorkApproval(0, false))
.to.emit(dao, 'ValidationPoolInitiated').withArgs(1)
.to.emit(work1, 'WorkApprovalSubmitted').withArgs(0, false);
});
it('should not be able to submit work approval/disapproval twice', async () => {
await work1.connect(account2).requestWork({ value: WORK1_PRICE });
await work1.submitWorkEvidence(0);
await work1.connect(account2).requestWork('req-content-id', { value: WORK1_PRICE });
await work1.submitWorkEvidence(0, 'evidence-content-id');
await expect(work1.submitWorkApproval(0, true)).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
await expect(work1.submitWorkApproval(0, true)).to.be.revertedWith('Status must be EvidenceSubmitted');
});
it('should not be able to submit work evidence after work approval', async () => {
await work1.connect(account2).requestWork({ value: WORK1_PRICE });
await work1.submitWorkEvidence(0);
await work1.connect(account2).requestWork('req-content-id', { value: WORK1_PRICE });
await work1.submitWorkEvidence(0, 'evidence-content-id');
await expect(work1.submitWorkApproval(0, true)).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
await expect(work1.submitWorkEvidence(0)).to.be.revertedWith('Status must be Requested');
await expect(work1.submitWorkEvidence(0, 'evidence-content-id')).to.be.revertedWith('Status must be Requested');
});
it('should not be able to submit work approval/disapproval before work evidence', async () => {
await work1.connect(account2).requestWork({ value: WORK1_PRICE });
await work1.connect(account2).requestWork('req-content-id', { value: WORK1_PRICE });
await expect(work1.submitWorkApproval(0, true)).to.be.revertedWith('Status must be EvidenceSubmitted');
});
});