Implement price change proposal workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 41s
Details
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 41s
Details
This commit is contained in:
parent
a3e3ebb71f
commit
04c31e0b90
|
@ -11,9 +11,15 @@ const dataDir = process.env.DATA_DIR || 'data';
|
|||
|
||||
const db = new Level(`${dataDir}/forum`, { valueEncoding: 'json' });
|
||||
|
||||
const verifySignature = ({ author, content, signature }) => {
|
||||
const verifySignature = ({
|
||||
author, content, signature, embeddedData,
|
||||
}) => {
|
||||
let contentToVerify = content;
|
||||
if (embeddedData && Object.entries(embeddedData).length) {
|
||||
contentToVerify += `\n\n${JSON.stringify(embeddedData, null, 2)}`;
|
||||
}
|
||||
try {
|
||||
const account = recoverPersonalSignature({ data: content, signature });
|
||||
const account = recoverPersonalSignature({ data: contentToVerify, signature });
|
||||
if (account !== author) {
|
||||
console.log('error: author does not match signature');
|
||||
return false;
|
||||
|
@ -28,17 +34,26 @@ const verifySignature = ({ author, content, signature }) => {
|
|||
app.use(express.json());
|
||||
|
||||
app.post('/write', async (req, res) => {
|
||||
const { body: { author, content, signature } } = req;
|
||||
const {
|
||||
body: {
|
||||
author, content, signature, embeddedData,
|
||||
},
|
||||
} = req;
|
||||
// Check author signature
|
||||
if (!verifySignature({ author, content, signature })) {
|
||||
if (!verifySignature({
|
||||
author, content, signature, embeddedData,
|
||||
})) {
|
||||
res.status(403).end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute content hash
|
||||
const data = { author, content, signature };
|
||||
const data = {
|
||||
author, content, signature, embeddedData,
|
||||
};
|
||||
const hash = objectHash(data);
|
||||
console.log('write', hash);
|
||||
console.log(data);
|
||||
|
||||
// Store content
|
||||
db.put(hash, data);
|
||||
|
@ -61,6 +76,10 @@ app.get('/read/:hash', async (req, res) => {
|
|||
return;
|
||||
}
|
||||
|
||||
data.embeddedData = data.embeddedData || undefined;
|
||||
|
||||
console.log(data);
|
||||
|
||||
// Verify hash
|
||||
const derivedHash = objectHash(data);
|
||||
if (derivedHash !== hash) {
|
||||
|
|
|
@ -480,10 +480,21 @@ function App() {
|
|||
</Tab>
|
||||
<Tab eventKey="worker" title="Worker">
|
||||
{work1 && (
|
||||
<WorkContract workContract={work1} title="Work Contract 1" verb="Work" />
|
||||
<WorkContract
|
||||
workContract={work1}
|
||||
title="Work Contract 1"
|
||||
verb="Work"
|
||||
showProposePriceChange
|
||||
/>
|
||||
)}
|
||||
{onboarding && (
|
||||
<WorkContract workContract={onboarding} title="Onboarding" verb="Onboarding" showRequestWork />
|
||||
<WorkContract
|
||||
workContract={onboarding}
|
||||
title="Onboarding"
|
||||
verb="Onboarding"
|
||||
showRequestWork
|
||||
showProposePriceChange
|
||||
/>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab eventKey="customer" title="Customer">
|
||||
|
|
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
|
@ -6,7 +6,9 @@ function ViewPostModal({
|
|||
show, setShow, title, post,
|
||||
}) {
|
||||
const handleClose = () => setShow(false);
|
||||
const { content, author } = post;
|
||||
const { content, author, embeddedData } = post;
|
||||
|
||||
const embeddedDataJson = JSON.stringify(embeddedData, null, 2);
|
||||
|
||||
return (
|
||||
<Modal show={show} onHide={handleClose}>
|
||||
|
@ -25,6 +27,11 @@ function ViewPostModal({
|
|||
<p className="post-content">
|
||||
{content}
|
||||
</p>
|
||||
{embeddedData && Object.entries(embeddedData).length && (
|
||||
<pre>
|
||||
{embeddedDataJson}
|
||||
</pre>
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import {
|
||||
useCallback, useContext, useEffect,
|
||||
} from 'react';
|
||||
import useList from '../../utils/List';
|
||||
import WorkContractContext from '../../contexts/WorkContractContext';
|
||||
|
||||
function PriceProposals() {
|
||||
const { workContract } = useContext(WorkContractContext);
|
||||
const [priceChangeProposals, dispatchPriceChangeProposal] = useList();
|
||||
const fetchPriceProposal = useCallback(async (index) => {
|
||||
const priceProposal = await workContract.methods.priceProposals(index).call();
|
||||
priceProposal.id = index;
|
||||
dispatchPriceChangeProposal({ type: 'update', item: priceProposal });
|
||||
}, [workContract, dispatchPriceChangeProposal]);
|
||||
|
||||
const fetchPriceProposals = useCallback(async () => {
|
||||
const count = await workContract.methods.priceProposalCount().call();
|
||||
const promises = [];
|
||||
dispatchPriceChangeProposal({ type: 'refresh' });
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
promises.push(fetchPriceProposal(i));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}, [workContract, fetchPriceProposal, dispatchPriceChangeProposal]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPriceProposals();
|
||||
// TODO: Event subscriptions/unsubscriptions
|
||||
}, [workContract, fetchPriceProposals]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>Price Change Proposals</h3>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Proposal</th>
|
||||
<th>Price</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{priceChangeProposals?.filter((x) => !!x).map((p) => (
|
||||
<tr key={p.id}>
|
||||
<td>{p.id.toString()}</td>
|
||||
<td>{p.proposalIndex.toString()}</td>
|
||||
<td>{p.price.toString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default PriceProposals;
|
|
@ -0,0 +1,102 @@
|
|||
import {
|
||||
useCallback, useContext, useEffect, useRef, useState,
|
||||
} from 'react';
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import Form from 'react-bootstrap/Form';
|
||||
import Modal from 'react-bootstrap/Modal';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Web3 from 'web3';
|
||||
import Web3Context from '../../contexts/Web3Context';
|
||||
import Post from '../../utils/Post';
|
||||
import ProposalsArtifact from '../../assets/Proposals.json';
|
||||
import { getContractAddressByChainId } from '../../utils/contract-config';
|
||||
import WorkContractContext from '../../contexts/WorkContractContext';
|
||||
|
||||
function ProposePriceChangeModal({
|
||||
show, setShow, title,
|
||||
}) {
|
||||
const {
|
||||
provider, account, chainId,
|
||||
} = useContext(Web3Context);
|
||||
const { workContract } = useContext(WorkContractContext);
|
||||
const [content, setContent] = useState('');
|
||||
const [proposedPrice, setProposedPrice] = useState();
|
||||
const proposalsContract = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
const web3 = new Web3(provider);
|
||||
const ProposalsAddress = getContractAddressByChainId(chainId, 'Proposals');
|
||||
const contract = new web3.eth.Contract(ProposalsArtifact.abi, ProposalsAddress);
|
||||
proposalsContract.current = contract;
|
||||
}, [provider, chainId]);
|
||||
|
||||
const handleClose = () => setShow(false);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
const post = new Post({ content });
|
||||
// Include price as embedded data
|
||||
post.embeddedData = { proposedPrice };
|
||||
// Include metamask signature
|
||||
await post.sign(provider, account);
|
||||
// Clear the input and hide the modal
|
||||
setContent('');
|
||||
setShow(false);
|
||||
// Write to API
|
||||
await post.write();
|
||||
// Publish to blockchain -- For now, Proposals.propose() does this for us
|
||||
// await post.publish(DAO, account);
|
||||
// Use content hash when calling Proposals.propose
|
||||
// TODO: Make durations configurable
|
||||
await workContract.methods.proposeNewPrice(proposedPrice, post.hash, [300, 300, 300]).send({
|
||||
from: account,
|
||||
gas: 999999,
|
||||
value: 200,
|
||||
});
|
||||
}, [provider, workContract, account, content, setShow, proposedPrice]);
|
||||
|
||||
return (
|
||||
<Modal show={show} onHide={handleClose}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{title}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>New Price</Form.Label>
|
||||
<Form.Control type="text" onChange={(e) => setProposedPrice(e.target.value)} />
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Explanation</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
rows={3}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleSubmit}>
|
||||
Submit
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
ProposePriceChangeModal.propTypes = {
|
||||
show: PropTypes.bool.isRequired,
|
||||
setShow: PropTypes.func.isRequired,
|
||||
title: PropTypes.string,
|
||||
};
|
||||
|
||||
ProposePriceChangeModal.defaultProps = {
|
||||
title: 'Propose Price Change',
|
||||
};
|
||||
|
||||
export default ProposePriceChangeModal;
|
|
@ -1,9 +1,17 @@
|
|||
import { useMemo } from 'react';
|
||||
import {
|
||||
useCallback, useContext, useEffect, useMemo, useState,
|
||||
} from 'react';
|
||||
import { PropTypes } from 'prop-types';
|
||||
import Web3 from 'web3';
|
||||
import Button from 'react-bootstrap/Button';
|
||||
|
||||
import Web3Context from '../../contexts/Web3Context';
|
||||
import useList from '../../utils/List';
|
||||
import WorkContractContext from '../../contexts/WorkContractContext';
|
||||
import AvailabilityStakes from './AvailabilityStakes';
|
||||
import WorkRequests from './WorkRequests';
|
||||
import ProposePriceChangeModal from './ProposePriceChangeModal';
|
||||
import PriceProposals from './PriceProposals';
|
||||
|
||||
function WorkContract({
|
||||
workContract,
|
||||
|
@ -13,14 +21,49 @@ function WorkContract({
|
|||
title,
|
||||
verb,
|
||||
showRequestWork,
|
||||
showProposePriceChange,
|
||||
}) {
|
||||
const [availabilityStakes, dispatchAvailabilityStake] = useList();
|
||||
const [priceWei, setPriceWei] = useState();
|
||||
const [priceEth, setPriceEth] = useState();
|
||||
const { provider } = useContext(Web3Context);
|
||||
const [showPriceChangeModal, setShowPriceChangeModal] = useState(false);
|
||||
|
||||
const fetchPrice = useCallback(async () => {
|
||||
const web3 = new Web3(provider);
|
||||
const fetchedPrice = await workContract.methods.price().call();
|
||||
setPriceWei(fetchedPrice);
|
||||
setPriceEth(web3.utils.fromWei(fetchedPrice, 'ether'));
|
||||
// TODO: Subscribe to price update event
|
||||
// TODO: Unsubscribe
|
||||
}, [workContract, provider]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPrice();
|
||||
}, [workContract, provider, fetchPrice]);
|
||||
|
||||
const workContractProviderValue = useMemo(() => ({
|
||||
workContract, availabilityStakes, dispatchAvailabilityStake,
|
||||
}), [workContract, availabilityStakes, dispatchAvailabilityStake]);
|
||||
workContract, availabilityStakes, dispatchAvailabilityStake, priceWei, priceEth,
|
||||
}), [workContract, availabilityStakes, dispatchAvailabilityStake, priceWei, priceEth]);
|
||||
|
||||
return (
|
||||
<WorkContractContext.Provider value={workContractProviderValue}>
|
||||
{showProposePriceChange && (
|
||||
<ProposePriceChangeModal
|
||||
show={showPriceChangeModal}
|
||||
setShow={setShowPriceChangeModal}
|
||||
/>
|
||||
)}
|
||||
<h2>{title}</h2>
|
||||
<h3>{`Price: ${priceEth} ETH`}</h3>
|
||||
{showProposePriceChange && (
|
||||
<>
|
||||
<Button onClick={() => setShowPriceChangeModal(true)}>
|
||||
Propose New Price
|
||||
</Button>
|
||||
<PriceProposals />
|
||||
</>
|
||||
)}
|
||||
<AvailabilityStakes
|
||||
showActions={showAvailabilityActions}
|
||||
showAmount={showAvailabilityAmount}
|
||||
|
@ -39,6 +82,7 @@ WorkContract.propTypes = {
|
|||
showAvailabilityActions: PropTypes.bool,
|
||||
showAvailabilityAmount: PropTypes.bool,
|
||||
onlyShowAvailable: PropTypes.bool,
|
||||
showProposePriceChange: PropTypes.bool,
|
||||
};
|
||||
|
||||
WorkContract.defaultProps = {
|
||||
|
@ -46,6 +90,7 @@ WorkContract.defaultProps = {
|
|||
showAvailabilityActions: true,
|
||||
showAvailabilityAmount: true,
|
||||
onlyShowAvailable: false,
|
||||
showProposePriceChange: false,
|
||||
};
|
||||
|
||||
export default WorkContract;
|
||||
|
|
|
@ -30,9 +30,8 @@ const getRequestStatus = (request) => {
|
|||
function WorkRequests({
|
||||
showRequestWork, verb,
|
||||
}) {
|
||||
const { workContract, availabilityStakes } = useContext(WorkContractContext);
|
||||
const { workContract, availabilityStakes, priceWei } = useContext(WorkContractContext);
|
||||
const [workRequests, dispatchWorkRequest] = useList();
|
||||
const [price, setPrice] = useState();
|
||||
const [showRequestModal, setShowRequestModal] = useState(false);
|
||||
const [showEvidenceModal, setShowEvidenceModal] = useState(false);
|
||||
const [currentRequestId, setCurrentRequestId] = useState();
|
||||
|
@ -43,12 +42,6 @@ function WorkRequests({
|
|||
provider, account,
|
||||
} = useContext(Web3Context);
|
||||
|
||||
const fetchPrice = useCallback(async () => {
|
||||
const web3 = new Web3(provider);
|
||||
const fetchedPrice = await workContract.methods.price().call();
|
||||
setPrice(web3.utils.fromWei(fetchedPrice, 'ether'));
|
||||
}, [workContract, provider]);
|
||||
|
||||
const fetchWorkRequest = useCallback(async (requestIndex) => {
|
||||
const web3 = new Web3(provider);
|
||||
const r = await workContract.methods.requests(requestIndex).call();
|
||||
|
@ -73,7 +66,6 @@ function WorkRequests({
|
|||
}, [workContract, dispatchWorkRequest, fetchWorkRequest]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPrice();
|
||||
fetchWorkRequests();
|
||||
|
||||
workContract.events.WorkAssigned({ fromBlock: 'latest' }).on('data', (event) => {
|
||||
|
@ -90,7 +82,7 @@ function WorkRequests({
|
|||
console.log('event: work approval submitted', event);
|
||||
fetchWorkRequest(event.returnValues.requestIndex);
|
||||
});
|
||||
}, [workContract, fetchWorkRequests, fetchPrice, fetchWorkRequest]);
|
||||
}, [workContract, fetchWorkRequests, fetchWorkRequest]);
|
||||
|
||||
const submitWorkApproval = useCallback(async (requestIndex) => {
|
||||
await workContract.methods.submitWorkApproval(requestIndex, true).send({
|
||||
|
@ -116,14 +108,12 @@ function WorkRequests({
|
|||
};
|
||||
|
||||
const onSubmitRequest = useCallback(async ({ hash }) => {
|
||||
const web3 = new Web3(provider);
|
||||
const priceWei = BigInt(web3.utils.toWei(price, 'ether'));
|
||||
await workContract.methods.requestWork(hash).send({
|
||||
from: account,
|
||||
gas: 1000000,
|
||||
value: priceWei,
|
||||
});
|
||||
}, [provider, workContract, account, price]);
|
||||
}, [workContract, account, priceWei]);
|
||||
|
||||
const onSubmitEvidence = useCallback(async ({ hash }) => {
|
||||
await workContract.methods.submitWorkEvidence(currentRequestId, hash).send({
|
||||
|
@ -143,9 +133,6 @@ function WorkRequests({
|
|||
<AddPostModal title={`${verb} Request`} show={showRequestModal} setShow={setShowRequestModal} onSubmit={onSubmitRequest} />
|
||||
<AddPostModal title="Work Evidence" show={showEvidenceModal} setShow={setShowEvidenceModal} onSubmit={onSubmitEvidence} />
|
||||
<ViewPostModal title={`${verb} Request`} show={showViewRequestModal} setShow={setShowViewRequestModal} post={viewRequest} />
|
||||
<div>
|
||||
{`Price: ${price} ETH`}
|
||||
</div>
|
||||
{showRequestWork && (
|
||||
<div>
|
||||
<Button onClick={handleShowWorkRequest}>{`Request ${verb}`}</Button>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"localhost": {
|
||||
"DAO": "0x84A5F75A39e25bD39B69F7d096d159557EaF2a59",
|
||||
"Work1": "0xaB3Bf8f9BE69289B0dd2a154a6390c8D9F780c59",
|
||||
"Onboarding": "0xf10E261AFF9Aa8b05716002bFf44D3e990401C02",
|
||||
"Proposals": "0xBD616B6331e0953Fc20281a54A684E614D8C4026"
|
||||
"DAO": "0x358A07B26F4c556140872ecdB69c58e8807E7178",
|
||||
"Work1": "0xC62b0b16B3ef06c417BFC4Fb02E0Da06aF5A95Ef",
|
||||
"Onboarding": "0x91B8D37F396cfb887996119CD37a0886C78a7B9C",
|
||||
"Proposals": "0x63472674239ffb70618Fae043610917f2d9B781C"
|
||||
},
|
||||
"sepolia": {
|
||||
"DAO": "0x58c8ea0ba031431423cD84787d7d57F0Bf7c6E63",
|
||||
|
|
|
@ -9,37 +9,59 @@ window.Buffer = Buffer;
|
|||
|
||||
class Post {
|
||||
constructor({
|
||||
author, content, signature, hash,
|
||||
author, content, signature, hash, embeddedData,
|
||||
}) {
|
||||
this.author = author;
|
||||
this.content = content;
|
||||
this.signature = signature;
|
||||
this.hash = hash;
|
||||
this.embeddedData = embeddedData;
|
||||
}
|
||||
|
||||
// Read from API
|
||||
static async read(hash) {
|
||||
const { data: { content, author, signature } } = await axios.get(`/api/read/${hash}`);
|
||||
const {
|
||||
data: {
|
||||
content, author, signature, embeddedData,
|
||||
},
|
||||
} = await axios.get(`/api/read/${hash}`);
|
||||
// Verify hash
|
||||
const derivedHash = objectHash({ author, content, signature });
|
||||
const derivedHash = objectHash({
|
||||
author, content, signature, embeddedData,
|
||||
});
|
||||
if (hash !== derivedHash) {
|
||||
throw new Error('Hash mismatch');
|
||||
}
|
||||
// Verify signature
|
||||
const recovered = recoverPersonalSignature({ data: content, signature });
|
||||
let contentToVerify = content;
|
||||
if (embeddedData && Object.entries(embeddedData).length) {
|
||||
contentToVerify += `\n\n${JSON.stringify(embeddedData, null, 2)}`;
|
||||
}
|
||||
const recovered = recoverPersonalSignature({ data: contentToVerify, signature });
|
||||
|
||||
if (recovered !== author) {
|
||||
throw new Error('Author mismatch');
|
||||
}
|
||||
return new Post({
|
||||
content, author, signature, hash,
|
||||
content, author, signature, hash, embeddedData,
|
||||
});
|
||||
}
|
||||
|
||||
static deriveEmbeddedData(content) {
|
||||
const dataStart = content.search(/^\{/);
|
||||
const dataStr = content.substring(dataStart);
|
||||
const embeddedData = JSON.parse(dataStr);
|
||||
return embeddedData;
|
||||
}
|
||||
|
||||
// Include MetaMask signature
|
||||
async sign(web3Provider, account) {
|
||||
this.author = account;
|
||||
const msg = `0x${Buffer.from(this.content, 'utf8').toString('hex')}`;
|
||||
let contentToSign = this.content;
|
||||
if (this.embeddedData && Object.entries(this.embeddedData).length) {
|
||||
contentToSign += `\n\n${JSON.stringify(this.embeddedData, null, 2)}`;
|
||||
}
|
||||
const msg = `0x${Buffer.from(contentToSign, 'utf8').toString('hex')}`;
|
||||
this.signature = await web3Provider.request({
|
||||
method: 'personal_sign',
|
||||
params: [msg, account],
|
||||
|
@ -53,6 +75,7 @@ class Post {
|
|||
author: this.author,
|
||||
content: this.content,
|
||||
signature: this.signature,
|
||||
embeddedData: this.embeddedData,
|
||||
};
|
||||
const { data: hash } = await axios.post('/api/write', data);
|
||||
this.hash = hash;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"localhost": {
|
||||
"DAO": "0x84A5F75A39e25bD39B69F7d096d159557EaF2a59",
|
||||
"Work1": "0xaB3Bf8f9BE69289B0dd2a154a6390c8D9F780c59",
|
||||
"Onboarding": "0xf10E261AFF9Aa8b05716002bFf44D3e990401C02",
|
||||
"Proposals": "0xBD616B6331e0953Fc20281a54A684E614D8C4026"
|
||||
"DAO": "0x358A07B26F4c556140872ecdB69c58e8807E7178",
|
||||
"Work1": "0xC62b0b16B3ef06c417BFC4Fb02E0Da06aF5A95Ef",
|
||||
"Onboarding": "0x91B8D37F396cfb887996119CD37a0886C78a7B9C",
|
||||
"Proposals": "0x63472674239ffb70618Fae043610917f2d9B781C"
|
||||
},
|
||||
"sepolia": {
|
||||
"DAO": "0x58c8ea0ba031431423cD84787d7d57F0Bf7c6E63",
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
// SPDX-License-Identifier: Unlicense
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
interface IOnProposalAccepted {
|
||||
function onProposalAccepted(
|
||||
uint stakedFor,
|
||||
uint stakedAgainst,
|
||||
bytes calldata callbackData
|
||||
) external;
|
||||
}
|
|
@ -6,7 +6,11 @@ import "./WorkContract.sol";
|
|||
import "./IOnValidate.sol";
|
||||
|
||||
contract Onboarding is WorkContract, IOnValidate {
|
||||
constructor(DAO dao_, uint price_) WorkContract(dao_, price_) {}
|
||||
constructor(
|
||||
DAO dao_,
|
||||
Proposals proposals_,
|
||||
uint price_
|
||||
) WorkContract(dao_, proposals_, price_) {}
|
||||
|
||||
/// Accept work approval/disapproval from customer
|
||||
function submitWorkApproval(
|
||||
|
|
|
@ -68,6 +68,9 @@ contract Proposals is DAOContract, IOnValidate {
|
|||
bool callbackOnValidate,
|
||||
bytes calldata callbackData
|
||||
) external payable returns (uint proposalIndex) {
|
||||
// TODO: Consider taking author as a parameter,
|
||||
// or else accepting a postIndex instead of contentId,
|
||||
// or support post lookup by contentId
|
||||
uint postIndex = dao.addPost(msg.sender, contentId);
|
||||
proposalIndex = proposalCount++;
|
||||
Proposal storage proposal = proposals[proposalIndex];
|
||||
|
@ -92,6 +95,9 @@ contract Proposals is DAOContract, IOnValidate {
|
|||
pools[2] = proposal.referenda[2].pools;
|
||||
}
|
||||
|
||||
// TODO: function getProposals()
|
||||
// Enumerate timing so clients can render it
|
||||
|
||||
/// External function for reputation holders to attest toward a given proposal;
|
||||
/// This is non-binding and non-encumbering, so it does not transfer any reputation.
|
||||
function attest(uint proposalIndex, uint amount) external {
|
||||
|
|
|
@ -3,7 +3,12 @@ pragma solidity ^0.8.24;
|
|||
|
||||
import "./DAO.sol";
|
||||
import "./WorkContract.sol";
|
||||
import "./Proposals.sol";
|
||||
|
||||
contract Work1 is WorkContract {
|
||||
constructor(DAO dao_, uint price_) WorkContract(dao_, price_) {}
|
||||
constructor(
|
||||
DAO dao_,
|
||||
Proposals proposals_,
|
||||
uint price_
|
||||
) WorkContract(dao_, proposals_, price_) {}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,14 @@ pragma solidity ^0.8.24;
|
|||
|
||||
import "./DAO.sol";
|
||||
import "./IAcceptAvailability.sol";
|
||||
import "./Proposals.sol";
|
||||
import "./IOnProposalAccepted.sol";
|
||||
|
||||
abstract contract WorkContract is DAOContract, IAcceptAvailability {
|
||||
abstract contract WorkContract is
|
||||
DAOContract,
|
||||
IAcceptAvailability,
|
||||
IOnProposalAccepted
|
||||
{
|
||||
struct AvailabilityStake {
|
||||
address worker;
|
||||
uint256 amount;
|
||||
|
@ -30,7 +36,15 @@ abstract contract WorkContract is DAOContract, IAcceptAvailability {
|
|||
bool approval;
|
||||
}
|
||||
|
||||
uint public immutable price;
|
||||
struct PriceProposal {
|
||||
uint price;
|
||||
uint proposalIndex;
|
||||
}
|
||||
|
||||
Proposals proposalsContract;
|
||||
uint public price;
|
||||
mapping(uint => PriceProposal) public priceProposals;
|
||||
uint public priceProposalCount;
|
||||
mapping(uint => AvailabilityStake) public stakes;
|
||||
uint public stakeCount;
|
||||
mapping(uint => WorkRequest) public requests;
|
||||
|
@ -43,8 +57,13 @@ abstract contract WorkContract is DAOContract, IAcceptAvailability {
|
|||
event WorkEvidenceSubmitted(uint requestIndex);
|
||||
event WorkApprovalSubmitted(uint requestIndex, bool approval);
|
||||
|
||||
constructor(DAO dao, uint price_) DAOContract(dao) {
|
||||
constructor(
|
||||
DAO dao,
|
||||
Proposals proposalsContract_,
|
||||
uint price_
|
||||
) DAOContract(dao) {
|
||||
price = price_;
|
||||
proposalsContract = proposalsContract_;
|
||||
}
|
||||
|
||||
/// Accept availability stakes as reputation token transfer
|
||||
|
@ -185,4 +204,39 @@ abstract contract WorkContract is DAOContract, IAcceptAvailability {
|
|||
);
|
||||
dao.stake(poolIndex, stake.amount, true);
|
||||
}
|
||||
|
||||
function proposeNewPrice(
|
||||
uint newPrice,
|
||||
string calldata contentId,
|
||||
uint[3] calldata durations
|
||||
) external payable {
|
||||
uint priceProposalIndex = priceProposalCount++;
|
||||
PriceProposal storage priceProposal = priceProposals[
|
||||
priceProposalIndex
|
||||
];
|
||||
priceProposal.price = newPrice;
|
||||
priceProposal.proposalIndex = proposalsContract.propose{
|
||||
value: msg.value
|
||||
}(
|
||||
contentId,
|
||||
durations[0],
|
||||
durations[1],
|
||||
durations[2],
|
||||
true,
|
||||
abi.encode(priceProposalIndex)
|
||||
);
|
||||
}
|
||||
|
||||
function onProposalAccepted(
|
||||
uint, // stakedFor
|
||||
uint, // stakedAgainst
|
||||
bytes calldata callbackData
|
||||
) external {
|
||||
uint priceProposalIndex = abi.decode(callbackData, (uint));
|
||||
PriceProposal storage priceProposal = priceProposals[
|
||||
priceProposalIndex
|
||||
];
|
||||
price = priceProposal.price;
|
||||
// TODO: Emit price change event
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@ const deployDAOContract = require('./util/deploy-dao-contract');
|
|||
|
||||
async function main() {
|
||||
await deployContract('DAO');
|
||||
await deployDAOContract('Proposals');
|
||||
await deployWorkContract('Work1');
|
||||
await deployWorkContract('Onboarding');
|
||||
await deployDAOContract('Proposals');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
|
|
@ -11,7 +11,10 @@ const deployWorkContract = async (name) => {
|
|||
const priceEnvVar = `${name.toUpperCase()}_PRICE`;
|
||||
const price = ethers.parseEther(process.env[priceEnvVar] || 0.001);
|
||||
|
||||
await deployContract(name, [contractAddresses[network].DAO, price]);
|
||||
await deployContract(name, [
|
||||
contractAddresses[network].DAO,
|
||||
contractAddresses[network].Proposals,
|
||||
price]);
|
||||
};
|
||||
|
||||
module.exports = deployWorkContract;
|
||||
|
|
|
@ -14,8 +14,10 @@ describe('Work1', () => {
|
|||
|
||||
const DAO = await ethers.getContractFactory('DAO');
|
||||
const dao = await DAO.deploy();
|
||||
const Proposals = await ethers.getContractFactory('Proposals');
|
||||
const proposals = await Proposals.deploy(dao.target);
|
||||
const Work1 = await ethers.getContractFactory('Work1');
|
||||
const work1 = await Work1.deploy(dao.target, WORK1_PRICE);
|
||||
const work1 = await Work1.deploy(dao.target, proposals.target, WORK1_PRICE);
|
||||
|
||||
await dao.addPost(account1, 'some-content-id');
|
||||
const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
|
||||
|
@ -34,7 +36,7 @@ describe('Work1', () => {
|
|||
await dao.evaluateOutcome(0);
|
||||
|
||||
return {
|
||||
dao, work1, account1, account2,
|
||||
dao, work1, proposals, account1, account2,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -283,4 +285,15 @@ describe('Work1', () => {
|
|||
await expect(work1.submitWorkApproval(0, true)).to.be.revertedWith('Status must be EvidenceSubmitted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Propose new price', () => {
|
||||
it('can propose a new price', async () => {
|
||||
const {
|
||||
proposals, work1,
|
||||
} = await loadFixture(deploy);
|
||||
expect(await proposals.proposalCount()).to.equal(0);
|
||||
await work1.proposeNewPrice(12345, 'content-id', [1, 1, 1]);
|
||||
expect(await proposals.proposalCount()).to.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue