Compare commits
No commits in common. "main" and "Virtual-branch" have entirely different histories.
main
...
Virtual-br
|
@ -0,0 +1,19 @@
|
|||
name: Gitea Actions Demo
|
||||
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
Explore-Gitea-Actions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
|
||||
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
|
||||
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
|
||||
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
||||
- name: List files in the repository
|
||||
run: |
|
||||
ls ${{ gitea.workspace }}
|
||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
104
README.md
104
README.md
|
@ -1,110 +1,6 @@
|
|||
# DGF Prototype
|
||||
Decentralized Governance Framework
|
||||
|
||||
* [Specification](https://spec.dgov.io)
|
||||
|
||||
* [Demo](https://demo.dgov.io)
|
||||
|
||||
* [Wiki](https://daogovernanceframework.com/wiki/DAO_Governance_Framework)
|
||||
|
||||
## Project Architecture
|
||||
|
||||
| directory | description |
|
||||
| --------- | ----------- |
|
||||
| ethereum | Solidity smart contracts and associated deploy scripts |
|
||||
| backend | Node.js application with an HTTP API that also functions as a Matrix bot and Ethereum client |
|
||||
| frontend | React.js frontend with a WebApp route and a Matrix Widget route |
|
||||
|
||||
### Data Flow Diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Blockchain <-- ethers --> API
|
||||
Blockchain <-- Web3<br>+ MetaMask --> WebApp
|
||||
Blockchain <-- Web3<br>+ MetaMask --> Widget
|
||||
WebApp <-- HTTPS --> API
|
||||
Widget <-- HTTPS --> API
|
||||
Widget <-- matrix-widget-api --> Matrix
|
||||
API <-- matrix-bot-sdk --> Matrix
|
||||
```
|
||||
|
||||
## Rollup
|
||||
|
||||
Instead of calling `DAO.initiateValidationPool()`, a contract can call `Rollup.addItem()`.
|
||||
|
||||
We demonstrate this by extending our base `Work` contract as `RollableWork`.
|
||||
|
||||
Our work contract normally triggeres a validation pool when the customer submits work approval. Instead, the fee and worker availability stakes are transferred to the `Rollup` contract.
|
||||
|
||||
The `Rollup` contract itself uses the `Availability` contract to assign a batch worker. This worker is responsible for making sure off-chain pools are conducted.
|
||||
|
||||
When ready, the worker submits the current batch on-chain by calling `DAO.addPost()` to create a batch post and then calling `Rollup.submitBatch()`, which initiates a validation pool targeting the batch post.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant client as Staking client
|
||||
participant matrix as Matrix room
|
||||
box Blockchain
|
||||
participant worker as Worker
|
||||
participant customer as Customer
|
||||
participant work as Work contract
|
||||
participant rollup as Rollup contract
|
||||
participant vp as Validation pool
|
||||
participant forum as Forum
|
||||
end
|
||||
|
||||
|
||||
worker ->> work : Availability stake<br />(REP)
|
||||
activate worker
|
||||
activate work
|
||||
customer ->> work : Request work<br />(fee)
|
||||
activate customer
|
||||
worker ->> work : Submit work evidence<br />(postId)
|
||||
deactivate worker
|
||||
customer ->> work : Submit work approval
|
||||
deactivate customer
|
||||
work ->> rollup : Add item<br />(fee, REP, postId)
|
||||
activate rollup
|
||||
deactivate work
|
||||
|
||||
rollup ->> client : Event: BatchItemAdded
|
||||
activate client
|
||||
client ->> matrix : io.dgov.pool.start<br />(postId)
|
||||
activate matrix
|
||||
matrix -->> client :
|
||||
client ->> matrix : io.dgov.pool.stake<br />(postId, REP, inFavor)
|
||||
matrix -->> client :
|
||||
client ->> matrix : io.dgov.pool.result<br />(postId, votePasses, quorumMet)
|
||||
matrix -->> client :
|
||||
|
||||
note right of client : All staking clients<br/>record each other's stakes
|
||||
|
||||
client ->> forum : Add post<br />(batchPostId)
|
||||
activate forum
|
||||
client ->> rollup : Submit batch<br />(batchPostId)
|
||||
client ->> matrix : io.dgov.rollup.submit
|
||||
matrix -->> client :
|
||||
deactivate matrix
|
||||
rollup ->> vp : Initiate validation pool<br />(fee, REP, batchPostId)
|
||||
activate vp
|
||||
note right of vp : Mints REP in<br />proportion to fee
|
||||
deactivate rollup
|
||||
|
||||
vp ->> client : Event: ValidationPoolInitiated
|
||||
|
||||
note right of client : Each staking client <br />verifies the rollup post
|
||||
|
||||
client ->> vp : Stake for/against
|
||||
client ->> vp : Evaluate outcome
|
||||
|
||||
vp ->> client : REP rewards for policin
|
||||
deactivate client
|
||||
vp ->> forum : Minted REP
|
||||
deactivate vp
|
||||
forum ->> worker : REP rewards for batch post authors
|
||||
deactivate forum
|
||||
```
|
||||
|
||||
## Local development setup
|
||||
|
||||
Clone this repository to a directory on your machine
|
||||
|
|
|
@ -10,9 +10,4 @@ MATRIX_PASSWORD=
|
|||
MATRIX_ACCESS_TOKEN=
|
||||
BOT_STORAGE_PATH="./data/bot-storage.json"
|
||||
BOT_CRYPTO_STORAGE_PATH="./data/bot-crypto"
|
||||
BOT_INSTANCE_ID=
|
||||
ENABLE_API=
|
||||
ENABLE_MATRIX=
|
||||
ENABLE_STAKING=
|
||||
START_PROPOSAL_ID=
|
||||
STOP_PROPOSAL_ID=
|
||||
BOT_INSTANCE_ID=
|
|
@ -1,6 +1,6 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: { es2020: true, mocha: true },
|
||||
env: { es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'airbnb',
|
||||
|
@ -11,7 +11,7 @@ module.exports = {
|
|||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{
|
||||
devDependencies: ['**/*.test.js'],
|
||||
devDependencies: false,
|
||||
optionalDependencies: false,
|
||||
peerDependencies: false,
|
||||
},
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
module.exports = {
|
||||
spec: ['src/**/*.test.js'],
|
||||
};
|
|
@ -1,24 +1,14 @@
|
|||
{
|
||||
"localhost": {
|
||||
"DAO": "0x3734B0944ea37694E85AEF60D5b256d19EDA04be",
|
||||
"Work1": "0x8BDA04936887cF11263B87185E4D19e8158c6296",
|
||||
"Onboarding": "0x8688E736D0D72161db4D25f68EF7d0EE4856ba19",
|
||||
"Proposals": "0x3287061aDCeE36C1aae420a06E4a5EaE865Fe3ce",
|
||||
"Rollup": "0x71cb20D63576a0Fa4F620a2E96C73F82848B09e1",
|
||||
"Work2": "0x76Dfe9F47f06112a1b78960bf37d87CfbB6D6133",
|
||||
"Reputation": "0xEAefe601Aad7422307B99be65bbE005aeA966012",
|
||||
"Forum": "0x79e365342329560e8420d7a0f016633d7640cB18",
|
||||
"Bench": "0xC0f00E5915F9abE6476858fD1961EAf79395ea64"
|
||||
"DAO": "0x8d914D38dD301FC4606f5aa9fEcF8A76389020d3",
|
||||
"Work1": "0xfe58B9EB03F75A603de1B286584f5E9532ab8fB5",
|
||||
"Onboarding": "0x1d63FDe5B461106729fE1e5e38A02fc68C518Af5",
|
||||
"Proposals": "0x050C420Cc4995B41217Eba1B54B82Fd5687e9139"
|
||||
},
|
||||
"sepolia": {
|
||||
"DAO": "0xBA2e65ae29667E145343bD5Fd655A72dcf873b08",
|
||||
"Work1": "0x251dB891768ea85DaCA6bb567669F97248D09Fe3",
|
||||
"Onboarding": "0x78FC8b520001560A9D7a61072855218320C71BDC",
|
||||
"Proposals": "0xA888cDC4Bd80d402b14B1FeDE5FF471F1737570c",
|
||||
"Reputation": "0x62cc0035B17F1686cE30320B90373c77fcaA58CD",
|
||||
"Forum": "0x51b5Af12707e0d879B985Cb0216bFAC6dca85501",
|
||||
"Bench": "0x98d9F0e97Af71936747819040ddBE896A548ef4d",
|
||||
"Rollup": "0x678DC2c846bfDCC813ea27DfEE428f1d7f2521ED",
|
||||
"Work2": "0x609102Fb6cA15da80D37E8cA68aBD5e1bD9C855B"
|
||||
"DAO": "0x8e5bd58B2ca8910C5F9be8de847d6883B15c60d2",
|
||||
"Work1": "0x1708A144F284C1a9615C25b674E4a08992CE93e4",
|
||||
"Onboarding": "0xb21D4c986715A1adb5e87F752842613648C20a7B",
|
||||
"Proposals": "0x930c47293F206780E8F166338bDaFF3520306032"
|
||||
}
|
||||
}
|
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
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
|
@ -18,21 +18,16 @@
|
|||
"express-async-errors": "^3.1.1",
|
||||
"fastq": "^1.17.1",
|
||||
"level": "^8.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"matrix-bot-sdk": "^0.7.1",
|
||||
"object-hash": "^3.0.0",
|
||||
"uuid": "^9.0.1"
|
||||
"object-hash": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"assert": "^2.1.0",
|
||||
"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",
|
||||
"mocha": "^10.4.0",
|
||||
"proxyquire": "^2.1.3"
|
||||
"eslint-plugin-react-hooks": "^4.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
|
@ -615,15 +610,6 @@
|
|||
"resolved": "https://registry.npmjs.org/another-json/-/another-json-0.2.0.tgz",
|
||||
"integrity": "sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg=="
|
||||
},
|
||||
"node_modules/ansi-colors": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
|
||||
"integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
|
@ -647,19 +633,6 @@
|
|||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
|
@ -832,19 +805,6 @@
|
|||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/assert": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
|
||||
"integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"is-nan": "^1.3.2",
|
||||
"object-is": "^1.1.5",
|
||||
"object.assign": "^4.1.4",
|
||||
"util": "^0.12.5"
|
||||
}
|
||||
},
|
||||
"node_modules/assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
|
@ -988,18 +948,6 @@
|
|||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bluebird": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||
|
@ -1051,18 +999,6 @@
|
|||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/browser-level": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/browser-level/-/browser-level-1.0.1.tgz",
|
||||
|
@ -1074,12 +1010,6 @@
|
|||
"run-parallel-limit": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/browser-stdout": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
|
||||
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
|
@ -1138,18 +1068,6 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
||||
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/caseless": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||
|
@ -1178,45 +1096,6 @@
|
|||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar/node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/classic-level": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/classic-level/-/classic-level-1.4.1.tgz",
|
||||
|
@ -1233,17 +1112,6 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
|
||||
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
@ -1378,18 +1246,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
|
||||
"integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
|
@ -1471,15 +1327,6 @@
|
|||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
|
||||
"integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||
|
@ -1739,15 +1586,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
|
@ -2324,31 +2162,6 @@
|
|||
"node": "^10.12.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-keys": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz",
|
||||
"integrity": "sha512-tcgI872xXjwFF4xgQmLxi76GnwJG3g/3isB1l4/G5Z4zrbddGpBjqZCO9oEAcB5wX0Hj/5iQB3toxfO7in1hHA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-object": "~1.0.1",
|
||||
"merge-descriptors": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||
|
@ -2395,15 +2208,6 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/flat": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
|
||||
"integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"flat": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/flat-cache": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
|
||||
|
@ -2495,20 +2299,6 @@
|
|||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
|
@ -2544,15 +2334,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||
|
@ -2791,15 +2572,6 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/html-to-text": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
|
||||
|
@ -2980,22 +2752,6 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arguments": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
|
||||
"integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"has-tostringtag": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-array-buffer": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
|
||||
|
@ -3039,18 +2795,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-boolean-object": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
|
||||
|
@ -3149,15 +2893,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-generator-function": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
|
||||
|
@ -3194,22 +2929,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-nan": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
|
||||
"integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.0",
|
||||
"define-properties": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-negative-zero": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
|
||||
|
@ -3222,15 +2941,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number-object": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
|
||||
|
@ -3246,15 +2956,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-object": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz",
|
||||
"integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-path-inside": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
|
||||
|
@ -3264,15 +2965,6 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-obj": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
|
||||
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-object": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||
|
@ -3376,18 +3068,6 @@
|
|||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
|
||||
},
|
||||
"node_modules/is-unicode-supported": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
||||
"integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-weakmap": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz",
|
||||
|
@ -3654,22 +3334,6 @@
|
|||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/log-symbols": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
||||
"integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
"is-unicode-supported": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
|
@ -3840,102 +3504,6 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.4.0.tgz",
|
||||
"integrity": "sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-colors": "4.1.1",
|
||||
"browser-stdout": "1.3.1",
|
||||
"chokidar": "3.5.3",
|
||||
"debug": "4.3.4",
|
||||
"diff": "5.0.0",
|
||||
"escape-string-regexp": "4.0.0",
|
||||
"find-up": "5.0.0",
|
||||
"glob": "8.1.0",
|
||||
"he": "1.2.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"log-symbols": "4.1.0",
|
||||
"minimatch": "5.0.1",
|
||||
"ms": "2.1.3",
|
||||
"serialize-javascript": "6.0.0",
|
||||
"strip-json-comments": "3.1.1",
|
||||
"supports-color": "8.1.1",
|
||||
"workerpool": "6.2.1",
|
||||
"yargs": "16.2.0",
|
||||
"yargs-parser": "20.2.4",
|
||||
"yargs-unparser": "2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"_mocha": "bin/_mocha",
|
||||
"mocha": "bin/mocha.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/glob": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
|
||||
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^5.0.1",
|
||||
"once": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/minimatch": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
|
||||
"integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mocha/node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/module-error": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/module-error/-/module-error-1.0.2.tgz",
|
||||
|
@ -3944,12 +3512,6 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/module-not-found-error": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz",
|
||||
"integrity": "sha512-pEk4ECWQXV6z2zjhRZUongnLJNUeGQJ3w6OQ5ctGwD+i5o93qjRQUk2Rt6VdNeu3sEP0AB4LcfvdebpxBRVr4g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/morgan": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
|
||||
|
@ -4051,15 +3613,6 @@
|
|||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/oauth-sign": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
||||
|
@ -4093,22 +3646,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/object-is": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
|
||||
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.7",
|
||||
"define-properties": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/object-keys": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||
|
@ -4378,18 +3915,6 @@
|
|||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pify": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
|
||||
|
@ -4491,17 +4016,6 @@
|
|||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/proxyquire": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz",
|
||||
"integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-keys": "^1.0.2",
|
||||
"module-not-found-error": "^1.0.1",
|
||||
"resolve": "^1.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/psl": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
||||
|
@ -4548,15 +4062,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
|
@ -4585,18 +4090,6 @@
|
|||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect.getprototypeof": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.5.tgz",
|
||||
|
@ -4735,15 +4228,6 @@
|
|||
"uuid": "bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.8",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
||||
|
@ -4972,15 +4456,6 @@
|
|||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/serialize-javascript": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
|
||||
"integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
||||
|
@ -5130,26 +4605,6 @@
|
|||
"graceful-fs": "^4.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/string.prototype.matchall": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz",
|
||||
|
@ -5285,18 +4740,6 @@
|
|||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
|
@ -5495,19 +4938,6 @@
|
|||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util": {
|
||||
"version": "0.12.5",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"is-arguments": "^1.0.4",
|
||||
"is-generator-function": "^1.0.7",
|
||||
"is-typed-array": "^1.1.3",
|
||||
"which-typed-array": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
|
@ -5640,29 +5070,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/workerpool": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
|
||||
"integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
|
@ -5689,62 +5096,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "16.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
||||
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cliui": "^7.0.2",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.0",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^20.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "20.2.4",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
|
||||
"integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-unparser": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
|
||||
"integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"camelcase": "^6.0.0",
|
||||
"decamelize": "^4.0.0",
|
||||
"flat": "^5.0.2",
|
||||
"is-plain-obj": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"description": "",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"test": "mocha",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"login": "node scripts/matrix-login"
|
||||
},
|
||||
"author": "",
|
||||
|
@ -19,20 +19,15 @@
|
|||
"express-async-errors": "^3.1.1",
|
||||
"fastq": "^1.17.1",
|
||||
"level": "^8.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"matrix-bot-sdk": "^0.7.1",
|
||||
"object-hash": "^3.0.0",
|
||||
"uuid": "^9.0.1"
|
||||
"object-hash": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"assert": "^2.1.0",
|
||||
"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",
|
||||
"mocha": "^10.4.0",
|
||||
"proxyquire": "^2.1.3"
|
||||
"eslint-plugin-react-hooks": "^4.6.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,8 +13,8 @@ const {
|
|||
const login = async () => {
|
||||
console.log(`MATRIX_HOMESERVER_URL="${MATRIX_HOMESERVER_URL}"`);
|
||||
const auth = new MatrixAuth(MATRIX_HOMESERVER_URL);
|
||||
const matrixClient = await auth.passwordLogin(MATRIX_USER, MATRIX_PASSWORD);
|
||||
console.log(`MATRIX_ACCESS_TOKEN="${matrixClient.accessToken}"`);
|
||||
const client = await auth.passwordLogin(MATRIX_USER, MATRIX_PASSWORD);
|
||||
console.log(`MATRIX_ACCESS_TOKEN="${client.accessToken}"`);
|
||||
};
|
||||
|
||||
login();
|
||||
|
|
|
@ -1,13 +1,29 @@
|
|||
const { matrixClient } = require('../matrix-bot');
|
||||
const { getClient } = require('../matrix-bot');
|
||||
|
||||
const { matrixUserToAuthorAddress } = require('../util/db');
|
||||
const write = require('../util/forum/write');
|
||||
const { wallet } = require('../util/contracts');
|
||||
const addPostWithRetry = require('../util/add-post-with-retry');
|
||||
const { dao } = require('../util/contracts');
|
||||
|
||||
const {
|
||||
ETH_NETWORK,
|
||||
} = process.env;
|
||||
|
||||
const addPostWithRetry = async (authors, hash, citations, retryDelay = 5000) => {
|
||||
try {
|
||||
await dao.addPost(authors, hash, citations);
|
||||
} catch (e) {
|
||||
if (e.code === 'REPLACEMENT_UNDERPRICED') {
|
||||
console.log('retry delay (sec):', retryDelay / 1000);
|
||||
await Promise.delay(retryDelay);
|
||||
return addPostWithRetry(authors, hash, citations, retryDelay * 2);
|
||||
} if (e.reason === 'A post with this contentId already exists') {
|
||||
return { alreadyAdded: true };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return { alreadyAdded: false };
|
||||
};
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
const {
|
||||
body: {
|
||||
|
@ -28,7 +44,8 @@ module.exports = async (req, res) => {
|
|||
console.log('roomId', roomId);
|
||||
console.log('eventId', eventId);
|
||||
|
||||
const event = await matrixClient.getEvent(roomId, eventId);
|
||||
const client = getClient();
|
||||
const event = await client.getEvent(roomId, eventId);
|
||||
console.log('event', event);
|
||||
|
||||
let authorAddress;
|
||||
|
@ -41,32 +58,30 @@ module.exports = async (req, res) => {
|
|||
}
|
||||
|
||||
// We want to add a post representing this matrix message.
|
||||
// We can't sign it on behalf of the author.
|
||||
// That means we need to support posts without signatures.
|
||||
const authors = [{ authorAddress, weightPPM: 1000000 }];
|
||||
// TODO: Take references as input to this API call, referencing other posts or matrix events
|
||||
const references = [];
|
||||
// TODO: Take citations as input to this API call, referencing other posts or matrix events
|
||||
const citations = [];
|
||||
const content = `Matrix event URI: ${eventUri}`;
|
||||
const embeddedData = {
|
||||
roomId,
|
||||
eventId,
|
||||
};
|
||||
// We can't sign it on behalf of the author, but we can sign it with our own key
|
||||
const sender = await wallet.getAddress();
|
||||
const contentToVerify = `${content}\n\n${JSON.stringify(embeddedData, null, 2)}`;
|
||||
const signature = await wallet.signMessage(contentToVerify);
|
||||
|
||||
const { hash } = await write({
|
||||
sender, authors, references, content, embeddedData, signature,
|
||||
authors, citations, content, embeddedData,
|
||||
});
|
||||
|
||||
// Now we want to add a post on-chain
|
||||
const { alreadyAdded } = await addPostWithRetry(authors, hash, references);
|
||||
const { alreadyAdded } = await addPostWithRetry(authors, hash, citations);
|
||||
|
||||
if (alreadyAdded) {
|
||||
console.log(`Post already added for matrix event ${eventUri}`);
|
||||
} else {
|
||||
console.log(`Added post to blockchain for matrix event ${eventUri}`);
|
||||
// Send matrix event reply to the targeted event, notifying of this blockchain post
|
||||
await matrixClient.replyNotice(roomId, event, `Added to ${ETH_NETWORK} blockchain as post ${hash}`);
|
||||
await client.replyNotice(roomId, event, `Added to ${ETH_NETWORK} blockchain as post ${hash}`);
|
||||
}
|
||||
|
||||
res.json({ postId: hash, alreadyAdded });
|
||||
|
|
|
@ -8,8 +8,8 @@ const { authorAddresses, authorPrivKeys } = require('../util/db');
|
|||
const { dao } = require('../util/contracts');
|
||||
const write = require('../util/forum/write');
|
||||
|
||||
// Each post allocates 30% of its reputation to references
|
||||
const PPM_TO_REFERENCES = 300000;
|
||||
// Each post allocates 30% of its reputation to citations
|
||||
const PPM_TO_CITATIONS = 300000;
|
||||
|
||||
const fetchWithRetry = async (url, retryDelay = 5000) => {
|
||||
let retry = false;
|
||||
|
@ -72,8 +72,8 @@ const getOrCreateAuthors = async (paper) => Promise.mapSeries(
|
|||
// Generate and store a new account
|
||||
const id = crypto.randomBytes(32).toString('hex');
|
||||
authorPrivKey = `0x${id}`;
|
||||
const authorWallet = new ethers.Wallet(authorPrivKey);
|
||||
authorAddress = authorWallet.address;
|
||||
const wallet = new ethers.Wallet(authorPrivKey);
|
||||
authorAddress = wallet.address;
|
||||
await authorAddresses.put(authorId, authorAddress);
|
||||
await authorPrivKeys.put(authorAddress, authorPrivKey);
|
||||
}
|
||||
|
@ -90,9 +90,9 @@ const generatePost = async (paper) => {
|
|||
throw new Error('Paper has no authors with id');
|
||||
}
|
||||
const firstAuthorWallet = new ethers.Wallet(authorsInfo[0].authorPrivKey);
|
||||
const eachAuthorWeightPPM = Math.floor(1000000 / authorsInfo.length);
|
||||
const eachAuthorWeightPercent = Math.floor(1000000 / authorsInfo.length);
|
||||
const authors = authorsInfo.map(({ authorAddress }) => ({
|
||||
weightPPM: eachAuthorWeightPPM,
|
||||
weightPPM: eachAuthorWeightPercent,
|
||||
authorAddress,
|
||||
}));
|
||||
// Make sure author weights sum to 100%
|
||||
|
@ -116,15 +116,15 @@ HREF ${paper.url}`;
|
|||
};
|
||||
};
|
||||
|
||||
const addPostWithRetry = async (authors, hash, references, retryDelay = 5000) => {
|
||||
const addPostWithRetry = async (authors, hash, citations, retryDelay = 5000) => {
|
||||
try {
|
||||
await dao.addPost(authors, hash, references);
|
||||
await dao.addPost(authors, hash, citations);
|
||||
} catch (e) {
|
||||
if (e.code === 'REPLACEMENT_UNDERPRICED') {
|
||||
console.log('retry delay (sec):', retryDelay / 1000);
|
||||
await Promise.delay(retryDelay);
|
||||
return addPostWithRetry(authors, hash, references, retryDelay * 2);
|
||||
} if (e.reason === 'A post with this postId already exists') {
|
||||
return addPostWithRetry(authors, hash, citations, retryDelay * 2);
|
||||
} if (e.reason === 'A post with this contentId already exists') {
|
||||
return { alreadyAdded: true };
|
||||
}
|
||||
throw e;
|
||||
|
@ -135,19 +135,19 @@ const addPostWithRetry = async (authors, hash, references, retryDelay = 5000) =>
|
|||
const importPaper = async (paper) => {
|
||||
console.log('references count:', paper.references.length);
|
||||
const { paperId } = paper;
|
||||
const paperReferences = paper.references.filter((x) => !!x.paperId);
|
||||
const eachReferenceWeightPPM = Math.floor(PPM_TO_REFERENCES / paperReferences.length);
|
||||
const references = (await Promise.mapSeries(
|
||||
paperReferences,
|
||||
const references = paper.references.filter((x) => !!x.paperId);
|
||||
const eachCitationWeightPercent = Math.floor(PPM_TO_CITATIONS / references.length);
|
||||
const citations = (await Promise.mapSeries(
|
||||
references,
|
||||
async (citedPaper) => {
|
||||
// We need to fetch this paper so we can generate the post we WOULD add to the forum.
|
||||
// That way, if we later add the cited paper to the blockchain it will have the correct hash.
|
||||
// The forum allows dangling references to support this use case.
|
||||
// The forum allows dangling citations to support this use case.
|
||||
try {
|
||||
const citedPost = await generatePost(citedPaper);
|
||||
const citedPostHash = objectHash(citedPost);
|
||||
return {
|
||||
weightPPM: eachReferenceWeightPPM,
|
||||
weightPPM: eachCitationWeightPercent,
|
||||
targetPostId: citedPostHash,
|
||||
};
|
||||
} catch (e) {
|
||||
|
@ -156,10 +156,10 @@ const importPaper = async (paper) => {
|
|||
},
|
||||
)).filter((x) => !!x);
|
||||
|
||||
// Make sure reference weights sum to the designated total
|
||||
if (references.length) {
|
||||
const totalReferenceWeight = references.reduce((t, { weightPPM }) => t + weightPPM, 0);
|
||||
references[0].weightPPM += PPM_TO_REFERENCES - totalReferenceWeight;
|
||||
// Make sure citation weights sum to the designated total
|
||||
if (citations.length) {
|
||||
const totalCitationWeight = citations.reduce((t, { weightPPM }) => t + weightPPM, 0);
|
||||
citations[0].weightPPM += PPM_TO_CITATIONS - totalCitationWeight;
|
||||
}
|
||||
|
||||
// Create a post for this paper
|
||||
|
@ -169,12 +169,12 @@ const importPaper = async (paper) => {
|
|||
|
||||
// Write the new post to our database
|
||||
const { hash } = await write({
|
||||
authors, content, signature, embeddedData, references,
|
||||
authors, content, signature, embeddedData, citations,
|
||||
});
|
||||
|
||||
// Add the post to the forum (on-chain)
|
||||
console.log('addPostWithRetry', { authors, hash, references });
|
||||
const { alreadyAdded } = await addPostWithRetry(authors, hash, references);
|
||||
console.log('addPostWithRetry', { authors, hash, citations });
|
||||
const { alreadyAdded } = await addPostWithRetry(authors, hash, citations);
|
||||
if (alreadyAdded) {
|
||||
console.log(`Post already added for paper ${paperId}`);
|
||||
} else {
|
||||
|
@ -201,20 +201,11 @@ module.exports = async (req, res) => {
|
|||
console.log(`importFromSS: author ${authorId}`);
|
||||
const papers = await fetchAuthorPapers(authorId);
|
||||
console.log('papers count:', papers.length);
|
||||
|
||||
const earlyResponseTimeout = setTimeout(() => {
|
||||
res.status(202).end();
|
||||
});
|
||||
|
||||
const result = await Promise.mapSeries(papers, importPaper);
|
||||
clearTimeout(earlyResponseTimeout);
|
||||
|
||||
if (result.length) {
|
||||
console.log(`Added posts for ${result.length} papers by author ${authorId}`);
|
||||
}
|
||||
if (!res.headersSent) {
|
||||
res.json(result);
|
||||
}
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(400).end();
|
||||
}
|
||||
|
|
|
@ -13,8 +13,9 @@ const port = process.env.API_LISTEN_PORT || 3000;
|
|||
app.use(express.json());
|
||||
|
||||
app.post('/write', async (req, res) => {
|
||||
const { hash } = await write(req.body);
|
||||
const { hash, data } = await write(req.body);
|
||||
console.log('write', hash);
|
||||
console.log(data);
|
||||
res.send(hash);
|
||||
});
|
||||
|
||||
|
@ -38,9 +39,7 @@ app.use((err, req, res, next) => {
|
|||
const status = err.response?.status ?? err.status ?? 500;
|
||||
const message = err.response?.data?.error ?? err.message;
|
||||
console.error(`error: ${message}`, err);
|
||||
if (!res.headersSent) {
|
||||
res.status(status).send(message);
|
||||
}
|
||||
res.status(status).send(message);
|
||||
next();
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
const proposalsListener = require('./proposals');
|
||||
|
||||
const start = () => {
|
||||
proposalsListener.start();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
const { proposals } = require('../util/contracts');
|
||||
const read = require('../api/read');
|
||||
const { sendNewProposalEvent } = require('../matrix-bot/outbound-queue');
|
||||
|
||||
// Subscribe to proposal events
|
||||
const start = () => {
|
||||
console.log('registering proposal listener');
|
||||
proposals.on('NewProposal', async (proposalIndex) => {
|
||||
console.log('New Proposal, index', proposalIndex);
|
||||
|
||||
const proposal = await proposals.proposals(proposalIndex);
|
||||
console.log('postId:', proposal.postId);
|
||||
|
||||
// Read post from database
|
||||
try {
|
||||
const post = await read(proposal.postId);
|
||||
console.log('post.content:', post.content);
|
||||
|
||||
// Send matrix room event
|
||||
let message = `Proposal ${proposalIndex}\n\n${post.content}`;
|
||||
if (post.embeddedData && Object.entries(post.embeddedData).length) {
|
||||
message += `\n\n${JSON.stringify(post.embeddedData, null, 2)}`;
|
||||
}
|
||||
|
||||
// The outbound queue handles deduplication
|
||||
sendNewProposalEvent(proposalIndex, message);
|
||||
} catch (e) {
|
||||
// Post for proposal not found
|
||||
console.error(`error: post for proposal ${proposalIndex} not found`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
};
|
|
@ -1,100 +0,0 @@
|
|||
const { registerMatrixMessageHandler } = require('../matrix-bot');
|
||||
const { setTargetRoomId } = require('../matrix-bot/outbound-queue');
|
||||
const {
|
||||
appState,
|
||||
proposalEventIds,
|
||||
matrixPools,
|
||||
} = require('../util/db');
|
||||
const submitRollup = require('./rollup/submit-rollup');
|
||||
const { resetBatchItems } = require('./rollup/batch-items');
|
||||
const { initiateMatrixPools } = require('./rollup/matrix-pools/initiate-matrix-pools');
|
||||
const initiateMatrixPool = require('./rollup/matrix-pools/initiate');
|
||||
const read = require('../util/forum/read');
|
||||
|
||||
const {
|
||||
BOT_INSTANCE_ID,
|
||||
ETH_NETWORK,
|
||||
} = process.env;
|
||||
|
||||
// TODO: Refactor into separate files
|
||||
const handleCommand = async (client, roomId, event) => {
|
||||
// Don't handle unhelpful events (ones that aren't text messages, are redacted, or sent by us)
|
||||
if (event.content?.msgtype !== 'm.text') return;
|
||||
if (event.sender === await client.getUserId()) return;
|
||||
|
||||
const helloRegex = /^!hello\b/i;
|
||||
const targetRegex = /^!target (.*)\b/i;
|
||||
const proposalRegex = /\bprop(|osal) ([0-9]+)\b/i;
|
||||
const submitRollupRegex = /^!submitBatch\b/i;
|
||||
const resetBatchRegex = /^!resetBatch (.*)\b/i;
|
||||
const restartMatrixPoolRegex = /^!restartMatrixPool (.*)\b/i;
|
||||
|
||||
const { body } = event.content;
|
||||
|
||||
if (helloRegex.test(body)) {
|
||||
console.log(`!hello roomId ${roomId}`);
|
||||
await client.replyNotice(roomId, event, 'Hello world!');
|
||||
} else if (targetRegex.test(body)) {
|
||||
const [, instanceId] = targetRegex.exec(body);
|
||||
console.log(`!target roomId ${roomId} instanceId ${instanceId}`);
|
||||
if (instanceId === BOT_INSTANCE_ID) {
|
||||
setTargetRoomId(roomId);
|
||||
await appState.put('targetRoomId', roomId);
|
||||
await client.replyNotice(roomId, event, `Events will be sent to this room (${roomId}) for network ${ETH_NETWORK}`);
|
||||
}
|
||||
} else if (proposalRegex.test(body)) {
|
||||
const [, , proposalIndexStr] = proposalRegex.exec(body);
|
||||
const proposalIndex = parseInt(proposalIndexStr, 10);
|
||||
console.log(`mention of proposal ${proposalIndex} in roomId ${roomId}`);
|
||||
try {
|
||||
const proposalEventId = await proposalEventIds.get(proposalIndex);
|
||||
const proposalEventUri = `https://matrix.to/#/${roomId}/${proposalEventId}`;
|
||||
// TODO: Send HTML message
|
||||
const content = {
|
||||
body: `Proposal ${proposalIndex}: ${proposalEventUri}`,
|
||||
msgtype: 'm.text',
|
||||
};
|
||||
if (event.content['m.relates_to']?.rel_type === 'm.thread') {
|
||||
content['m.relates_to'] = event.content['m.relates_to'];
|
||||
}
|
||||
await client.sendEvent(roomId, 'm.room.message', content);
|
||||
} catch (e) {
|
||||
// Not found
|
||||
}
|
||||
} else if (submitRollupRegex.test(body)) {
|
||||
console.log('!submitBatch');
|
||||
const { batchPostId, batchItems, authors } = await submitRollup();
|
||||
if (batchItems.length) {
|
||||
await client.replyText(roomId, event, `Submitted batch, post ${batchPostId} with ${batchItems.length} posts by ${authors.length} authors`);
|
||||
} else {
|
||||
await client.replyText(roomId, event, 'No matrix pools have finished since the last batch was submitted');
|
||||
}
|
||||
} else if (resetBatchRegex.test(body)) {
|
||||
const [, instanceId] = resetBatchRegex.exec(body);
|
||||
console.log(`!resetBatch roomId ${roomId} instanceId ${instanceId}`);
|
||||
if (instanceId === BOT_INSTANCE_ID) {
|
||||
console.log('!resetBatch');
|
||||
const batchItems = await resetBatchItems();
|
||||
await initiateMatrixPools();
|
||||
await client.replyText(roomId, event, `Reset batch, now contains ${batchItems.length} items`);
|
||||
}
|
||||
} else if (restartMatrixPoolRegex.test(body)) {
|
||||
const [, postId] = restartMatrixPoolRegex.exec(body);
|
||||
console.log(`!restartMatrixPool roomId ${roomId} postId ${postId}`);
|
||||
try {
|
||||
const { sender, fee } = await matrixPools.get(postId);
|
||||
const post = await read(postId);
|
||||
await initiateMatrixPool(postId, post, sender, fee);
|
||||
} catch (e) {
|
||||
// Can't restart if it was never started
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const start = () => {
|
||||
registerMatrixMessageHandler(handleCommand);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
};
|
|
@ -1,19 +0,0 @@
|
|||
const proposalsNotifier = require('./proposals-notifier');
|
||||
const validationPools = require('./validation-pools');
|
||||
const work1 = require('./work1');
|
||||
const rollup = require('./rollup');
|
||||
const registerIdentity = require('./register-identity');
|
||||
const botCommands = require('./bot-commands');
|
||||
|
||||
const start = () => {
|
||||
proposalsNotifier.start();
|
||||
validationPools.start();
|
||||
work1.start();
|
||||
rollup.start();
|
||||
registerIdentity.start();
|
||||
botCommands.start();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
};
|
|
@ -1,54 +0,0 @@
|
|||
const { proposals } = require('../util/contracts');
|
||||
const read = require('../util/forum/read');
|
||||
const { sendMatrixText } = require('../matrix-bot/outbound-queue');
|
||||
const { proposalEventIds } = require('../util/db');
|
||||
|
||||
// Subscribe to proposal events
|
||||
const start = () => {
|
||||
console.log('registering proposal listener for proposal notifier');
|
||||
proposals.on('NewProposal', async (proposalIndex) => {
|
||||
console.log('New Proposal, index', proposalIndex);
|
||||
|
||||
const proposal = await proposals.proposals(proposalIndex);
|
||||
console.log('postId:', proposal.postId);
|
||||
|
||||
// Read post from database
|
||||
let post;
|
||||
try {
|
||||
post = await read(proposal.postId);
|
||||
} catch (e) {
|
||||
// Post for proposal not found
|
||||
console.error(`error: post for proposal ${proposalIndex} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('post.content:', post.content);
|
||||
|
||||
// Send matrix room event
|
||||
// TODO: Send HTML message
|
||||
let message = `Proposal ${proposalIndex}\n\n${post.content}`;
|
||||
if (post.embeddedData && Object.entries(post.embeddedData).length) {
|
||||
message += `\n\n${JSON.stringify(post.embeddedData, null, 2)}`;
|
||||
}
|
||||
|
||||
try {
|
||||
await proposalEventIds.get(Number(proposalIndex));
|
||||
// If this doesn't throw, it means we already sent a message for this proposal
|
||||
} catch (e) {
|
||||
if (e.status === 404) {
|
||||
console.log('sending new proposal event to room', { message });
|
||||
const { eventId } = await sendMatrixText(message);
|
||||
await proposalEventIds.put(Number(proposalIndex), eventId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
proposals.on('ProposalAccepted', async (proposalIndex) => {
|
||||
console.log('Proposal Accepted, index:', proposalIndex);
|
||||
// TODO: Send notification as a reply to the new proposal message
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
};
|
|
@ -1,39 +0,0 @@
|
|||
const { applicationData } = require('../../../util/db');
|
||||
|
||||
let batchItems;
|
||||
|
||||
const initializeBatchItems = async () => {
|
||||
try {
|
||||
batchItems = await applicationData.get('batchItems');
|
||||
} catch (e) {
|
||||
batchItems = [];
|
||||
}
|
||||
};
|
||||
|
||||
const getBatchItems = () => batchItems;
|
||||
|
||||
const addBatchItem = async (postId) => {
|
||||
if (!batchItems.includes(postId)) {
|
||||
batchItems.push(postId);
|
||||
await applicationData.put('batchItems', batchItems);
|
||||
}
|
||||
};
|
||||
|
||||
const clearBatchItems = async (itemsToClear) => {
|
||||
batchItems = batchItems.filter((item) => !itemsToClear.includes(item));
|
||||
await applicationData.put('batchItems', batchItems);
|
||||
};
|
||||
|
||||
const resetBatchItems = async () => {
|
||||
batchItems = [];
|
||||
await applicationData.put('batchItems', batchItems);
|
||||
return batchItems;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
initializeBatchItems,
|
||||
getBatchItems,
|
||||
addBatchItem,
|
||||
clearBatchItems,
|
||||
resetBatchItems,
|
||||
};
|
|
@ -1,34 +0,0 @@
|
|||
const { rollup, wallet } = require('../../../util/contracts');
|
||||
|
||||
let batchWorker;
|
||||
let batchStart;
|
||||
|
||||
const getCurrentBatchWorker = () => batchWorker;
|
||||
|
||||
const initializeBatchWorker = async () => {
|
||||
batchWorker = await rollup.batchWorker();
|
||||
|
||||
console.log('At startup, batch worker:', batchWorker);
|
||||
|
||||
rollup.on('BatchWorkerAssigned', async (batchWorker_) => {
|
||||
batchWorker = batchWorker_;
|
||||
batchStart = new Date();
|
||||
console.log('Batch worker assigned:', batchWorker);
|
||||
if (batchWorker === await wallet.getAddress()) {
|
||||
console.log('This instance is the new batch worker');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setBatchWorker = (batchWorker_) => {
|
||||
batchWorker = batchWorker_;
|
||||
};
|
||||
|
||||
const getBatchAge = (new Date() - batchStart) / 1000;
|
||||
|
||||
module.exports = {
|
||||
getCurrentBatchWorker,
|
||||
initializeBatchWorker,
|
||||
setBatchWorker,
|
||||
getBatchAge,
|
||||
};
|
|
@ -1,59 +0,0 @@
|
|||
const Promise = require('bluebird');
|
||||
const read = require('../../../util/forum/read');
|
||||
const { matrixPools } = require('../../../util/db');
|
||||
|
||||
const WEIGHT_TO_REFERENCES = 300000;
|
||||
|
||||
const computeBatchPost = async (batchItems_) => {
|
||||
const weights = {};
|
||||
let references = [];
|
||||
await Promise.each(batchItems_, async (postId) => {
|
||||
const post = await read(postId);
|
||||
const matrixPool = await matrixPools.get(postId);
|
||||
const { fee, result: { votePasses, quorumMet } } = matrixPool;
|
||||
if (votePasses && quorumMet) {
|
||||
post.authors.forEach(({ authorAddress, weightPPM }) => {
|
||||
weights[authorAddress] = weights[authorAddress] ?? 0;
|
||||
// scale by matrix pool fee
|
||||
weights[authorAddress] += weightPPM * fee;
|
||||
});
|
||||
post.references?.forEach(({ targetPostId, weightPPM }) => {
|
||||
// scale by matrix pool fee
|
||||
references.push({
|
||||
targetPostId,
|
||||
weightPPM: weightPPM * fee,
|
||||
});
|
||||
});
|
||||
}
|
||||
// TODO: Rewards for policing
|
||||
});
|
||||
|
||||
// Rescale author weights so they sum to 1000000
|
||||
const sumOfWeights = Object.values(weights).reduce((t, v) => t + v, 0);
|
||||
if (!sumOfWeights) {
|
||||
return [];
|
||||
}
|
||||
const scaledWeights = Object.values(weights)
|
||||
.map((weight) => Math.floor((weight * 1000000) / sumOfWeights));
|
||||
const sumOfScaledWeights = scaledWeights.reduce((t, v) => t + v, 0);
|
||||
scaledWeights[0] += 1000000 - sumOfScaledWeights;
|
||||
const authors = Object.keys(weights)
|
||||
.map((authorAddress, i) => ({ authorAddress, weightPPM: scaledWeights[i] }));
|
||||
|
||||
// Rescale reference weights so they sum to WEIGHT_TO_REFERENCES
|
||||
if (references.length) {
|
||||
const sumOfReferenceWeights = references.reduce((t, { weightPPM }) => t + weightPPM, 0);
|
||||
const scaledReferences = references.map((reference) => ({
|
||||
targetPostId: reference.targetPostId,
|
||||
weightPPM: Math.floor((reference.weightPPM * WEIGHT_TO_REFERENCES) / sumOfReferenceWeights),
|
||||
}));
|
||||
const sumOfScaledReferenceWeights = scaledReferences
|
||||
.reduce((t, { weightPPM }) => t + weightPPM, 0);
|
||||
scaledReferences[0].weightPPM += WEIGHT_TO_REFERENCES - sumOfScaledReferenceWeights;
|
||||
references = scaledReferences;
|
||||
}
|
||||
|
||||
return { authors, references };
|
||||
};
|
||||
|
||||
module.exports = computeBatchPost;
|
|
@ -1,128 +0,0 @@
|
|||
// const { expect } = require('chai');
|
||||
const assert = require('assert');
|
||||
const proxyquire = require('proxyquire');
|
||||
|
||||
let posts = {};
|
||||
let pools = {};
|
||||
const read = (postId) => posts[postId];
|
||||
const matrixPools = {
|
||||
get: (postId) => pools[postId],
|
||||
};
|
||||
|
||||
const computeBatchPost = proxyquire('./compute-batch-post', {
|
||||
'../../../util/forum/read': read,
|
||||
'../../../util/db': { matrixPools },
|
||||
});
|
||||
|
||||
describe('computeBatchPost', () => {
|
||||
it('multiple posts by one author', async () => {
|
||||
posts = {
|
||||
a: { authors: [{ authorAddress: '0xa1', weightPPM: 1000000 }] },
|
||||
b: { authors: [{ authorAddress: '0xa1', weightPPM: 1000000 }] },
|
||||
c: { authors: [{ authorAddress: '0xa1', weightPPM: 1000000 }] },
|
||||
};
|
||||
pools = {
|
||||
a: { fee: 100, result: { votePasses: true, quorumMet: true } },
|
||||
b: { fee: 100, result: { votePasses: true, quorumMet: true } },
|
||||
c: { fee: 100, result: { votePasses: true, quorumMet: true } },
|
||||
};
|
||||
|
||||
const { authors, references } = await computeBatchPost(['a', 'b', 'c']);
|
||||
assert.deepEqual(authors, [{ authorAddress: '0xa1', weightPPM: 1000000 }]);
|
||||
assert.deepEqual(references, []);
|
||||
});
|
||||
|
||||
it('posts by different authors', async () => {
|
||||
posts = {
|
||||
a: { authors: [{ authorAddress: '0xa1', weightPPM: 1000000 }] },
|
||||
b: { authors: [{ authorAddress: '0xa2', weightPPM: 1000000 }] },
|
||||
};
|
||||
pools = {
|
||||
a: { fee: 100, result: { votePasses: true, quorumMet: true } },
|
||||
b: { fee: 100, result: { votePasses: true, quorumMet: true } },
|
||||
};
|
||||
|
||||
const { authors, references } = await computeBatchPost(['a', 'b']);
|
||||
assert.deepEqual(authors, [
|
||||
{ authorAddress: '0xa1', weightPPM: 500000 },
|
||||
{ authorAddress: '0xa2', weightPPM: 500000 },
|
||||
]);
|
||||
assert.deepEqual(references, []);
|
||||
});
|
||||
|
||||
it('posts by different authors and pools with different fees', async () => {
|
||||
posts = {
|
||||
a: { authors: [{ authorAddress: '0xa1', weightPPM: 1000000 }] },
|
||||
b: { authors: [{ authorAddress: '0xa2', weightPPM: 1000000 }] },
|
||||
};
|
||||
pools = {
|
||||
a: { fee: 100, result: { votePasses: true, quorumMet: true } },
|
||||
b: { fee: 200, result: { votePasses: true, quorumMet: true } },
|
||||
};
|
||||
|
||||
const { authors, references } = await computeBatchPost(['a', 'b']);
|
||||
assert.deepEqual(authors, [
|
||||
{ authorAddress: '0xa1', weightPPM: 333334 },
|
||||
{ authorAddress: '0xa2', weightPPM: 666666 },
|
||||
]);
|
||||
assert.deepEqual(references, []);
|
||||
});
|
||||
|
||||
it('posts with multiple authors', async () => {
|
||||
posts = {
|
||||
a: { authors: [{ authorAddress: '0xa1', weightPPM: 500000 }, { authorAddress: '0xa2', weightPPM: 500000 }] },
|
||||
b: { authors: [{ authorAddress: '0xa1', weightPPM: 500000 }, { authorAddress: '0xa3', weightPPM: 500000 }] },
|
||||
};
|
||||
pools = {
|
||||
a: { fee: 100, result: { votePasses: true, quorumMet: true } },
|
||||
b: { fee: 100, result: { votePasses: true, quorumMet: true } },
|
||||
};
|
||||
|
||||
const { authors, references } = await computeBatchPost(['a', 'b']);
|
||||
assert.deepEqual(authors, [
|
||||
{ authorAddress: '0xa1', weightPPM: 500000 },
|
||||
{ authorAddress: '0xa2', weightPPM: 250000 },
|
||||
{ authorAddress: '0xa3', weightPPM: 250000 },
|
||||
]);
|
||||
assert.deepEqual(references, []);
|
||||
});
|
||||
|
||||
it('post with references', async () => {
|
||||
posts = {
|
||||
a: { authors: [{ authorAddress: '0xa1', weightPPM: 1000000 }] },
|
||||
b: { authors: [{ authorAddress: '0xa2', weightPPM: 1000000 }], references: [{ targetPostId: 'a', weightPPM: 500000 }] },
|
||||
};
|
||||
pools = {
|
||||
b: { fee: 100, result: { votePasses: true, quorumMet: true } },
|
||||
};
|
||||
|
||||
const { authors, references } = await computeBatchPost(['b']);
|
||||
assert.deepEqual(authors, [
|
||||
{ authorAddress: '0xa2', weightPPM: 1000000 },
|
||||
]);
|
||||
assert.deepEqual(references, [{ targetPostId: 'a', weightPPM: 300000 }]);
|
||||
});
|
||||
|
||||
it('post with references and pools with different fees', async () => {
|
||||
posts = {
|
||||
a: { authors: [{ authorAddress: '0xa1', weightPPM: 1000000 }] },
|
||||
b: { authors: [{ authorAddress: '0xa2', weightPPM: 1000000 }] },
|
||||
c: { authors: [{ authorAddress: '0xa3', weightPPM: 1000000 }], references: [{ targetPostId: 'a', weightPPM: 500000 }] },
|
||||
d: { authors: [{ authorAddress: '0xa4', weightPPM: 1000000 }], references: [{ targetPostId: 'b', weightPPM: 500000 }] },
|
||||
};
|
||||
pools = {
|
||||
c: { fee: 100, result: { votePasses: true, quorumMet: true } },
|
||||
d: { fee: 200, result: { votePasses: true, quorumMet: true } },
|
||||
};
|
||||
|
||||
const { authors, references } = await computeBatchPost(['c', 'd']);
|
||||
assert.deepEqual(authors, [
|
||||
{ authorAddress: '0xa3', weightPPM: 333334 },
|
||||
{ authorAddress: '0xa4', weightPPM: 666666 },
|
||||
]);
|
||||
assert.deepEqual(references, [
|
||||
{ targetPostId: 'a', weightPPM: 100000 },
|
||||
{ targetPostId: 'b', weightPPM: 200000 },
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
const { rollup } = require('../../../util/contracts');
|
||||
|
||||
const fetchBatchItemsInfo = async () => {
|
||||
// Read from Rollup.items
|
||||
const itemCount = await rollup.itemCount();
|
||||
const promises = [];
|
||||
for (let i = 0; i < itemCount; i += 1) {
|
||||
promises.push(rollup.items(i));
|
||||
}
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
module.exports = fetchBatchItemsInfo;
|
|
@ -1,71 +0,0 @@
|
|||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
const write = require('../../../util/forum/write');
|
||||
const addPostWithRetry = require('../../../util/add-post-with-retry');
|
||||
const callWithRetry = require('../../../util/call-with-retry');
|
||||
const { getBatchItems, clearBatchItems } = require('./batch-items');
|
||||
const computeBatchPost = require('./compute-batch-post');
|
||||
const { wallet, rollup } = require('../../../util/contracts');
|
||||
const { sendMatrixEvent } = require('../../../matrix-bot');
|
||||
const { stakeRollupAvailability } = require('../utils');
|
||||
const fetchBatchItemsInfo = require('./fetch-batch-items-info');
|
||||
|
||||
const submitRollup = async () => {
|
||||
const availableBatchItems = getBatchItems();
|
||||
const batchItems = [];
|
||||
const batchItemsInfo = await fetchBatchItemsInfo();
|
||||
console.log('available batch items', availableBatchItems);
|
||||
for (let i = 0; i < batchItemsInfo.length; i += 1) {
|
||||
const { postId } = batchItemsInfo[i];
|
||||
if (availableBatchItems.includes(postId)) {
|
||||
console.log(`post ${postId} is available`);
|
||||
batchItems.push(postId);
|
||||
} else {
|
||||
// Batch items have to be submitted in the correct order, with no gaps
|
||||
console.log(`post ${postId} is not available`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!batchItems.length) {
|
||||
return { batchItems: [] };
|
||||
}
|
||||
const { authors, references } = await computeBatchPost(batchItems);
|
||||
if (!authors.length) {
|
||||
return { batchItems: [] };
|
||||
}
|
||||
const content = `Batch of ${batchItems.length} items`;
|
||||
const embeddedData = {
|
||||
batchItems,
|
||||
nonce: uuidv4().replaceAll('-', ''),
|
||||
};
|
||||
const sender = await wallet.getAddress();
|
||||
const contentToVerify = `${content}\n\n${JSON.stringify(embeddedData, null, 2)}`;
|
||||
const signature = await wallet.signMessage(contentToVerify);
|
||||
// Write to the forum database
|
||||
const { hash: batchPostId } = await write({
|
||||
sender, authors, references, content, embeddedData, signature,
|
||||
});
|
||||
// Add rollup post on-chain
|
||||
console.log('adding batch post on-chain', { authors, batchPostId, references });
|
||||
await addPostWithRetry(authors, batchPostId, references);
|
||||
// Stake our availability to be the next rollup worker
|
||||
console.log('staking availability to be the next rollup worker');
|
||||
await stakeRollupAvailability();
|
||||
// Call Rollup.submitBatch
|
||||
console.log('Submitting batch', { batchPostId, batchItems, authors });
|
||||
const poolDuration = 60;
|
||||
await callWithRetry(() => rollup.submitBatch(batchPostId, batchItems, poolDuration));
|
||||
// Send matrix event
|
||||
await sendMatrixEvent('io.dgov.rollup.submit', {
|
||||
batchPostId, batchItems, authors, references,
|
||||
});
|
||||
// Clear the batch in preparation for next batch
|
||||
await clearBatchItems(batchItems);
|
||||
return {
|
||||
batchPostId,
|
||||
batchItems,
|
||||
authors,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = submitRollup;
|
|
@ -1,9 +0,0 @@
|
|||
const {
|
||||
ROLLUP_AVAILABILITY_STAKE_DURATION,
|
||||
ROLLUP_INTERVAL,
|
||||
} = process.env;
|
||||
|
||||
module.exports = {
|
||||
rollupInterval: ROLLUP_INTERVAL,
|
||||
availabilityStakeDuration: ROLLUP_AVAILABILITY_STAKE_DURATION || 600,
|
||||
};
|
|
@ -1,189 +0,0 @@
|
|||
const { isEqual } = require('lodash');
|
||||
|
||||
const { registerDecider } = require('../validation-pools');
|
||||
const { registerMatrixEventHandler, sendMatrixText, sendMatrixEvent } = require('../../matrix-bot');
|
||||
const { matrixPools, matrixUserToAuthorAddress } = require('../../util/db');
|
||||
const {
|
||||
rollup, wallet,
|
||||
} = require('../../util/contracts');
|
||||
const read = require('../../util/forum/read');
|
||||
const { availabilityStakeDuration } = require('./config');
|
||||
const {
|
||||
stakeRollupAvailability, authorsMatch, validatePost,
|
||||
referencesMatch,
|
||||
} = require('./utils');
|
||||
const computeMatrixPoolResult = require('./matrix-pools/compute-result');
|
||||
const { initializeBatchItems, addBatchItem, clearBatchItems } = require('./batch/batch-items');
|
||||
const { getCurrentBatchWorker, initializeBatchWorker } = require('./batch/batch-worker');
|
||||
const initiateMatrixPool = require('./matrix-pools/initiate');
|
||||
const { initiateMatrixPools } = require('./matrix-pools/initiate-matrix-pools');
|
||||
const computeBatchPost = require('./batch/compute-batch-post');
|
||||
|
||||
const start = async () => {
|
||||
console.log('registering validation pool decider for rollup');
|
||||
registerDecider(async (pool, post) => {
|
||||
// If this is not sent by the work1 contract, it's not of interest here.
|
||||
if (pool.sender !== rollup.target) return false;
|
||||
|
||||
// A rollup post should contain
|
||||
// - a list of off-chain validation pools
|
||||
// - authorship corresponding to the result of those off-chain pools
|
||||
if (!post.embeddedData?.batchItems) return false;
|
||||
|
||||
// Our task here is to check whether the posted result agrees with our own computations
|
||||
try {
|
||||
const { authors, references } = await computeBatchPost(post.embeddedData.batchItems);
|
||||
const valid = authorsMatch(post.authors, authors)
|
||||
&& referencesMatch(post.references, references);
|
||||
console.log(`batch post ${pool.props.postId} is ${valid ? 'valid' : 'invalid'}`);
|
||||
return valid;
|
||||
} catch (e) {
|
||||
console.error('Error calculating batch post author weights', e);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Even if we're not the current batch worker, keep track of batch items
|
||||
initializeBatchItems();
|
||||
|
||||
// Check for an assigned batch worker
|
||||
await initializeBatchWorker();
|
||||
|
||||
// Stake availability and set an interval to maintain it
|
||||
await stakeRollupAvailability();
|
||||
setInterval(stakeRollupAvailability, availabilityStakeDuration * 1000);
|
||||
|
||||
// Initiate any matrix pools that haven't already occurred
|
||||
await initiateMatrixPools();
|
||||
|
||||
/// `sender` is the address that called Rollup.addItem on chain, i.e. the Work2 contract.
|
||||
rollup.on('BatchItemAdded', async (postId, sender, fee) => {
|
||||
// If we are the batch worker or there is no batch worker, initiate a matrix pool
|
||||
const batchWorker = getCurrentBatchWorker();
|
||||
if (batchWorker === await wallet.getAddress()
|
||||
|| batchWorker === '0x0000000000000000000000000000000000000000') {
|
||||
let post;
|
||||
try {
|
||||
post = await read(postId);
|
||||
} catch (e) {
|
||||
console.error(`Post ID ${postId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize a matrix pool
|
||||
try {
|
||||
await matrixPools.get(postId);
|
||||
// If this doesn't throw, it means we or someone else already sent this event
|
||||
console.log(`Matrix pool start event has already been sent for postId ${postId}`);
|
||||
} catch (e) {
|
||||
if (e.status === 404) {
|
||||
await initiateMatrixPool(postId, post, sender, fee);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registerMatrixEventHandler(async (client, roomId, event) => {
|
||||
switch (event.type) {
|
||||
case 'io.dgov.pool.start': {
|
||||
// Note that matrix pools are identified by the postId to which they pertain.
|
||||
// This means that for a given post there can only be one matrix pool at a time.
|
||||
const { postId, sender, ...params } = event.content;
|
||||
// We can use LevelDB to store information about validation pools
|
||||
const eventId = event.event_id;
|
||||
console.log('Matrix pool started', { postId, ...params });
|
||||
// Validate the target post, and stake for/against
|
||||
let post;
|
||||
try {
|
||||
post = await read(postId);
|
||||
} catch (e) {
|
||||
console.error(`Post ID ${postId} not found`);
|
||||
break;
|
||||
}
|
||||
// Register our own stake and send a message
|
||||
const { amount, inFavor } = await validatePost(sender, post);
|
||||
sendMatrixEvent('io.dgov.pool.stake', { postId, amount, inFavor });
|
||||
const matrixPool = {
|
||||
postId,
|
||||
roomId,
|
||||
eventId,
|
||||
...params,
|
||||
stakes: [{ amount, inFavor, account: await wallet.getAddress() }],
|
||||
};
|
||||
await matrixPools.put(postId, matrixPool);
|
||||
break;
|
||||
}
|
||||
case 'io.dgov.pool.stake': {
|
||||
const { postId, amount, inFavor } = event.content;
|
||||
let account;
|
||||
try {
|
||||
account = await matrixUserToAuthorAddress(event.sender);
|
||||
} catch (e) {
|
||||
// Error, sender has not registered their matrix identity
|
||||
sendMatrixText(`Matrix user ${event.sender} has not registered their wallet address`);
|
||||
break;
|
||||
}
|
||||
let matrixPool;
|
||||
try {
|
||||
matrixPool = await matrixPools.get(postId);
|
||||
} catch (e) {
|
||||
// Error. matrix pool not found
|
||||
sendMatrixText(`Received stake for unknown matrix pool, for post ${postId}. Stake sent by ${event.sender}`);
|
||||
break;
|
||||
}
|
||||
const stake = { account, amount, inFavor };
|
||||
matrixPool.stakes = matrixPool.stakes ?? [];
|
||||
matrixPool.stakes.push(stake);
|
||||
await matrixPools.put(postId, matrixPool);
|
||||
console.log(`registered stake in matrix pool for post ${postId} by ${account}`);
|
||||
break;
|
||||
}
|
||||
case 'io.dgov.pool.result': {
|
||||
// This should be sent by the current batch worker
|
||||
// const { stakedFor, stakedAgainst, totalSupply, votePasses, quorumMet, } = result;
|
||||
const { postId, result } = event.content;
|
||||
let matrixPool;
|
||||
try {
|
||||
matrixPool = await matrixPools.get(postId);
|
||||
} catch (e) {
|
||||
// Error. matrix pool not found
|
||||
sendMatrixText(`Received result for unknown matrix pool, for post ${postId}. Result sent by ${event.sender}`);
|
||||
break;
|
||||
}
|
||||
// Compare batch worker's result with ours to verify and provide early warning
|
||||
const expectedResult = await computeMatrixPoolResult(matrixPool);
|
||||
if (!isEqual(result, expectedResult)) {
|
||||
sendMatrixText(`Unexpected result for matrix pool, for post ${postId}. Result sent by ${event.sender}\n\n`
|
||||
+ `received ${JSON.stringify(result)}\n`
|
||||
+ `expected ${JSON.stringify(expectedResult)}`);
|
||||
}
|
||||
matrixPool.result = result;
|
||||
await matrixPools.put(postId, matrixPool);
|
||||
await addBatchItem(postId);
|
||||
break;
|
||||
}
|
||||
case 'io.dgov.rollup.submit': {
|
||||
// This should include the identifier of the on-chain validation pool
|
||||
const {
|
||||
batchPostId, batchItems, authors, references,
|
||||
} = event.content;
|
||||
// Compare batch worker's result with ours to verify
|
||||
const { expectedAuthors, expectedReferences } = await computeBatchPost(batchItems);
|
||||
if (!authorsMatch(authors, expectedAuthors)
|
||||
|| !referencesMatch(references, expectedReferences)) {
|
||||
sendMatrixText(`Unexpected result for batch post ${batchPostId}`);
|
||||
}
|
||||
// Reset batchItems in preparation for next batch
|
||||
await clearBatchItems(batchItems);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
};
|
|
@ -1,23 +0,0 @@
|
|||
const {
|
||||
dao,
|
||||
} = require('../../../util/contracts');
|
||||
|
||||
const computeMatrixPoolResult = async (matrixPool) => {
|
||||
// This should already contain all the info we need to evaluate the outcome
|
||||
const { stakes, quorum, winRatio } = matrixPool;
|
||||
const stakedFor = stakes
|
||||
.filter((x) => x.inFavor)
|
||||
.reduce((total, { amount }) => total + amount, 0);
|
||||
const stakedAgainst = stakes
|
||||
.filter((x) => !x.inFavor)
|
||||
.reduce((total, { amount }) => total + amount, 0);
|
||||
const votePasses = stakedFor * winRatio[1] >= (stakedFor + stakedAgainst) * winRatio[0];
|
||||
const totalSupply = Number(await dao.totalSupply());
|
||||
const quorumMet = (stakedFor + stakedAgainst) * quorum[1] >= totalSupply * quorum[0];
|
||||
const result = {
|
||||
stakedFor, stakedAgainst, totalSupply, votePasses, quorumMet,
|
||||
};
|
||||
return result;
|
||||
};
|
||||
|
||||
module.exports = computeMatrixPoolResult;
|
|
@ -1,45 +0,0 @@
|
|||
const { sendMatrixEvent } = require('../../../matrix-bot');
|
||||
const { wallet } = require('../../../util/contracts');
|
||||
const { matrixPools } = require('../../../util/db');
|
||||
const { addBatchItem, getBatchItems } = require('../batch/batch-items');
|
||||
const { getCurrentBatchWorker, getBatchAge } = require('../batch/batch-worker');
|
||||
const computeMatrixPoolResult = require('./compute-result');
|
||||
const { rollupInterval } = require('../config');
|
||||
const submitRollup = require('../batch/submit-rollup');
|
||||
const { stakeRollupAvailability } = require('../utils');
|
||||
|
||||
const evaluateMatrixPoolOutcome = async (postId) => {
|
||||
const matrixPool = await matrixPools.get(postId);
|
||||
const result = await computeMatrixPoolResult(matrixPool);
|
||||
console.log(`Matrix pool for post ${postId} outcome evaluated`, result);
|
||||
matrixPool.result = result;
|
||||
await matrixPools.put(postId, matrixPool);
|
||||
sendMatrixEvent('io.dgov.pool.result', { postId, result });
|
||||
|
||||
await addBatchItem(postId);
|
||||
|
||||
let submitBatch = false;
|
||||
const batchWorker = getCurrentBatchWorker();
|
||||
if (batchWorker === '0x0000000000000000000000000000000000000000') {
|
||||
// If there's no batch worker, we should stake our availability
|
||||
// and then submit the batch immediately.
|
||||
console.log('There is no batch worker assigned. Staking availability and submitting first batch.');
|
||||
submitBatch = true;
|
||||
} else if (batchWorker === await wallet.getAddress()) {
|
||||
// If we are the batch worker, we should wait an appropriate amout of time /
|
||||
// number of matrix pools before submitting a batch.
|
||||
const batchAge = getBatchAge();
|
||||
const batchItems = getBatchItems();
|
||||
if (batchAge > rollupInterval && batchItems.length) {
|
||||
console.log(`Batch age = ${batchAge}, size = ${batchItems.length}. Submitting batch.`);
|
||||
submitBatch = true;
|
||||
}
|
||||
}
|
||||
if (submitBatch) {
|
||||
await stakeRollupAvailability();
|
||||
await submitRollup();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
module.exports = evaluateMatrixPoolOutcome;
|
|
@ -1,35 +0,0 @@
|
|||
const Promise = require('bluebird');
|
||||
const { matrixPools } = require('../../../util/db');
|
||||
const read = require('../../../util/forum/read');
|
||||
const initiateMatrixPool = require('./initiate');
|
||||
const { addBatchItem, getBatchItems } = require('../batch/batch-items');
|
||||
const fetchBatchItemsInfo = require('../batch/fetch-batch-items-info');
|
||||
|
||||
const initiateMatrixPools = async () => {
|
||||
const batchItemsInfo = await fetchBatchItemsInfo();
|
||||
// Make sure there's a matrix pool for each batch item.
|
||||
// If there's not, then let's start one.
|
||||
await Promise.each(batchItemsInfo, async ({ postId, sender, fee }) => {
|
||||
let post;
|
||||
try {
|
||||
post = await read(postId);
|
||||
} catch (e) {
|
||||
console.error(`Post ID ${postId} not found`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const matrixPool = await matrixPools.get(postId);
|
||||
if (matrixPool.result) {
|
||||
await addBatchItem(postId);
|
||||
}
|
||||
} catch (e) {
|
||||
// TODO: It's possible we missed messages about pools that have already occurred.
|
||||
await initiateMatrixPool(postId, post, sender, fee);
|
||||
}
|
||||
});
|
||||
console.log('batch items count:', getBatchItems().length);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
initiateMatrixPools,
|
||||
};
|
|
@ -1,47 +0,0 @@
|
|||
const { sendMatrixEvent } = require('../../../matrix-bot');
|
||||
const { validatePost } = require('../utils');
|
||||
const evaluateMatrixPoolOutcome = require('./evaluate');
|
||||
const { matrixPools } = require('../../../util/db');
|
||||
const { wallet } = require('../../../util/contracts');
|
||||
|
||||
const initiateMatrixPool = async (postId, post, sender, fee) => {
|
||||
const duration = 20;
|
||||
const quorum = [1, 3];
|
||||
const winRatio = [1, 2];
|
||||
const params = {
|
||||
sender,
|
||||
fee: Number(fee),
|
||||
duration,
|
||||
quorum,
|
||||
winRatio,
|
||||
};
|
||||
|
||||
console.log('sending matrix pool start event');
|
||||
const { roomId, eventId } = await sendMatrixEvent('io.dgov.pool.start', {
|
||||
postId,
|
||||
...params,
|
||||
});
|
||||
console.log('sent matrix pool start event');
|
||||
// Register our own stake and send a message
|
||||
const { amount, inFavor } = await validatePost(sender, post);
|
||||
sendMatrixEvent('io.dgov.pool.stake', { postId, amount, inFavor });
|
||||
const matrixPool = {
|
||||
postId,
|
||||
roomId,
|
||||
eventId,
|
||||
...params,
|
||||
stakes: [{ amount, inFavor, account: await wallet.getAddress() }],
|
||||
};
|
||||
await matrixPools.put(postId, matrixPool);
|
||||
|
||||
// Since we're assuming responsibility as the batch worker,
|
||||
// set a timeout to evaulate the outcome
|
||||
setTimeout(
|
||||
() => {
|
||||
evaluateMatrixPoolOutcome(postId);
|
||||
},
|
||||
duration * 1000,
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = initiateMatrixPool;
|
|
@ -1,58 +0,0 @@
|
|||
const callWithRetry = require('../../util/call-with-retry');
|
||||
const {
|
||||
rollup, wallet, dao,
|
||||
work2,
|
||||
} = require('../../util/contracts');
|
||||
const { availabilityStakeDuration } = require('./config');
|
||||
|
||||
const stakeRollupAvailability = async () => {
|
||||
const currentRep = await dao.balanceOf(await wallet.getAddress());
|
||||
if (currentRep) {
|
||||
await callWithRetry(() => dao.stakeAvailability(
|
||||
rollup.target,
|
||||
currentRep,
|
||||
availabilityStakeDuration,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
const authorsMatch = async (authors, expectedAuthors) => {
|
||||
if (expectedAuthors.length !== authors.length) return false;
|
||||
return authors.every(({ authorAddress, weightPPM }) => {
|
||||
const expectedAuthor = expectedAuthors.find((x) => x.authorAddress === authorAddress);
|
||||
return weightPPM === expectedAuthor.weightPPM;
|
||||
});
|
||||
};
|
||||
|
||||
const referencesMatch = async (references, expectedReferences) => {
|
||||
if (expectedReferences.length !== references.length) return false;
|
||||
return references.every(({ targetPostId, weightPPM }) => {
|
||||
const expectedReference = expectedReferences.find((x) => x.targetPostId === targetPostId);
|
||||
return weightPPM === expectedReference.weightPPM;
|
||||
});
|
||||
};
|
||||
|
||||
const validateWorkEvidence = async (sender, post) => {
|
||||
let valid = false;
|
||||
if (sender === work2.target) {
|
||||
const expectedContent = 'This is a work evidence post';
|
||||
valid = post.content.startsWith(expectedContent);
|
||||
}
|
||||
console.log(`Work evidence ${valid ? 'matched' : 'did not match'} the expected content`);
|
||||
return valid;
|
||||
};
|
||||
|
||||
const validatePost = async (sender, post) => {
|
||||
const currentRep = Number(await dao.balanceOf(await wallet.getAddress()));
|
||||
const valid = await validateWorkEvidence(sender, post);
|
||||
const stake = { amount: currentRep, inFavor: valid };
|
||||
return stake;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
stakeRollupAvailability,
|
||||
authorsMatch,
|
||||
referencesMatch,
|
||||
validateWorkEvidence,
|
||||
validatePost,
|
||||
};
|
|
@ -1,82 +0,0 @@
|
|||
const Promise = require('bluebird');
|
||||
|
||||
const { dao, wallet } = require('../util/contracts');
|
||||
const read = require('../util/forum/read');
|
||||
const gateByProposal = require('../util/gate-by-proposal');
|
||||
|
||||
const {
|
||||
ENABLE_STAKING,
|
||||
} = process.env;
|
||||
|
||||
const deciders = [];
|
||||
|
||||
const registerDecider = (decider) => {
|
||||
deciders.push(decider);
|
||||
};
|
||||
|
||||
let enableStaking;
|
||||
|
||||
if (ENABLE_STAKING === 'false') {
|
||||
console.log('STAKING DISABLED');
|
||||
enableStaking = false;
|
||||
} else {
|
||||
gateByProposal(((enable) => {
|
||||
enableStaking = enable;
|
||||
}));
|
||||
}
|
||||
|
||||
const start = async () => {
|
||||
dao.on('ValidationPoolInitiated', async (poolIndex) => {
|
||||
console.log('Validation Pool Initiated, index', poolIndex);
|
||||
const pool = await dao.getValidationPool(poolIndex);
|
||||
// Read post from database
|
||||
let post;
|
||||
try {
|
||||
post = await read(pool.props.postId);
|
||||
} catch (e) {
|
||||
// Post not found
|
||||
console.error(`error: post for validation pool ${poolIndex} not found`);
|
||||
return;
|
||||
}
|
||||
console.log('postId:', pool.props.postId);
|
||||
console.log('post.content:', post.content);
|
||||
// We have the opportunity to stake for/against this validation pool.
|
||||
// To implement the legislative process of upgrading this protocol,
|
||||
// the execution of this code can be protected by a given proposal.
|
||||
// The code will only execute if the proposal has been accepted.
|
||||
if (!enableStaking) {
|
||||
return;
|
||||
}
|
||||
const decisions = await Promise.mapSeries(deciders, (decider) => decider(pool, post));
|
||||
const inFavor = decisions.some((x) => x === true);
|
||||
const nullResult = decisions.some((x) => x === null);
|
||||
const currentRep = await dao.balanceOf(await wallet.getAddress());
|
||||
let stakeAmount = currentRep;
|
||||
if (!inFavor && nullResult) {
|
||||
console.log(`Obtained a NULL RESULT for pool ${poolIndex}.`);
|
||||
// TODO: Retry?
|
||||
// TODO: Notify
|
||||
// Calculate the minimum stake S against the post, such that if the honest actors
|
||||
// each stake S, the result will be enough to meet the win ratio.
|
||||
// This way, we combat the threat of a truly invalid post,
|
||||
// while reducing our exposure in the case that the error is unique to us.
|
||||
// Assume 2/3 honest actors.
|
||||
// S * (2/3) = 1/3
|
||||
// S = 1/2;
|
||||
stakeAmount = Math.ceil(currentRep / 2);
|
||||
}
|
||||
// Stake all available reputation
|
||||
console.log(`STAKING ${stakeAmount} ${inFavor ? 'in favor of' : 'against'} pool ${poolIndex}`);
|
||||
try {
|
||||
await dao.stakeOnValidationPool(poolIndex, stakeAmount, inFavor);
|
||||
} catch (e) {
|
||||
// Maybe the end time passed?
|
||||
console.error(`STAKING failed, reason: ${e.reason}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
registerDecider,
|
||||
};
|
|
@ -1,26 +0,0 @@
|
|||
const { getContractAddressByNetworkName } = require('../util/contract-config');
|
||||
const { registerDecider } = require('./validation-pools');
|
||||
|
||||
const {
|
||||
ETH_NETWORK,
|
||||
} = process.env;
|
||||
|
||||
const work1Address = getContractAddressByNetworkName(ETH_NETWORK, 'Work1');
|
||||
|
||||
const start = async () => {
|
||||
console.log('registering validation pool decider for work1');
|
||||
registerDecider((pool, post) => {
|
||||
// If this is not sent by the work1 contract, it's not of interest here.
|
||||
if (pool.sender !== work1Address) return false;
|
||||
|
||||
const expectedContent = 'This is a work evidence post';
|
||||
const result = post.content.startsWith(expectedContent);
|
||||
|
||||
console.log(`Work evidence ${result ? 'matched' : 'did not match'} the expected content`);
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
};
|
|
@ -2,19 +2,12 @@ require('dotenv').config();
|
|||
|
||||
const api = require('./api');
|
||||
const matrixBot = require('./matrix-bot');
|
||||
const eventHandlers = require('./event-handlers');
|
||||
const contractListeners = require('./contract-listeners');
|
||||
|
||||
const {
|
||||
ENABLE_API,
|
||||
ENABLE_MATRIX,
|
||||
} = process.env;
|
||||
api.start();
|
||||
|
||||
if (ENABLE_API !== 'false') {
|
||||
api.start();
|
||||
}
|
||||
|
||||
if (ENABLE_MATRIX !== 'false') {
|
||||
if (process.env.ENABLE_MATRIX !== 'false') {
|
||||
matrixBot.start();
|
||||
}
|
||||
|
||||
eventHandlers.start();
|
||||
contractListeners.start();
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
const { setTargetRoomId } = require('./outbound-queue');
|
||||
|
||||
const {
|
||||
appState,
|
||||
proposalEventIds,
|
||||
} = require('../util/db');
|
||||
|
||||
const {
|
||||
BOT_INSTANCE_ID,
|
||||
ETH_NETWORK,
|
||||
} = process.env;
|
||||
|
||||
const handleCommand = async (client, roomId, event) => {
|
||||
// Don't handle unhelpful events (ones that aren't text messages, are redacted, or sent by us)
|
||||
if (event.content?.msgtype !== 'm.text') return;
|
||||
if (event.sender === await client.getUserId()) return;
|
||||
|
||||
const helloRegex = /^!hello\b/i;
|
||||
const targetRegex = /^!target (.*)\b/i;
|
||||
const proposalRegex = /\bprop(|osal) ([0-9]+)\b/i;
|
||||
|
||||
const { body } = event.content;
|
||||
|
||||
if (helloRegex.test(body)) {
|
||||
console.log(`!hello roomId ${roomId}`);
|
||||
await client.replyNotice(roomId, event, 'Hello world!');
|
||||
} else if (targetRegex.test(body)) {
|
||||
const [, instanceId] = targetRegex.exec(body);
|
||||
console.log(`!target roomId ${roomId} instanceId ${instanceId}`);
|
||||
if (instanceId === BOT_INSTANCE_ID) {
|
||||
setTargetRoomId(roomId);
|
||||
await appState.put('targetRoomId', roomId);
|
||||
await client.replyNotice(roomId, event, `Proposal events will be sent to this room for network ${ETH_NETWORK}`);
|
||||
}
|
||||
} else if (proposalRegex.test(body)) {
|
||||
const [, , proposalIndexStr] = proposalRegex.exec(body);
|
||||
const proposalIndex = parseInt(proposalIndexStr, 10);
|
||||
console.log(`mention of proposal ${proposalIndex} in roomId ${roomId}`);
|
||||
try {
|
||||
const proposalEventId = await proposalEventIds.get(proposalIndex);
|
||||
const proposalEventUri = `https://matrix.to/#/${roomId}/${proposalEventId}`;
|
||||
const content = {
|
||||
body: `Proposal ${proposalIndex}: ${proposalEventUri}`,
|
||||
msgtype: 'm.text',
|
||||
};
|
||||
if (event.content['m.relates_to']?.rel_type === 'm.thread') {
|
||||
content['m.relates_to'] = event.content['m.relates_to'];
|
||||
}
|
||||
await client.sendEvent(roomId, 'm.room.message', content);
|
||||
} catch (e) {
|
||||
// Not found
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const registerCommands = (client) => {
|
||||
client.on('room.message', (roomId, event) => handleCommand(client, roomId, event));
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
registerCommands,
|
||||
};
|
|
@ -5,6 +5,9 @@ const {
|
|||
SimpleFsStorageProvider,
|
||||
} = require('matrix-bot-sdk');
|
||||
|
||||
const { registerCommands } = require('./commands');
|
||||
const { registerRoomEventHandler } = require('./room-events');
|
||||
|
||||
const {
|
||||
MATRIX_HOMESERVER_URL,
|
||||
MATRIX_ACCESS_TOKEN,
|
||||
|
@ -14,51 +17,42 @@ const {
|
|||
|
||||
const storageProvider = new SimpleFsStorageProvider(BOT_STORAGE_PATH);
|
||||
const cryptoProvider = new RustSdkCryptoStorageProvider(BOT_CRYPTO_STORAGE_PATH);
|
||||
console.log('MATRIX_HOMESERVER_URL:', MATRIX_HOMESERVER_URL);
|
||||
const matrixClient = new MatrixClient(
|
||||
MATRIX_HOMESERVER_URL,
|
||||
MATRIX_ACCESS_TOKEN,
|
||||
storageProvider,
|
||||
cryptoProvider,
|
||||
);
|
||||
let client;
|
||||
let joinedRooms;
|
||||
|
||||
const { initializeOutboundQueue, sendMatrixEvent, sendMatrixText } = require('./outbound-queue');
|
||||
const { startOutboundQueue } = require('./outbound-queue');
|
||||
|
||||
const start = async () => {
|
||||
// Automatically join a room to which we are invited
|
||||
AutojoinRoomsMixin.setupOnClient(matrixClient);
|
||||
console.log('MATRIX_HOMESERVER_URL:', MATRIX_HOMESERVER_URL);
|
||||
client = new MatrixClient(
|
||||
MATRIX_HOMESERVER_URL,
|
||||
MATRIX_ACCESS_TOKEN,
|
||||
storageProvider,
|
||||
cryptoProvider,
|
||||
);
|
||||
|
||||
joinedRooms = await matrixClient.getJoinedRooms();
|
||||
// Automatically join a room to which we are invited
|
||||
AutojoinRoomsMixin.setupOnClient(client);
|
||||
|
||||
joinedRooms = await client.getJoinedRooms();
|
||||
console.log('joined rooms:', joinedRooms);
|
||||
|
||||
matrixClient.start().then(() => {
|
||||
console.log('Matrix bot started!');
|
||||
// Before we start the bot, register our command handler
|
||||
registerCommands(client);
|
||||
|
||||
// Handler for custom events
|
||||
registerRoomEventHandler(client);
|
||||
|
||||
client.start().then(() => {
|
||||
console.log('Bot started!');
|
||||
// Start the outbound queue
|
||||
initializeOutboundQueue(matrixClient);
|
||||
startOutboundQueue(client);
|
||||
});
|
||||
};
|
||||
|
||||
const registerMatrixMessageHandler = (eventHandler) => {
|
||||
matrixClient.on('room.message', async (roomId, event) => {
|
||||
if (event.sender === await matrixClient.getUserId()) return;
|
||||
eventHandler(matrixClient, roomId, event);
|
||||
});
|
||||
};
|
||||
|
||||
const registerMatrixEventHandler = (eventHandler) => {
|
||||
matrixClient.on('room.event', async (roomId, event) => {
|
||||
if (event.sender === await matrixClient.getUserId()) return;
|
||||
if (event.state_key !== undefined) return; // state event
|
||||
eventHandler(matrixClient, roomId, event);
|
||||
});
|
||||
};
|
||||
const getClient = () => client;
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
matrixClient,
|
||||
registerMatrixMessageHandler,
|
||||
registerMatrixEventHandler,
|
||||
sendMatrixEvent,
|
||||
sendMatrixText,
|
||||
getClient,
|
||||
};
|
||||
|
|
|
@ -1,82 +1,55 @@
|
|||
const fastq = require('fastq');
|
||||
|
||||
const { applicationData } = require('../util/db');
|
||||
const {
|
||||
proposalEventIds,
|
||||
} = require('../util/db');
|
||||
|
||||
let matrixClient;
|
||||
let client;
|
||||
let targetRoomId;
|
||||
|
||||
const setTargetRoomId = (roomId) => {
|
||||
targetRoomId = roomId;
|
||||
};
|
||||
|
||||
const processOutboundQueue = async ({ type, ...args }) => {
|
||||
console.log('processing outbound queue item');
|
||||
if (!targetRoomId) return;
|
||||
switch (type) {
|
||||
case 'MatrixEvent': {
|
||||
const { eventType, content, onSend } = args;
|
||||
const eventId = await matrixClient.sendEvent(targetRoomId, eventType, content);
|
||||
onSend(targetRoomId, eventId);
|
||||
break;
|
||||
}
|
||||
case 'MatrixText': {
|
||||
const { text, onSend } = args;
|
||||
const eventId = await matrixClient.sendText(targetRoomId, text);
|
||||
onSend(targetRoomId, eventId);
|
||||
case 'NewProposal': {
|
||||
const { proposalIndex, text } = args;
|
||||
try {
|
||||
await proposalEventIds.get(Number(proposalIndex));
|
||||
// If this doesn't throw, it means we already sent a message for this proposal
|
||||
} catch (e) {
|
||||
if (e.status === 404) {
|
||||
console.log('sending to room', targetRoomId, { text });
|
||||
const eventId = await client.sendText(targetRoomId, text);
|
||||
await proposalEventIds.put(Number(proposalIndex), eventId);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
};
|
||||
|
||||
const outboundQueue = fastq.promise(processOutboundQueue, 1);
|
||||
// Pause outbound queue until matrixClient and targetRoomId are set
|
||||
const outboundQueue = fastq(processOutboundQueue, 1);
|
||||
// Pause until client is set
|
||||
outboundQueue.pause();
|
||||
|
||||
const setTargetRoomId = async (roomId) => {
|
||||
targetRoomId = roomId;
|
||||
console.log('target room ID:', targetRoomId);
|
||||
await applicationData.put('targetRoomId', targetRoomId);
|
||||
if (matrixClient) {
|
||||
console.log('Starting Matrix outbound queue processor');
|
||||
outboundQueue.resume();
|
||||
}
|
||||
const startOutboundQueue = (c) => {
|
||||
client = c;
|
||||
// Resume now that client is set
|
||||
outboundQueue.resume();
|
||||
};
|
||||
|
||||
const initializeOutboundQueue = async (matrixClient_) => {
|
||||
matrixClient = matrixClient_;
|
||||
try {
|
||||
targetRoomId = await applicationData.get('targetRoomId');
|
||||
console.log('target room ID:', targetRoomId);
|
||||
} catch (e) {
|
||||
// No target room set
|
||||
console.warn('target room ID is not set -- will not be able to send messages until it is set. Use !target <bot-id>');
|
||||
}
|
||||
if (targetRoomId) {
|
||||
console.log('Starting Matrix outbound queue processor');
|
||||
outboundQueue.resume();
|
||||
}
|
||||
const sendNewProposalEvent = (proposalIndex, text) => {
|
||||
outboundQueue.push({ type: 'NewProposal', proposalIndex, text });
|
||||
};
|
||||
|
||||
const sendMatrixEvent = async (eventType, content) => new Promise((resolve) => {
|
||||
outboundQueue.push({
|
||||
type: 'MatrixEvent',
|
||||
eventType,
|
||||
content,
|
||||
onSend: ((roomId, eventId) => {
|
||||
resolve({ roomId, eventId });
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
const sendMatrixText = async (text) => new Promise((resolve) => {
|
||||
outboundQueue.push({
|
||||
type: 'MatrixText',
|
||||
text,
|
||||
onSend: ((roomId, eventId) => {
|
||||
resolve({ roomId, eventId });
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
setTargetRoomId,
|
||||
outboundQueue,
|
||||
initializeOutboundQueue,
|
||||
sendMatrixEvent,
|
||||
sendMatrixText,
|
||||
startOutboundQueue,
|
||||
sendNewProposalEvent,
|
||||
};
|
||||
|
|
|
@ -4,10 +4,8 @@ const {
|
|||
matrixUserToAuthorAddress,
|
||||
authorAddressToMatrixUser,
|
||||
} = require('../util/db');
|
||||
const { registerMatrixEventHandler } = require('../matrix-bot');
|
||||
|
||||
const handleRegisterIdentity = async (client, roomId, event) => {
|
||||
if (event.type !== 'io.dgov.identity.register') return;
|
||||
const { message, signature } = event.content;
|
||||
console.log('Received request to register identity');
|
||||
let account;
|
||||
|
@ -33,10 +31,20 @@ const handleRegisterIdentity = async (client, roomId, event) => {
|
|||
}
|
||||
};
|
||||
|
||||
const start = () => {
|
||||
registerMatrixEventHandler(handleRegisterIdentity);
|
||||
const registerRoomEventHandler = (client) => {
|
||||
client.on('room.event', (roomId, event) => {
|
||||
// Note that state events can also be sent down this listener too
|
||||
if (event.state_key !== undefined) return; // state event
|
||||
|
||||
switch (event.type) {
|
||||
case 'io.dgov.identity.register':
|
||||
handleRegisterIdentity(client, roomId, event);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
registerRoomEventHandler,
|
||||
};
|
|
@ -1,16 +0,0 @@
|
|||
const callWithRetry = require('./call-with-retry');
|
||||
const { dao } = require('./contracts');
|
||||
|
||||
const addPostWithRetry = async (authors, hash, references) => {
|
||||
try {
|
||||
await callWithRetry(() => dao.addPost(authors, hash, references));
|
||||
} catch (e) {
|
||||
if (e.reason === 'A post with this postId already exists') {
|
||||
return { alreadyAdded: true };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return { alreadyAdded: false };
|
||||
};
|
||||
|
||||
module.exports = addPostWithRetry;
|
|
@ -1,18 +0,0 @@
|
|||
const Promise = require('bluebird');
|
||||
|
||||
const callWithRetry = async (contractCall, retryDelay = 5000) => {
|
||||
let result;
|
||||
try {
|
||||
result = await contractCall();
|
||||
} catch (e) {
|
||||
if (e.code === 'REPLACEMENT_UNDERPRICED') {
|
||||
console.log('retry delay (sec):', retryDelay / 1000);
|
||||
await Promise.delay(retryDelay);
|
||||
return callWithRetry(contractCall, retryDelay * 2);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
module.exports = callWithRetry;
|
|
@ -3,54 +3,36 @@ const ethers = require('ethers');
|
|||
const { getContractAddressByNetworkName } = require('./contract-config');
|
||||
const DAOArtifact = require('../../contractArtifacts/DAO.json');
|
||||
const ProposalsArtifact = require('../../contractArtifacts/Proposals.json');
|
||||
const RollupArtifact = require('../../contractArtifacts/Rollup.json');
|
||||
const Work2Artifact = require('../../contractArtifacts/Work2.json');
|
||||
|
||||
const {
|
||||
ETH_NETWORK,
|
||||
ETH_PRIVATE_KEY,
|
||||
INFURA_API_KEY,
|
||||
} = process.env;
|
||||
const network = process.env.ETH_NETWORK;
|
||||
|
||||
console.log('network:', ETH_NETWORK);
|
||||
console.log('network:', network);
|
||||
|
||||
const getProvider = () => {
|
||||
switch (ETH_NETWORK) {
|
||||
switch (network) {
|
||||
case 'localhost':
|
||||
return ethers.getDefaultProvider('http://localhost:8545');
|
||||
case 'sepolia':
|
||||
return new ethers.InfuraProvider(
|
||||
ETH_NETWORK,
|
||||
INFURA_API_KEY,
|
||||
network,
|
||||
process.env.INFURA_API_KEY,
|
||||
);
|
||||
default:
|
||||
throw new Error('Unknown network');
|
||||
}
|
||||
};
|
||||
|
||||
const wallet = new ethers.Wallet(ETH_PRIVATE_KEY, getProvider());
|
||||
const wallet = new ethers.Wallet(process.env.ETH_PRIVATE_KEY, getProvider());
|
||||
|
||||
module.exports = {
|
||||
wallet,
|
||||
getProvider,
|
||||
dao: new ethers.Contract(
|
||||
getContractAddressByNetworkName(ETH_NETWORK, 'DAO'),
|
||||
getContractAddressByNetworkName(process.env.ETH_NETWORK, 'DAO'),
|
||||
DAOArtifact.abi,
|
||||
wallet,
|
||||
),
|
||||
proposals: new ethers.Contract(
|
||||
getContractAddressByNetworkName(ETH_NETWORK, 'Proposals'),
|
||||
getContractAddressByNetworkName(process.env.ETH_NETWORK, 'Proposals'),
|
||||
ProposalsArtifact.abi,
|
||||
wallet,
|
||||
),
|
||||
rollup: new ethers.Contract(
|
||||
getContractAddressByNetworkName(ETH_NETWORK, 'Rollup'),
|
||||
RollupArtifact.abi,
|
||||
wallet,
|
||||
),
|
||||
work2: new ethers.Contract(
|
||||
getContractAddressByNetworkName(ETH_NETWORK, 'Work2'),
|
||||
Work2Artifact.abi,
|
||||
wallet,
|
||||
),
|
||||
};
|
||||
|
|
|
@ -3,7 +3,6 @@ const { Level } = require('level');
|
|||
const dataDir = process.env.LEVEL_DATA_DIR || 'data';
|
||||
|
||||
module.exports = {
|
||||
applicationData: new Level(`${dataDir}/applicationData`, { valueEncoding: 'json' }),
|
||||
forum: new Level(`${dataDir}/forum`, { valueEncoding: 'json' }),
|
||||
authorAddresses: new Level(`${dataDir}/authorAddresses`),
|
||||
authorPrivKeys: new Level(`${dataDir}/authorPrivKeys`),
|
||||
|
@ -12,5 +11,4 @@ module.exports = {
|
|||
referendumEventIds: new Level(`${dataDir}/referendumEventIds`, { keyEncoding: 'json' }),
|
||||
matrixUserToAuthorAddress: new Level(`${dataDir}/matrixUserToAuthorAddress`),
|
||||
authorAddressToMatrixUser: new Level(`${dataDir}/authorAddressToMatrixUser`),
|
||||
matrixPools: new Level(`${dataDir}/matrixPools`, { valueEncoding: 'json' }),
|
||||
};
|
||||
|
|
|
@ -10,12 +10,12 @@ const read = async (hash) => {
|
|||
data.embeddedData = data.embeddedData || undefined;
|
||||
|
||||
const {
|
||||
sender, authors, content, signature, embeddedData, references,
|
||||
authors, content, signature, embeddedData, citations,
|
||||
} = data;
|
||||
|
||||
// Verify hash
|
||||
const derivedHash = objectHash({
|
||||
sender, authors, content, signature, embeddedData,
|
||||
authors, content, signature, embeddedData,
|
||||
});
|
||||
if (derivedHash !== hash) {
|
||||
throw new Error('hash mismatch');
|
||||
|
@ -29,7 +29,7 @@ const read = async (hash) => {
|
|||
}
|
||||
|
||||
return {
|
||||
sender, authors, content, signature, embeddedData, references,
|
||||
authors, content, signature, embeddedData, citations,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -2,50 +2,30 @@ const objectHash = require('object-hash');
|
|||
|
||||
const verifySignature = require('../verify-signature');
|
||||
const { forum } = require('../db');
|
||||
const read = require('./read');
|
||||
|
||||
const write = async ({
|
||||
sender, authors, content, references, embeddedData, signature,
|
||||
authors, content, citations, embeddedData, signature,
|
||||
}) => {
|
||||
if (signature) {
|
||||
// Check author signature
|
||||
if (!verifySignature({
|
||||
sender, authors, content, signature, embeddedData,
|
||||
})) {
|
||||
const err = new Error();
|
||||
err.status = 403;
|
||||
err.message = 'Signature verification failed';
|
||||
throw err;
|
||||
if (!verifySignature({
|
||||
authors, content, signature, embeddedData,
|
||||
})) {
|
||||
const err = new Error();
|
||||
err.status = 403;
|
||||
err.message = 'Signature verification failed';
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Compute content hash
|
||||
const data = {
|
||||
sender, authors, content, signature, embeddedData, references,
|
||||
authors, content, signature, embeddedData, citations,
|
||||
};
|
||||
// We omit references from the hash in order to support forum graph import.
|
||||
// When a post is imported, the hashes can be precomputed for cited posts,
|
||||
// without traversing the graph infinitely to compute hashes along entire reference chain.
|
||||
const hash = objectHash({
|
||||
sender, authors, content, signature, embeddedData,
|
||||
authors, content, signature, embeddedData,
|
||||
});
|
||||
|
||||
// Make sure a post with this hash has not already been written
|
||||
let existingPost;
|
||||
try {
|
||||
existingPost = await read(hash);
|
||||
// If this doesn't throw, it means a post with this hash was already written
|
||||
} catch (e) {
|
||||
if (e.status !== 404) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingPost) {
|
||||
const err = new Error();
|
||||
err.status = 403;
|
||||
err.message = `A post with hash ${hash} already exists`;
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Store content
|
||||
await forum.put(hash, data);
|
||||
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
const { proposals } = require('./contracts');
|
||||
|
||||
const {
|
||||
START_PROPOSAL_ID,
|
||||
STOP_PROPOSAL_ID,
|
||||
} = process.env;
|
||||
|
||||
const gateByProposal = async (enable) => {
|
||||
enable(true);
|
||||
|
||||
if (STOP_PROPOSAL_ID) {
|
||||
enable(false);
|
||||
// Check for status
|
||||
const proposal = await proposals.proposals(STOP_PROPOSAL_ID);
|
||||
if (proposal.stage === BigInt(5)) {
|
||||
// Proposal is accepted
|
||||
enable(false);
|
||||
console.log(`STOP_PROPOSAL_ID ${STOP_PROPOSAL_ID} proposal is accepted. Disabling staking.`);
|
||||
} else if (proposal.stage === BigInt(4)) {
|
||||
// Proposal is failed
|
||||
console.log(`STOP_PROPOSAL_ID ${STOP_PROPOSAL_ID} proposal is failed. No effect.`);
|
||||
} else {
|
||||
// Register a ProposalAccepted event handler.
|
||||
console.log(`STOP_PROPOSAL_ID ${STOP_PROPOSAL_ID} proposal is stage ${proposal.stage.toString()}. Registering listener.`);
|
||||
const proposalAcceptedHandler = (proposalIndex) => {
|
||||
if (proposalIndex.toString() === STOP_PROPOSAL_ID) {
|
||||
console.log(`STOP_PROPOSAL_ID ${STOP_PROPOSAL_ID} proposal is accepted. Disabling staking.`);
|
||||
enable(false);
|
||||
proposals.off('ProposalAccepted', proposalAcceptedHandler);
|
||||
}
|
||||
};
|
||||
proposals.on('ProposalAccepted', proposalAcceptedHandler);
|
||||
}
|
||||
}
|
||||
if (START_PROPOSAL_ID) {
|
||||
enable(false);
|
||||
// Check for status
|
||||
const proposal = await proposals.proposals(START_PROPOSAL_ID);
|
||||
if (proposal.stage === BigInt(5)) {
|
||||
// Proposal is accepted
|
||||
enable(true);
|
||||
console.log(`START_PROPOSAL_ID ${START_PROPOSAL_ID} proposal is accepted. Enabling staking.`);
|
||||
} else if (proposal.stage === BigInt(4)) {
|
||||
// Proposal is failed
|
||||
console.log(`START_PROPOSAL_ID ${START_PROPOSAL_ID} proposal is failed. Disabling staking.`);
|
||||
} else {
|
||||
// Register a ProposalAccepted event handler.
|
||||
console.log(`START_PROPOSAL_ID ${START_PROPOSAL_ID} proposal is stage ${proposal.stage.toString()}. Registering listener.`);
|
||||
const proposalAcceptedHandler = (proposalIndex) => {
|
||||
if (proposalIndex.toString() === START_PROPOSAL_ID) {
|
||||
console.log(`START_PROPOSAL_ID ${START_PROPOSAL_ID} proposal is accepted. Enabling staking.`);
|
||||
enable(true);
|
||||
proposals.off('ProposalAccepted', proposalAcceptedHandler);
|
||||
}
|
||||
};
|
||||
proposals.on('ProposalAccepted', proposalAcceptedHandler);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = gateByProposal;
|
|
@ -1,7 +1,7 @@
|
|||
const { recoverPersonalSignature } = require('@metamask/eth-sig-util');
|
||||
|
||||
const verifySignature = ({
|
||||
sender, authors, content, signature, embeddedData,
|
||||
authors, content, signature, embeddedData,
|
||||
}) => {
|
||||
let contentToVerify = content;
|
||||
if (embeddedData && Object.entries(embeddedData).length) {
|
||||
|
@ -9,12 +9,9 @@ const verifySignature = ({
|
|||
}
|
||||
try {
|
||||
const account = recoverPersonalSignature({ data: contentToVerify, signature });
|
||||
const addresses = authors
|
||||
.map((author) => author.authorAddress)
|
||||
.concat(sender ? [sender] : [])
|
||||
.map((authorAddress) => authorAddress.toLowerCase());
|
||||
if (!addresses.includes(account.toLowerCase())) {
|
||||
console.log('error: signer is not among the authors or sender');
|
||||
const authorAddresses = authors.map((author) => author.authorAddress.toLowerCase());
|
||||
if (!authorAddresses.includes(account.toLowerCase())) {
|
||||
console.log('error: signer is not among the authors');
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
const ethers = require('ethers');
|
||||
|
||||
const { getProvider } = require('./contracts');
|
||||
|
||||
const {
|
||||
ETH_PRIVATE_KEY,
|
||||
} = process.env;
|
||||
|
||||
const wallet = new ethers.Wallet(ETH_PRIVATE_KEY, getProvider());
|
||||
|
||||
module.exports = wallet;
|
|
@ -6,5 +6,4 @@ MAINNET_PRIVATE_KEY=
|
|||
SEED_PHRASE=
|
||||
ETHERSCAN_API_KEY=
|
||||
WORK1_PRICE="0.001"
|
||||
ONBOARDING_PRICE="0.001"
|
||||
ROLLUP_INTERVAL=120
|
||||
ONBOARDING_PRICE="0.001"
|
|
@ -1,24 +1,14 @@
|
|||
{
|
||||
"localhost": {
|
||||
"DAO": "0x3734B0944ea37694E85AEF60D5b256d19EDA04be",
|
||||
"Work1": "0x8BDA04936887cF11263B87185E4D19e8158c6296",
|
||||
"Onboarding": "0x8688E736D0D72161db4D25f68EF7d0EE4856ba19",
|
||||
"Proposals": "0x3287061aDCeE36C1aae420a06E4a5EaE865Fe3ce",
|
||||
"Rollup": "0x71cb20D63576a0Fa4F620a2E96C73F82848B09e1",
|
||||
"Work2": "0x76Dfe9F47f06112a1b78960bf37d87CfbB6D6133",
|
||||
"Reputation": "0xEAefe601Aad7422307B99be65bbE005aeA966012",
|
||||
"Forum": "0x79e365342329560e8420d7a0f016633d7640cB18",
|
||||
"Bench": "0xC0f00E5915F9abE6476858fD1961EAf79395ea64"
|
||||
"DAO": "0x8d914D38dD301FC4606f5aa9fEcF8A76389020d3",
|
||||
"Work1": "0xfe58B9EB03F75A603de1B286584f5E9532ab8fB5",
|
||||
"Onboarding": "0x1d63FDe5B461106729fE1e5e38A02fc68C518Af5",
|
||||
"Proposals": "0x050C420Cc4995B41217Eba1B54B82Fd5687e9139"
|
||||
},
|
||||
"sepolia": {
|
||||
"DAO": "0xBA2e65ae29667E145343bD5Fd655A72dcf873b08",
|
||||
"Work1": "0x251dB891768ea85DaCA6bb567669F97248D09Fe3",
|
||||
"Onboarding": "0x78FC8b520001560A9D7a61072855218320C71BDC",
|
||||
"Proposals": "0xA888cDC4Bd80d402b14B1FeDE5FF471F1737570c",
|
||||
"Reputation": "0x62cc0035B17F1686cE30320B90373c77fcaA58CD",
|
||||
"Forum": "0x51b5Af12707e0d879B985Cb0216bFAC6dca85501",
|
||||
"Bench": "0x98d9F0e97Af71936747819040ddBE896A548ef4d",
|
||||
"Rollup": "0x678DC2c846bfDCC813ea27DfEE428f1d7f2521ED",
|
||||
"Work2": "0x609102Fb6cA15da80D37E8cA68aBD5e1bD9C855B"
|
||||
"DAO": "0x8e5bd58B2ca8910C5F9be8de847d6883B15c60d2",
|
||||
"Work1": "0x1708A144F284C1a9615C25b674E4a08992CE93e4",
|
||||
"Onboarding": "0xb21D4c986715A1adb5e87F752842613648C20a7B",
|
||||
"Proposals": "0x930c47293F206780E8F166338bDaFF3520306032"
|
||||
}
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
// SPDX-License-Identifier: Unlicense
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "./core/DAO.sol";
|
||||
import "./interfaces/IAcceptAvailability.sol";
|
||||
|
||||
contract Availability is IAcceptAvailability, DAOContract {
|
||||
struct AvailabilityStake {
|
||||
address worker;
|
||||
uint256 amount;
|
||||
uint endTime;
|
||||
bool assigned;
|
||||
}
|
||||
|
||||
mapping(uint => AvailabilityStake) public stakes;
|
||||
mapping(address worker => uint stakeIndex) activeWorkerStakes;
|
||||
uint public stakeCount;
|
||||
|
||||
event AvailabilityStaked(uint stakeIndex);
|
||||
|
||||
constructor(DAO dao) DAOContract(dao) {}
|
||||
|
||||
/// Accept availability stakes as reputation token transfer
|
||||
function acceptAvailability(
|
||||
address worker,
|
||||
uint256 amount,
|
||||
uint duration
|
||||
) external returns (uint refund) {
|
||||
require(
|
||||
msg.sender == address(dao),
|
||||
"acceptAvailability must only be called by DAO contract"
|
||||
);
|
||||
require(amount > 0, "No stake provided");
|
||||
// If we already have a stake for this worker, replace it
|
||||
uint stakeIndex = activeWorkerStakes[worker];
|
||||
if (stakeIndex == 0 && stakes[stakeIndex].worker != worker) {
|
||||
// We don't have an existing stake for this worker
|
||||
stakeIndex = stakeCount++;
|
||||
activeWorkerStakes[worker] = stakeIndex;
|
||||
} else if (stakes[stakeIndex].assigned) {
|
||||
// Stake has already been assigned; We need to create a new one
|
||||
stakeIndex = stakeCount++;
|
||||
activeWorkerStakes[worker] = stakeIndex;
|
||||
} else {
|
||||
// We are replacing an existing stake.
|
||||
// That means we can refund some of the granted allowance
|
||||
refund = stakes[stakeIndex].amount;
|
||||
}
|
||||
AvailabilityStake storage stake = stakes[stakeIndex];
|
||||
stake.worker = worker;
|
||||
stake.amount = amount;
|
||||
stake.endTime = block.timestamp + duration;
|
||||
emit AvailabilityStaked(stakeIndex);
|
||||
}
|
||||
|
||||
/// Select a worker randomly from among the available workers, weighted by amount staked
|
||||
function randomWeightedSelection() internal view returns (uint stakeIndex) {
|
||||
uint totalStakes;
|
||||
for (uint i = 0; i < stakeCount; i++) {
|
||||
if (stakes[i].assigned) continue;
|
||||
if (block.timestamp > stakes[i].endTime) continue;
|
||||
totalStakes += stakes[i].amount;
|
||||
}
|
||||
require(totalStakes > 0, "No available worker stakes");
|
||||
uint select = block.prevrandao % totalStakes;
|
||||
uint acc;
|
||||
for (uint i = 0; i < stakeCount; i++) {
|
||||
if (stakes[i].assigned) continue;
|
||||
if (block.timestamp > stakes[i].endTime) continue;
|
||||
acc += stakes[i].amount;
|
||||
if (acc > select) {
|
||||
stakeIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Assign a random available worker
|
||||
function assignWork() internal returns (uint stakeIndex) {
|
||||
stakeIndex = randomWeightedSelection();
|
||||
AvailabilityStake storage stake = stakes[stakeIndex];
|
||||
stake.assigned = true;
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
// SPDX-License-Identifier: Unlicense
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
struct Reference {
|
||||
int weightPPM;
|
||||
string targetPostId;
|
||||
}
|
||||
|
||||
struct Author {
|
||||
uint weightPPM;
|
||||
address authorAddress;
|
||||
}
|
||||
|
||||
struct Post {
|
||||
string id;
|
||||
address sender;
|
||||
Author[] authors;
|
||||
Reference[] references;
|
||||
string content;
|
||||
}
|
||||
|
||||
contract GlobalForum {
|
||||
mapping(string => Post) posts;
|
||||
string[] public postIds;
|
||||
uint public postCount;
|
||||
|
||||
event PostAdded(string id);
|
||||
|
||||
function addPost(
|
||||
Author[] calldata authors,
|
||||
string calldata postId,
|
||||
Reference[] calldata references
|
||||
) external {
|
||||
require(authors.length > 0, "Post must include at least one author");
|
||||
postCount++;
|
||||
postIds.push(postId);
|
||||
Post storage post = posts[postId];
|
||||
require(
|
||||
post.authors.length == 0,
|
||||
"A post with this postId already exists"
|
||||
);
|
||||
post.sender = msg.sender;
|
||||
post.id = postId;
|
||||
uint authorTotalWeightPPM;
|
||||
for (uint i = 0; i < authors.length; i++) {
|
||||
authorTotalWeightPPM += authors[i].weightPPM;
|
||||
post.authors.push(authors[i]);
|
||||
}
|
||||
require(
|
||||
authorTotalWeightPPM == 1000000,
|
||||
"Author weights must sum to 1000000"
|
||||
);
|
||||
for (uint i = 0; i < references.length; i++) {
|
||||
post.references.push(references[i]);
|
||||
}
|
||||
int totalReferenceWeightPos;
|
||||
int totalReferenceWeightNeg;
|
||||
for (uint i = 0; i < post.references.length; i++) {
|
||||
int weight = post.references[i].weightPPM;
|
||||
require(
|
||||
weight >= -1000000,
|
||||
"Each reference weight must be >= -1000000"
|
||||
);
|
||||
require(
|
||||
weight <= 1000000,
|
||||
"Each reference weight must be <= 1000000"
|
||||
);
|
||||
if (weight > 0) totalReferenceWeightPos += weight;
|
||||
else totalReferenceWeightNeg += weight;
|
||||
}
|
||||
require(
|
||||
totalReferenceWeightPos <= 1000000,
|
||||
"Sum of positive references must be <= 1000000"
|
||||
);
|
||||
require(
|
||||
totalReferenceWeightNeg >= -1000000,
|
||||
"Sum of negative references must be >= -1000000"
|
||||
);
|
||||
emit PostAdded(postId);
|
||||
}
|
||||
|
||||
function getPostAuthors(
|
||||
string calldata postId
|
||||
) external view returns (Author[] memory) {
|
||||
Post storage post = posts[postId];
|
||||
return post.authors;
|
||||
}
|
||||
|
||||
function getPost(
|
||||
string calldata postId
|
||||
)
|
||||
external
|
||||
view
|
||||
returns (
|
||||
Author[] memory authors,
|
||||
Reference[] memory references,
|
||||
address sender
|
||||
)
|
||||
{
|
||||
Post storage post = posts[postId];
|
||||
authors = post.authors;
|
||||
references = post.references;
|
||||
sender = post.sender;
|
||||
}
|
||||
}
|
|
@ -2,16 +2,16 @@
|
|||
pragma solidity ^0.8.24;
|
||||
|
||||
import "./core/DAO.sol";
|
||||
import "./Work.sol";
|
||||
import "./core/Forum.sol";
|
||||
import "./WorkContract.sol";
|
||||
import "./interfaces/IOnValidate.sol";
|
||||
|
||||
contract Onboarding is Work, IOnValidate {
|
||||
contract Onboarding is WorkContract, IOnValidate {
|
||||
constructor(
|
||||
DAO dao,
|
||||
GlobalForum forum,
|
||||
Proposals proposals,
|
||||
uint price
|
||||
) Work(dao, forum, proposals, price) {}
|
||||
DAO dao_,
|
||||
Proposals proposals_,
|
||||
uint price_
|
||||
) WorkContract(dao_, proposals_, price_) {}
|
||||
|
||||
/// Accept work approval/disapproval from customer
|
||||
function submitWorkApproval(
|
||||
|
@ -29,13 +29,13 @@ contract Onboarding is Work, IOnValidate {
|
|||
// Make work evidence post
|
||||
Author[] memory authors = new Author[](1);
|
||||
authors[0] = Author(1000000, stake.worker);
|
||||
forum.addPost(authors, request.evidencePostId, request.references);
|
||||
dao.addPost(authors, request.evidenceContentId, request.citations);
|
||||
emit WorkApprovalSubmitted(requestIndex, approval);
|
||||
// Initiate validation pool
|
||||
uint poolIndex = dao.initiateValidationPool{
|
||||
value: request.fee - request.fee / 10
|
||||
}(
|
||||
request.evidencePostId,
|
||||
request.evidenceContentId,
|
||||
POOL_DURATION,
|
||||
[uint256(1), uint256(3)],
|
||||
[uint256(1), uint256(2)],
|
||||
|
@ -60,7 +60,7 @@ contract Onboarding is Work, IOnValidate {
|
|||
uint,
|
||||
uint,
|
||||
bytes calldata callbackData
|
||||
) external {
|
||||
) external returns (uint) {
|
||||
require(
|
||||
msg.sender == address(dao),
|
||||
"onValidate may only be called by the DAO contract"
|
||||
|
@ -70,15 +70,15 @@ contract Onboarding is Work, IOnValidate {
|
|||
if (!votePasses || !quorumMet) {
|
||||
// refund the customer the remaining amount
|
||||
payable(request.customer).transfer(request.fee / 10);
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
// Make onboarding post
|
||||
Reference[] memory emptyReferences;
|
||||
Citation[] memory emptyCitations;
|
||||
Author[] memory authors = new Author[](1);
|
||||
authors[0] = Author(1000000, request.customer);
|
||||
forum.addPost(authors, request.requestPostId, emptyReferences);
|
||||
dao.addPost(authors, request.requestContentId, emptyCitations);
|
||||
dao.initiateValidationPool{value: request.fee / 10}(
|
||||
request.requestPostId,
|
||||
request.requestContentId,
|
||||
POOL_DURATION,
|
||||
[uint256(1), uint256(3)],
|
||||
[uint256(1), uint256(2)],
|
||||
|
@ -87,5 +87,6 @@ contract Onboarding is Work, IOnValidate {
|
|||
false,
|
||||
""
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,17 +59,22 @@ contract Proposals is DAOContract, IOnValidate {
|
|||
|
||||
// TODO receive : we want to be able to accept refunds from validation pools
|
||||
|
||||
/// Submit a post as a proposal. forum.addPost should be called before this.
|
||||
function propose(
|
||||
string calldata postId,
|
||||
string calldata contentId,
|
||||
address author,
|
||||
uint[3] calldata durations,
|
||||
bool callbackOnAccepted,
|
||||
bytes calldata callbackData
|
||||
) external payable returns (uint proposalIndex) {
|
||||
// TODO: Take citations as a parameter
|
||||
Citation[] memory emptyCitations;
|
||||
Author[] memory authors = new Author[](1);
|
||||
authors[0] = Author(1000000, author);
|
||||
dao.addPost(authors, contentId, emptyCitations);
|
||||
proposalIndex = proposalCount++;
|
||||
Proposal storage proposal = proposals[proposalIndex];
|
||||
proposal.sender = msg.sender;
|
||||
proposal.postId = postId;
|
||||
proposal.postId = contentId;
|
||||
proposal.startTime = block.timestamp;
|
||||
proposal.referenda[0].duration = durations[0];
|
||||
proposal.referenda[1].duration = durations[1];
|
||||
|
@ -153,7 +158,7 @@ contract Proposals is DAOContract, IOnValidate {
|
|||
uint stakedFor,
|
||||
uint stakedAgainst,
|
||||
bytes calldata callbackData
|
||||
) external {
|
||||
) external returns (uint) {
|
||||
require(
|
||||
msg.sender == address(dao),
|
||||
"onValidate may only be called by the DAO contract"
|
||||
|
@ -177,7 +182,7 @@ contract Proposals is DAOContract, IOnValidate {
|
|||
proposal.stage = Stage.Failed;
|
||||
emit ProposalFailed(proposalIndex, "Quorum not met");
|
||||
proposal.remainingFee += fee;
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Participation threshold of 50%
|
||||
|
@ -238,7 +243,7 @@ contract Proposals is DAOContract, IOnValidate {
|
|||
} else if (proposal.stage == Stage.Referendum100) {
|
||||
initiateValidationPool(proposalIndex, 2, proposal.fee / 10);
|
||||
}
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// External function that will advance a proposal to the referendum process
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
// SPDX-License-Identifier: Unlicense
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "./Work.sol";
|
||||
import "./Rollup.sol";
|
||||
|
||||
abstract contract RollableWork is Work {
|
||||
Rollup immutable rollupContract;
|
||||
|
||||
constructor(
|
||||
DAO dao,
|
||||
GlobalForum forum,
|
||||
Proposals proposalsContract,
|
||||
Rollup rollupContract_,
|
||||
uint price
|
||||
) Work(dao, forum, proposalsContract, price) {
|
||||
rollupContract = rollupContract_;
|
||||
}
|
||||
|
||||
/// Accept work approval/disapproval from customer
|
||||
function submitWorkApproval(
|
||||
uint requestIndex,
|
||||
bool approval
|
||||
) external override {
|
||||
WorkRequest storage request = requests[requestIndex];
|
||||
require(
|
||||
request.status == WorkStatus.EvidenceSubmitted,
|
||||
"Status must be EvidenceSubmitted"
|
||||
);
|
||||
AvailabilityStake storage stake = stakes[request.stakeIndex];
|
||||
request.status = WorkStatus.ApprovalSubmitted;
|
||||
request.approval = approval;
|
||||
emit WorkApprovalSubmitted(requestIndex, approval);
|
||||
|
||||
// Make work evidence post
|
||||
Author[] memory authors = new Author[](1);
|
||||
authors[0] = Author(1000000, stake.worker);
|
||||
forum.addPost(authors, request.evidencePostId, request.references);
|
||||
|
||||
// send worker stakes and customer fee to rollup contract
|
||||
dao.forwardAllowance(
|
||||
stake.worker,
|
||||
address(rollupContract),
|
||||
stake.amount
|
||||
);
|
||||
rollupContract.addItem{value: request.fee}(
|
||||
stake.worker,
|
||||
stake.amount,
|
||||
request.evidencePostId
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
// SPDX-License-Identifier: Unlicense
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "./core/DAO.sol";
|
||||
import "./Availability.sol";
|
||||
|
||||
contract Rollup is Availability {
|
||||
struct BatchItem {
|
||||
address sender;
|
||||
address worker;
|
||||
uint stakeAmount;
|
||||
uint fee;
|
||||
string postId;
|
||||
}
|
||||
|
||||
mapping(uint => BatchItem) public items;
|
||||
uint public itemCount;
|
||||
address public batchWorker;
|
||||
uint batchWorkerStakeIndex;
|
||||
uint public immutable batchInterval;
|
||||
uint public batchStart;
|
||||
uint lastWorkerReset;
|
||||
uint constant minResetInterval = 120;
|
||||
|
||||
event BatchItemAdded(string postId, address sender, uint fee);
|
||||
event BatchWorkerAssigned(address batchWorker);
|
||||
|
||||
constructor(DAO dao, uint batchInterval_) Availability(dao) {
|
||||
batchInterval = batchInterval_;
|
||||
}
|
||||
|
||||
/// Instead of initiating a validation pool, call this method to include
|
||||
/// the stakes and fee in the next batch validation pool
|
||||
function addItem(
|
||||
address author,
|
||||
uint stakeAmount,
|
||||
string calldata postId
|
||||
) public payable {
|
||||
BatchItem storage item = items[itemCount++];
|
||||
item.sender = msg.sender;
|
||||
item.worker = author;
|
||||
item.stakeAmount = stakeAmount;
|
||||
item.fee = msg.value;
|
||||
item.postId = postId;
|
||||
emit BatchItemAdded(postId, item.sender, item.fee);
|
||||
}
|
||||
|
||||
/// To be called by the currently assigned batch worker,
|
||||
/// If no batch worker has been assigned this may be called by anybody,
|
||||
/// but it will only succeed if it is able to assign a new worker.
|
||||
function submitBatch(
|
||||
string calldata batchPostId,
|
||||
string[] calldata batchItems,
|
||||
uint poolDuration
|
||||
) public returns (uint poolIndex) {
|
||||
if (batchWorker != address(0)) {
|
||||
require(
|
||||
msg.sender == batchWorker,
|
||||
"Batch result must be submitted by current batch worker"
|
||||
);
|
||||
}
|
||||
require(batchItems.length <= itemCount, "Batch size too large");
|
||||
// Make sure all batch items match
|
||||
for (uint i = 0; i < batchItems.length; i++) {
|
||||
require(
|
||||
keccak256(bytes(batchItems[i])) ==
|
||||
keccak256(bytes(items[i].postId)),
|
||||
"Batch item mismatch"
|
||||
);
|
||||
}
|
||||
// initiate a validation pool for this batch
|
||||
uint fee;
|
||||
for (uint i = 0; i < batchItems.length; i++) {
|
||||
fee += items[i].fee;
|
||||
}
|
||||
poolIndex = dao.initiateValidationPool{value: fee}(
|
||||
batchPostId,
|
||||
poolDuration,
|
||||
[uint256(1), uint256(3)],
|
||||
[uint256(1), uint256(2)],
|
||||
100,
|
||||
true,
|
||||
false,
|
||||
""
|
||||
);
|
||||
// Include all the availability stakes from the batched work
|
||||
for (uint i = 0; i < batchItems.length; i++) {
|
||||
dao.delegatedStakeOnValidationPool(
|
||||
poolIndex,
|
||||
items[i].worker,
|
||||
items[i].stakeAmount,
|
||||
true
|
||||
);
|
||||
}
|
||||
// Include availability stakes from the batch worker
|
||||
if (batchWorker != address(0)) {
|
||||
dao.delegatedStakeOnValidationPool(
|
||||
poolIndex,
|
||||
batchWorker,
|
||||
stakes[batchWorkerStakeIndex].amount,
|
||||
true
|
||||
);
|
||||
}
|
||||
if (batchItems.length < itemCount) {
|
||||
// Some items were added after this batch was computed.
|
||||
// Keep them in the queue to be included in the next batch.
|
||||
for (uint i = 0; i < itemCount - batchItems.length; i++) {
|
||||
items[i] = items[batchItems.length + i];
|
||||
}
|
||||
itemCount = itemCount - batchItems.length;
|
||||
} else {
|
||||
// Reset item count so we can start the next batch
|
||||
itemCount = 0;
|
||||
}
|
||||
// Select the next batch worker
|
||||
batchWorkerStakeIndex = assignWork();
|
||||
batchWorker = stakes[batchWorkerStakeIndex].worker;
|
||||
batchStart = block.timestamp;
|
||||
emit BatchWorkerAssigned(batchWorker);
|
||||
}
|
||||
|
||||
/// If the batch worker fails to submit the batch, a new batch worker may be selected
|
||||
function resetBatchWorker() public {
|
||||
// TODO: Grace period after the current batch is due and before the worker can be replaced
|
||||
require(
|
||||
block.timestamp - batchStart > batchInterval,
|
||||
"Current batch interval has not yet elapsed"
|
||||
);
|
||||
require(itemCount > 0, "Current batch is empty");
|
||||
require(
|
||||
lastWorkerReset == 0 ||
|
||||
block.timestamp - lastWorkerReset >= minResetInterval,
|
||||
"Mininum reset interval has not elapsed since last batch worker reset"
|
||||
);
|
||||
// TODO: Submit a validation pool targeting a null post, and send the worker's availability stake
|
||||
// This gives the DAO an opportunity to police the failed work
|
||||
// Select a new batch worker
|
||||
batchWorkerStakeIndex = assignWork();
|
||||
batchWorker = stakes[batchWorkerStakeIndex].worker;
|
||||
}
|
||||
}
|
|
@ -2,14 +2,13 @@
|
|||
pragma solidity ^0.8.24;
|
||||
|
||||
import "./core/DAO.sol";
|
||||
import "./Work.sol";
|
||||
import "./WorkContract.sol";
|
||||
import "./Proposals.sol";
|
||||
|
||||
contract Work1 is Work {
|
||||
contract Work1 is WorkContract {
|
||||
constructor(
|
||||
DAO dao,
|
||||
GlobalForum forum,
|
||||
Proposals proposals,
|
||||
uint price
|
||||
) Work(dao, forum, proposals, price) {}
|
||||
DAO dao_,
|
||||
Proposals proposals_,
|
||||
uint price_
|
||||
) WorkContract(dao_, proposals_, price_) {}
|
||||
}
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
// 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,
|
||||
GlobalForum forum,
|
||||
Proposals proposals,
|
||||
Rollup rollup,
|
||||
uint price
|
||||
) RollableWork(dao, forum, proposals, rollup, price) {}
|
||||
}
|
|
@ -2,11 +2,23 @@
|
|||
pragma solidity ^0.8.24;
|
||||
|
||||
import "./core/DAO.sol";
|
||||
import "./Availability.sol";
|
||||
import "./core/Forum.sol";
|
||||
import "./Proposals.sol";
|
||||
import "./interfaces/IAcceptAvailability.sol";
|
||||
import "./interfaces/IOnProposalAccepted.sol";
|
||||
|
||||
abstract contract Work is Availability, IOnProposalAccepted {
|
||||
abstract contract WorkContract is
|
||||
DAOContract,
|
||||
IAcceptAvailability,
|
||||
IOnProposalAccepted
|
||||
{
|
||||
struct AvailabilityStake {
|
||||
address worker;
|
||||
uint256 amount;
|
||||
uint endTime;
|
||||
bool assigned;
|
||||
}
|
||||
|
||||
enum WorkStatus {
|
||||
Requested,
|
||||
EvidenceSubmitted,
|
||||
|
@ -19,9 +31,9 @@ abstract contract Work is Availability, IOnProposalAccepted {
|
|||
uint256 fee;
|
||||
WorkStatus status;
|
||||
uint stakeIndex;
|
||||
string requestPostId;
|
||||
string evidencePostId;
|
||||
Reference[] references;
|
||||
string requestContentId;
|
||||
string evidenceContentId;
|
||||
Citation[] citations;
|
||||
bool approval;
|
||||
}
|
||||
|
||||
|
@ -30,16 +42,18 @@ abstract contract Work is Availability, IOnProposalAccepted {
|
|||
uint proposalIndex;
|
||||
}
|
||||
|
||||
GlobalForum forum;
|
||||
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;
|
||||
uint public requestCount;
|
||||
|
||||
uint constant POOL_DURATION = 20;
|
||||
|
||||
event AvailabilityStaked(uint stakeIndex);
|
||||
event WorkAssigned(uint requestIndex, uint stakeIndex);
|
||||
event WorkEvidenceSubmitted(uint requestIndex);
|
||||
event WorkApprovalSubmitted(uint requestIndex, bool approval);
|
||||
|
@ -48,32 +62,89 @@ abstract contract Work is Availability, IOnProposalAccepted {
|
|||
|
||||
constructor(
|
||||
DAO dao,
|
||||
GlobalForum forum_,
|
||||
Proposals proposalsContract_,
|
||||
uint price_
|
||||
) Availability(dao) {
|
||||
) DAOContract(dao) {
|
||||
price = price_;
|
||||
proposalsContract = proposalsContract_;
|
||||
forum = forum_;
|
||||
}
|
||||
|
||||
/// Accept availability stakes as reputation token transfer
|
||||
function acceptAvailability(
|
||||
address sender,
|
||||
uint256 amount,
|
||||
uint duration
|
||||
) external {
|
||||
require(amount > 0, "No stake provided");
|
||||
uint stakeIndex = stakeCount++;
|
||||
AvailabilityStake storage stake = stakes[stakeIndex];
|
||||
stake.worker = sender;
|
||||
stake.amount = amount;
|
||||
stake.endTime = block.timestamp + duration;
|
||||
emit AvailabilityStaked(stakeIndex);
|
||||
}
|
||||
|
||||
function extendAvailability(uint stakeIndex, uint duration) external {
|
||||
AvailabilityStake storage stake = stakes[stakeIndex];
|
||||
require(
|
||||
msg.sender == stake.worker,
|
||||
"Worker can only extend their own availability stake"
|
||||
);
|
||||
require(!stake.assigned, "Stake has already been assigned work");
|
||||
if (block.timestamp > stake.endTime) {
|
||||
stake.endTime = block.timestamp + duration;
|
||||
} else {
|
||||
stake.endTime = stake.endTime + duration;
|
||||
}
|
||||
emit AvailabilityStaked(stakeIndex);
|
||||
}
|
||||
|
||||
/// Select a worker randomly from among the available workers, weighted by amount staked
|
||||
function randomWeightedSelection() internal view returns (uint stakeIndex) {
|
||||
uint totalStakes;
|
||||
for (uint i = 0; i < stakeCount; i++) {
|
||||
if (stakes[i].assigned) continue;
|
||||
if (block.timestamp > stakes[i].endTime) continue;
|
||||
totalStakes += stakes[i].amount;
|
||||
}
|
||||
require(totalStakes > 0, "No available worker stakes");
|
||||
uint select = block.prevrandao % totalStakes;
|
||||
uint acc;
|
||||
for (uint i = 0; i < stakeCount; i++) {
|
||||
if (stakes[i].assigned) continue;
|
||||
if (block.timestamp > stakes[i].endTime) continue;
|
||||
acc += stakes[i].amount;
|
||||
if (acc > select) {
|
||||
stakeIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Assign a random available worker
|
||||
function assignWork(uint requestIndex) internal returns (uint stakeIndex) {
|
||||
stakeIndex = randomWeightedSelection();
|
||||
AvailabilityStake storage stake = stakes[stakeIndex];
|
||||
stake.assigned = true;
|
||||
emit WorkAssigned(requestIndex, stakeIndex);
|
||||
}
|
||||
|
||||
/// Accept work request with fee
|
||||
function requestWork(string calldata requestPostId) 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();
|
||||
request.requestPostId = requestPostId;
|
||||
emit WorkAssigned(requestIndex, request.stakeIndex);
|
||||
request.stakeIndex = assignWork(requestIndex);
|
||||
request.requestContentId = requestContentId;
|
||||
}
|
||||
|
||||
/// Accept work evidence from worker
|
||||
function submitWorkEvidence(
|
||||
uint requestIndex,
|
||||
string calldata evidencePostId,
|
||||
Reference[] calldata references
|
||||
string calldata evidenceContentId,
|
||||
Citation[] calldata citations
|
||||
) external {
|
||||
WorkRequest storage request = requests[requestIndex];
|
||||
require(
|
||||
|
@ -86,9 +157,9 @@ abstract contract Work is Availability, IOnProposalAccepted {
|
|||
"Worker can only submit evidence for work they are assigned"
|
||||
);
|
||||
request.status = WorkStatus.EvidenceSubmitted;
|
||||
request.evidencePostId = evidencePostId;
|
||||
for (uint i = 0; i < references.length; i++) {
|
||||
request.references.push(references[i]);
|
||||
request.evidenceContentId = evidenceContentId;
|
||||
for (uint i = 0; i < citations.length; i++) {
|
||||
request.citations.push(citations[i]);
|
||||
}
|
||||
emit WorkEvidenceSubmitted(requestIndex);
|
||||
}
|
||||
|
@ -109,11 +180,11 @@ abstract contract Work is Availability, IOnProposalAccepted {
|
|||
// Make work evidence post
|
||||
Author[] memory authors = new Author[](1);
|
||||
authors[0] = Author(1000000, stake.worker);
|
||||
forum.addPost(authors, request.evidencePostId, request.references);
|
||||
dao.addPost(authors, request.evidenceContentId, request.citations);
|
||||
emit WorkApprovalSubmitted(requestIndex, approval);
|
||||
// Initiate validation pool
|
||||
uint poolIndex = dao.initiateValidationPool{value: request.fee}(
|
||||
request.evidencePostId,
|
||||
request.evidenceContentId,
|
||||
POOL_DURATION,
|
||||
[uint256(1), uint256(3)],
|
||||
[uint256(1), uint256(2)],
|
||||
|
@ -131,11 +202,9 @@ abstract contract Work is Availability, IOnProposalAccepted {
|
|||
);
|
||||
}
|
||||
|
||||
/// Initiate a new proposal to change the price for this work contract.
|
||||
/// This takes a postId; DAO.addPost should be called before or concurrently with this.
|
||||
function proposeNewPrice(
|
||||
uint newPrice,
|
||||
string calldata postId,
|
||||
string calldata contentId,
|
||||
uint[3] calldata durations
|
||||
) external payable {
|
||||
uint priceProposalIndex = priceProposalCount++;
|
||||
|
@ -145,7 +214,13 @@ abstract contract Work is Availability, IOnProposalAccepted {
|
|||
priceProposal.price = newPrice;
|
||||
priceProposal.proposalIndex = proposalsContract.propose{
|
||||
value: msg.value
|
||||
}(postId, durations, true, abi.encode(priceProposalIndex));
|
||||
}(
|
||||
contentId,
|
||||
msg.sender,
|
||||
durations,
|
||||
true,
|
||||
abi.encode(priceProposalIndex)
|
||||
);
|
||||
emit PriceChangeProposed(priceProposalIndex);
|
||||
}
|
||||
|
|
@ -1,419 +0,0 @@
|
|||
// SPDX-License-Identifier: Unlicense
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "./DAO.sol";
|
||||
import "../GlobalForum.sol";
|
||||
|
||||
struct ValidationPoolParams {
|
||||
uint duration;
|
||||
uint[2] quorum; // [ Numerator, Denominator ]
|
||||
uint[2] winRatio; // [ Numerator, Denominator ]
|
||||
uint bindingPercent;
|
||||
bool redistributeLosingStakes;
|
||||
}
|
||||
|
||||
struct ValidationPoolProps {
|
||||
string postId;
|
||||
uint fee;
|
||||
uint minted;
|
||||
uint endTime;
|
||||
bool resolved;
|
||||
bool outcome;
|
||||
}
|
||||
|
||||
contract Bench {
|
||||
struct Stake {
|
||||
uint id;
|
||||
bool inFavor;
|
||||
uint amount;
|
||||
address sender;
|
||||
}
|
||||
|
||||
struct Pool {
|
||||
uint id;
|
||||
address sender;
|
||||
mapping(uint => Stake) stakes;
|
||||
uint stakeCount;
|
||||
ValidationPoolParams params;
|
||||
ValidationPoolProps props;
|
||||
bool callbackOnValidate;
|
||||
bytes callbackData;
|
||||
}
|
||||
|
||||
mapping(uint => Pool) public validationPools;
|
||||
uint public validationPoolCount;
|
||||
DAO dao;
|
||||
GlobalForum forum;
|
||||
|
||||
// Validation Pool parameters
|
||||
uint constant minDuration = 1; // 1 second
|
||||
uint constant maxDuration = 365_000_000 days; // 1 million years
|
||||
uint[2] minQuorum = [1, 10];
|
||||
|
||||
// Forum parameters
|
||||
// TODO: Make depth limit configurable; take as param
|
||||
uint depthLimit = 3;
|
||||
|
||||
mapping(string => mapping(string => int)) _edgeBalances;
|
||||
|
||||
function registerDAO(DAO dao_, GlobalForum forum_) external {
|
||||
require(
|
||||
address(dao) == address(0),
|
||||
"A DAO has already been registered"
|
||||
);
|
||||
dao = dao_;
|
||||
forum = forum_;
|
||||
}
|
||||
|
||||
/// Register a stake for/against a validation pool
|
||||
function stakeOnValidationPool(
|
||||
uint poolIndex,
|
||||
address sender,
|
||||
uint256 amount,
|
||||
bool inFavor
|
||||
) external {
|
||||
require(
|
||||
msg.sender == address(dao),
|
||||
"Only DAO contract may call stakeOnValidationPool"
|
||||
);
|
||||
Pool storage pool = validationPools[poolIndex];
|
||||
require(
|
||||
block.timestamp <= pool.props.endTime,
|
||||
"Pool end time has passed"
|
||||
);
|
||||
// We don't call _update here; We defer that until evaluateOutcome.
|
||||
uint stakeIndex = pool.stakeCount++;
|
||||
Stake storage s = pool.stakes[stakeIndex];
|
||||
s.sender = sender;
|
||||
s.inFavor = inFavor;
|
||||
s.amount = amount;
|
||||
s.id = stakeIndex;
|
||||
}
|
||||
|
||||
/// Accept fee to initiate a validation pool
|
||||
function initiateValidationPool(
|
||||
address sender,
|
||||
string calldata postId,
|
||||
uint duration,
|
||||
uint[2] calldata quorum, // [Numerator, Denominator]
|
||||
uint[2] calldata winRatio, // [Numerator, Denominator]
|
||||
uint bindingPercent,
|
||||
bool redistributeLosingStakes,
|
||||
bool callbackOnValidate,
|
||||
bytes calldata callbackData
|
||||
) external payable returns (uint poolIndex) {
|
||||
require(
|
||||
msg.sender == address(dao),
|
||||
"Only DAO contract may call initiateValidationPool"
|
||||
);
|
||||
require(duration >= minDuration, "Duration is too short");
|
||||
require(duration <= maxDuration, "Duration is too long");
|
||||
require(
|
||||
minQuorum[1] * quorum[0] >= minQuorum[0] * quorum[1],
|
||||
"Quorum is below minimum"
|
||||
);
|
||||
require(quorum[0] <= quorum[1], "Quorum is greater than one");
|
||||
require(winRatio[0] <= winRatio[1], "Win ratio is greater than one");
|
||||
require(bindingPercent <= 100, "Binding percent must be <= 100");
|
||||
poolIndex = validationPoolCount++;
|
||||
Pool storage pool = validationPools[poolIndex];
|
||||
pool.id = poolIndex;
|
||||
pool.sender = sender;
|
||||
pool.props.postId = postId;
|
||||
pool.props.fee = msg.value;
|
||||
pool.props.endTime = block.timestamp + duration;
|
||||
pool.params.quorum = quorum;
|
||||
pool.params.winRatio = winRatio;
|
||||
pool.params.bindingPercent = bindingPercent;
|
||||
pool.params.redistributeLosingStakes = redistributeLosingStakes;
|
||||
pool.params.duration = duration;
|
||||
pool.callbackOnValidate = callbackOnValidate;
|
||||
pool.callbackData = callbackData;
|
||||
// We use our privilege as the DAO contract to mint reputation in proportion with the fee.
|
||||
// Here we assume a minting ratio of 1
|
||||
// TODO: Make minting ratio an adjustable parameter
|
||||
dao.mint(address(dao), pool.props.fee);
|
||||
pool.props.minted = msg.value;
|
||||
dao.emitValidationPoolInitiated(poolIndex);
|
||||
}
|
||||
|
||||
/// Evaluate outcome of a validation pool
|
||||
function evaluateOutcome(uint poolIndex) public returns (bool votePasses) {
|
||||
require(
|
||||
msg.sender == address(dao),
|
||||
"Only DAO contract may call evaluateOutcome"
|
||||
);
|
||||
Pool storage pool = validationPools[poolIndex];
|
||||
require(pool.props.resolved == false, "Pool is already resolved");
|
||||
uint stakedFor;
|
||||
uint stakedAgainst;
|
||||
Stake storage s;
|
||||
for (uint i = 0; i < pool.stakeCount; i++) {
|
||||
s = pool.stakes[i];
|
||||
// Make sure the sender still has the required balance.
|
||||
// If not, automatically decrease the staked amount.
|
||||
if (dao.balanceOf(s.sender) < s.amount) {
|
||||
s.amount = dao.balanceOf(s.sender);
|
||||
}
|
||||
if (s.inFavor) {
|
||||
stakedFor += s.amount;
|
||||
} else {
|
||||
stakedAgainst += s.amount;
|
||||
}
|
||||
}
|
||||
stakedFor += pool.props.minted / 2;
|
||||
stakedAgainst += pool.props.minted / 2;
|
||||
if (pool.props.minted % 2 != 0) {
|
||||
stakedFor += 1;
|
||||
}
|
||||
// Special case for early evaluation if dao.totalSupply has been staked
|
||||
require(
|
||||
block.timestamp > pool.props.endTime ||
|
||||
stakedFor + stakedAgainst == dao.totalSupply(),
|
||||
"Pool end time has not yet arrived"
|
||||
);
|
||||
// Check that quorum is met
|
||||
if (
|
||||
pool.params.quorum[1] * (stakedFor + stakedAgainst) <=
|
||||
dao.totalSupply() * pool.params.quorum[0]
|
||||
) {
|
||||
// TODO: Refund fee
|
||||
// TODO: this could be made available for the sender to withdraw
|
||||
// payable(pool.sender).transfer(pool.props.fee);
|
||||
pool.props.resolved = true;
|
||||
dao.emitValidationPoolResolved(poolIndex, false, false);
|
||||
// Callback if requested
|
||||
if (pool.callbackOnValidate) {
|
||||
dao.onValidate(
|
||||
pool.sender,
|
||||
votePasses,
|
||||
false,
|
||||
stakedFor,
|
||||
stakedAgainst,
|
||||
pool.callbackData
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
// A tie is resolved in favor of the validation pool.
|
||||
// This is especially important so that the DAO's first pool can pass,
|
||||
// when no reputation has yet been minted.
|
||||
votePasses =
|
||||
stakedFor * pool.params.winRatio[1] >=
|
||||
(stakedFor + stakedAgainst) * pool.params.winRatio[0];
|
||||
pool.props.resolved = true;
|
||||
pool.props.outcome = votePasses;
|
||||
dao.emitValidationPoolResolved(poolIndex, votePasses, true);
|
||||
|
||||
// Value of losing stakes should be distributed among winners, in proportion to their stakes
|
||||
// Only bindingPercent % should be redistributed
|
||||
// Stake senders should get (1000000-bindingPercent) % back
|
||||
uint amountFromWinners = votePasses ? stakedFor : stakedAgainst;
|
||||
uint totalRewards;
|
||||
uint totalAllocated;
|
||||
for (uint i = 0; i < pool.stakeCount; i++) {
|
||||
s = pool.stakes[i];
|
||||
if (votePasses != s.inFavor) {
|
||||
// Losing stake
|
||||
uint amount = (s.amount * pool.params.bindingPercent) / 100;
|
||||
if (pool.params.redistributeLosingStakes) {
|
||||
dao.update(s.sender, address(dao), amount);
|
||||
totalRewards += amount;
|
||||
} else {
|
||||
dao.burn(s.sender, amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (votePasses) {
|
||||
// If vote passes, reward the author as though they had staked the winning portion of the VP initial stake
|
||||
// Here we assume a stakeForAuthor ratio of 0.5
|
||||
// TODO: Make stakeForAuthor an adjustable parameter
|
||||
totalRewards += pool.props.minted / 2;
|
||||
// Include the losign portion of the VP initial stake
|
||||
// Issue rewards to the winners
|
||||
for (uint i = 0; i < pool.stakeCount; i++) {
|
||||
s = pool.stakes[i];
|
||||
if (
|
||||
pool.params.redistributeLosingStakes &&
|
||||
votePasses == s.inFavor
|
||||
) {
|
||||
// Winning stake
|
||||
uint reward = (((totalRewards * s.amount) /
|
||||
amountFromWinners) * pool.params.bindingPercent) / 100;
|
||||
totalAllocated += reward;
|
||||
dao.update(address(dao), s.sender, reward);
|
||||
}
|
||||
}
|
||||
// Due to rounding, there may be some excess REP. Award it to the author.
|
||||
uint remainder = totalRewards - totalAllocated;
|
||||
if (pool.props.minted % 2 != 0) {
|
||||
// We staked the odd remainder in favor of the post, on behalf of the author.
|
||||
remainder += 1;
|
||||
}
|
||||
|
||||
// Transfer REP to the forum instead of to the author directly
|
||||
propagateReputation(
|
||||
pool.props.postId,
|
||||
int(pool.props.minted / 2 + remainder),
|
||||
false,
|
||||
0
|
||||
);
|
||||
} else {
|
||||
// If vote does not pass, divide the losing stake among the winners
|
||||
totalRewards += pool.props.minted;
|
||||
for (uint i = 0; i < pool.stakeCount; i++) {
|
||||
s = pool.stakes[i];
|
||||
if (
|
||||
pool.params.redistributeLosingStakes &&
|
||||
votePasses == s.inFavor
|
||||
) {
|
||||
// Winning stake
|
||||
uint reward = (((totalRewards * s.amount) /
|
||||
(amountFromWinners - pool.props.minted / 2)) *
|
||||
pool.params.bindingPercent) / 100;
|
||||
totalAllocated += reward;
|
||||
dao.update(address(dao), s.sender, reward);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Distribute fee proportionately among all reputation holders
|
||||
dao.distributeFeeAmongMembers{value: pool.props.fee}();
|
||||
|
||||
// Callback if requested
|
||||
if (pool.callbackOnValidate) {
|
||||
dao.onValidate(
|
||||
pool.sender,
|
||||
votePasses,
|
||||
true,
|
||||
stakedFor,
|
||||
stakedAgainst,
|
||||
pool.callbackData
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function _handleReference(
|
||||
string memory postId,
|
||||
Reference memory ref,
|
||||
int amount,
|
||||
bool initialNegative,
|
||||
uint depth
|
||||
) internal returns (int outboundAmount) {
|
||||
outboundAmount = (amount * ref.weightPPM) / 1000000;
|
||||
if (bytes(ref.targetPostId).length == 0) {
|
||||
// Incineration
|
||||
require(
|
||||
outboundAmount >= 0,
|
||||
"Leaching from incinerator is forbidden"
|
||||
);
|
||||
dao.burn(address(dao), uint(outboundAmount));
|
||||
return outboundAmount;
|
||||
}
|
||||
int balanceToOutbound = _edgeBalances[postId][ref.targetPostId];
|
||||
if (initialNegative) {
|
||||
if (outboundAmount < 0) {
|
||||
outboundAmount = outboundAmount > -balanceToOutbound
|
||||
? outboundAmount
|
||||
: -balanceToOutbound;
|
||||
} else {
|
||||
outboundAmount = outboundAmount < -balanceToOutbound
|
||||
? outboundAmount
|
||||
: -balanceToOutbound;
|
||||
}
|
||||
}
|
||||
int refund = propagateReputation(
|
||||
ref.targetPostId,
|
||||
outboundAmount,
|
||||
initialNegative || (depth == 0 && ref.weightPPM < 0),
|
||||
depth + 1
|
||||
);
|
||||
outboundAmount -= refund;
|
||||
_edgeBalances[postId][ref.targetPostId] += outboundAmount;
|
||||
}
|
||||
|
||||
function _distributeAmongAuthors(
|
||||
Author[] memory authors,
|
||||
int amount
|
||||
) internal returns (int refund) {
|
||||
int allocated;
|
||||
|
||||
for (uint i = 0; i < authors.length; i++) {
|
||||
dao.registerMember(authors[i].authorAddress);
|
||||
}
|
||||
for (uint i = 0; i < authors.length; i++) {
|
||||
Author memory author = authors[i];
|
||||
int share;
|
||||
if (i < authors.length - 1) {
|
||||
share = (amount * int(author.weightPPM)) / 1000000;
|
||||
allocated += share;
|
||||
} else {
|
||||
// For the last author, allocate the remainder.
|
||||
share = amount - allocated;
|
||||
}
|
||||
if (share > 0) {
|
||||
dao.update(address(dao), author.authorAddress, uint(share));
|
||||
} else if (dao.balanceOf(author.authorAddress) < uint(-share)) {
|
||||
// Author has already lost some REP gained from this post.
|
||||
// That means other DAO members have earned it for policing.
|
||||
// We need to refund the difference here to ensure accurate bookkeeping
|
||||
uint authorBalance = dao.balanceOf(author.authorAddress);
|
||||
refund += share + int(authorBalance);
|
||||
dao.update(
|
||||
author.authorAddress,
|
||||
address(dao),
|
||||
dao.balanceOf(author.authorAddress)
|
||||
);
|
||||
} else {
|
||||
dao.update(author.authorAddress, address(dao), uint(-share));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function propagateReputation(
|
||||
string memory postId,
|
||||
int amount,
|
||||
bool initialNegative,
|
||||
uint depth
|
||||
) internal returns (int refundToInbound) {
|
||||
if (depth >= depthLimit) {
|
||||
return amount;
|
||||
}
|
||||
Reference[] memory references;
|
||||
Author[] memory authors;
|
||||
address sender;
|
||||
(authors, references, sender) = forum.getPost(postId);
|
||||
if (authors.length == 0) {
|
||||
// We most likely got here via a reference to a post that hasn't been added yet.
|
||||
// We support this scenario so that a reference graph can be imported one post at a time.
|
||||
return amount;
|
||||
}
|
||||
// Propagate negative references first
|
||||
for (uint i = 0; i < references.length; i++) {
|
||||
if (references[i].weightPPM < 0) {
|
||||
amount -= _handleReference(
|
||||
postId,
|
||||
references[i],
|
||||
amount,
|
||||
initialNegative,
|
||||
depth
|
||||
);
|
||||
}
|
||||
}
|
||||
// Now propagate positive references
|
||||
for (uint i = 0; i < references.length; i++) {
|
||||
if (references[i].weightPPM > 0) {
|
||||
amount -= _handleReference(
|
||||
postId,
|
||||
references[i],
|
||||
amount,
|
||||
initialNegative,
|
||||
depth
|
||||
);
|
||||
}
|
||||
}
|
||||
refundToInbound = _distributeAmongAuthors(authors, amount);
|
||||
}
|
||||
}
|
|
@ -3,381 +3,21 @@ pragma solidity ^0.8.24;
|
|||
|
||||
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
import "./Reputation.sol";
|
||||
import "./Bench.sol";
|
||||
import "./LightweightBench.sol";
|
||||
import "../GlobalForum.sol";
|
||||
import "./ValidationPools.sol";
|
||||
import "./Forum.sol";
|
||||
import "../interfaces/IAcceptAvailability.sol";
|
||||
import "../interfaces/IOnValidate.sol";
|
||||
|
||||
contract DAO {
|
||||
Reputation rep;
|
||||
GlobalForum forum;
|
||||
Bench bench;
|
||||
LightweightBench lightweightBench;
|
||||
mapping(uint => address) public members;
|
||||
uint public memberCount;
|
||||
mapping(address => bool) public isMember;
|
||||
|
||||
event PostAdded(string id);
|
||||
event ValidationPoolInitiated(uint poolIndex);
|
||||
event ValidationPoolResolved(
|
||||
uint poolIndex,
|
||||
bool votePasses,
|
||||
bool quorumMet
|
||||
);
|
||||
event LWValidationPoolInitiated(uint poolIndex);
|
||||
event LWValidationPoolResolved(
|
||||
uint poolIndex,
|
||||
bool votePasses,
|
||||
bool quorumMet
|
||||
);
|
||||
event LWResultProposed(
|
||||
uint poolIndex,
|
||||
uint proposedResultIndex,
|
||||
string proposedResultHash
|
||||
);
|
||||
|
||||
constructor(
|
||||
Reputation reputation_,
|
||||
Bench bench_,
|
||||
LightweightBench lightweightBench_,
|
||||
GlobalForum forum_
|
||||
) {
|
||||
rep = reputation_;
|
||||
bench = bench_;
|
||||
lightweightBench = lightweightBench_;
|
||||
forum = forum_;
|
||||
rep.registerDAO(this);
|
||||
bench.registerDAO(this, forum);
|
||||
lightweightBench.registerDAO(this);
|
||||
}
|
||||
|
||||
function emitPostAdded(string calldata id) public {
|
||||
emit PostAdded(id);
|
||||
}
|
||||
|
||||
function emitValidationPoolInitiated(uint poolIndex) public {
|
||||
emit ValidationPoolInitiated(poolIndex);
|
||||
}
|
||||
|
||||
function emitValidationPoolResolved(
|
||||
uint poolIndex,
|
||||
bool votePasses,
|
||||
bool quorumMet
|
||||
) public {
|
||||
emit ValidationPoolResolved(poolIndex, votePasses, quorumMet);
|
||||
}
|
||||
|
||||
function emitLWValidationPoolInitiated(uint poolIndex) public {
|
||||
emit LWValidationPoolInitiated(poolIndex);
|
||||
}
|
||||
|
||||
function emitLWResultProposed(
|
||||
uint poolIndex,
|
||||
uint proposedResultIndex,
|
||||
string calldata proposedResultHash
|
||||
) public {
|
||||
emit LWResultProposed(
|
||||
poolIndex,
|
||||
proposedResultIndex,
|
||||
proposedResultHash
|
||||
);
|
||||
}
|
||||
|
||||
function update(address from, address to, uint256 value) public {
|
||||
require(
|
||||
msg.sender == address(lightweightBench) ||
|
||||
msg.sender == address(bench),
|
||||
"Only DAO core contracts may call update"
|
||||
);
|
||||
rep.update(from, to, value);
|
||||
}
|
||||
|
||||
function mint(address account, uint256 value) public {
|
||||
require(
|
||||
msg.sender == address(lightweightBench) ||
|
||||
msg.sender == address(bench),
|
||||
"Only DAO core contracts may call mint"
|
||||
);
|
||||
rep.mint(account, value);
|
||||
}
|
||||
|
||||
function burn(address account, uint256 value) public {
|
||||
require(
|
||||
msg.sender == address(lightweightBench) ||
|
||||
msg.sender == address(bench),
|
||||
"Only DAO core contracts may call burn"
|
||||
);
|
||||
rep.burn(account, value);
|
||||
}
|
||||
|
||||
function registerMember(address account) public {
|
||||
require(
|
||||
msg.sender == address(lightweightBench) ||
|
||||
msg.sender == address(bench),
|
||||
"Only DAO core contracts may call registerMember"
|
||||
);
|
||||
if (!isMember[account]) {
|
||||
members[memberCount++] = account;
|
||||
isMember[account] = true;
|
||||
}
|
||||
}
|
||||
|
||||
function balanceOf(address account) public view returns (uint256) {
|
||||
return rep.balanceOf(account);
|
||||
}
|
||||
|
||||
function totalSupply() public view returns (uint256) {
|
||||
return rep.totalSupply();
|
||||
}
|
||||
|
||||
function allowance(
|
||||
address owner,
|
||||
address spender
|
||||
) public view returns (uint256) {
|
||||
return rep.allowance(owner, spender);
|
||||
}
|
||||
|
||||
function forwardAllowance(
|
||||
address owner,
|
||||
address to,
|
||||
uint256 amount
|
||||
) public {
|
||||
rep.spendAllowance(owner, msg.sender, amount);
|
||||
rep.approve(owner, to, rep.allowance(owner, to) + amount);
|
||||
}
|
||||
|
||||
contract DAO is Reputation, Forum, ValidationPools {
|
||||
/// Authorize a contract to transfer REP, and call that contract's acceptAvailability method
|
||||
function stakeAvailability(
|
||||
address to,
|
||||
uint256 value,
|
||||
uint duration
|
||||
) external returns (bool) {
|
||||
uint refund = IAcceptAvailability(to).acceptAvailability(
|
||||
msg.sender,
|
||||
value,
|
||||
duration
|
||||
);
|
||||
rep.approve(
|
||||
msg.sender,
|
||||
to,
|
||||
rep.allowance(msg.sender, to) + value - refund
|
||||
);
|
||||
_approve(msg.sender, to, value);
|
||||
IAcceptAvailability(to).acceptAvailability(msg.sender, value, duration);
|
||||
return true;
|
||||
}
|
||||
|
||||
function distributeFeeAmongMembers() public payable {
|
||||
uint allocated;
|
||||
for (uint i = 0; i < memberCount; i++) {
|
||||
address member = members[i];
|
||||
uint share;
|
||||
if (i < memberCount - 1) {
|
||||
share = (msg.value * balanceOf(member)) / totalSupply();
|
||||
allocated += share;
|
||||
} else {
|
||||
// Due to rounding, give the remainder to the last member
|
||||
share = msg.value - allocated;
|
||||
}
|
||||
// TODO: For efficiency this could be modified to hold the funds for recipients to withdraw
|
||||
payable(member).transfer(share);
|
||||
}
|
||||
}
|
||||
|
||||
function getValidationPool(
|
||||
uint poolIndex
|
||||
)
|
||||
public
|
||||
view
|
||||
returns (
|
||||
uint id,
|
||||
address sender,
|
||||
uint stakeCount,
|
||||
ValidationPoolParams memory params,
|
||||
ValidationPoolProps memory props,
|
||||
bool callbackOnValidate,
|
||||
bytes memory callbackData
|
||||
)
|
||||
{
|
||||
return bench.validationPools(poolIndex);
|
||||
}
|
||||
|
||||
function getValidationPoolCount() public view returns (uint) {
|
||||
return bench.validationPoolCount();
|
||||
}
|
||||
|
||||
function initiateValidationPool(
|
||||
string calldata postId,
|
||||
uint duration,
|
||||
uint[2] calldata quorum, // [Numerator, Denominator]
|
||||
uint[2] calldata winRatio, // [Numerator, Denominator]
|
||||
uint bindingPercent,
|
||||
bool redistributeLosingStakes,
|
||||
bool callbackOnValidate,
|
||||
bytes calldata callbackData
|
||||
) external payable returns (uint) {
|
||||
return
|
||||
bench.initiateValidationPool{value: msg.value}(
|
||||
msg.sender,
|
||||
postId,
|
||||
duration,
|
||||
quorum,
|
||||
winRatio,
|
||||
bindingPercent,
|
||||
redistributeLosingStakes,
|
||||
callbackOnValidate,
|
||||
callbackData
|
||||
);
|
||||
}
|
||||
|
||||
function stakeOnValidationPool(
|
||||
uint poolIndex,
|
||||
uint256 amount,
|
||||
bool inFavor
|
||||
) public {
|
||||
require(
|
||||
balanceOf(msg.sender) >= amount,
|
||||
"Insufficient REP balance to cover stake"
|
||||
);
|
||||
bench.stakeOnValidationPool(poolIndex, msg.sender, amount, inFavor);
|
||||
}
|
||||
|
||||
/// Accept reputation stakes toward a validation pool
|
||||
function delegatedStakeOnValidationPool(
|
||||
uint poolIndex,
|
||||
address owner,
|
||||
uint256 amount,
|
||||
bool inFavor
|
||||
) public {
|
||||
if (allowance(owner, msg.sender) < amount) {
|
||||
amount = allowance(owner, msg.sender);
|
||||
}
|
||||
rep.spendAllowance(owner, msg.sender, amount);
|
||||
bench.stakeOnValidationPool(poolIndex, owner, amount, inFavor);
|
||||
}
|
||||
|
||||
function evaluateOutcome(uint poolIndex) public returns (bool) {
|
||||
return bench.evaluateOutcome(poolIndex);
|
||||
}
|
||||
|
||||
function getLWValidationPool(
|
||||
uint poolIndex
|
||||
)
|
||||
public
|
||||
view
|
||||
returns (
|
||||
uint id,
|
||||
address sender,
|
||||
uint stakeCount,
|
||||
LWVPoolParams memory params,
|
||||
LWVPoolProps memory props,
|
||||
bool callbackOnValidate,
|
||||
bytes memory callbackData
|
||||
)
|
||||
{
|
||||
return lightweightBench.validationPools(poolIndex);
|
||||
}
|
||||
|
||||
function getLWValidationPoolCount() public view returns (uint) {
|
||||
return lightweightBench.validationPoolCount();
|
||||
}
|
||||
|
||||
function initiateLWValidationPool(
|
||||
string calldata postId,
|
||||
uint duration,
|
||||
uint[2] calldata quorum, // [Numerator, Denominator]
|
||||
uint[2] calldata winRatio, // [Numerator, Denominator]
|
||||
uint bindingPercent,
|
||||
bool redistributeLosingStakes,
|
||||
bool callbackOnValidate,
|
||||
bytes calldata callbackData
|
||||
) external payable returns (uint) {
|
||||
return
|
||||
lightweightBench.initiateValidationPool{value: msg.value}(
|
||||
msg.sender,
|
||||
postId,
|
||||
duration,
|
||||
quorum,
|
||||
winRatio,
|
||||
bindingPercent,
|
||||
redistributeLosingStakes,
|
||||
callbackOnValidate,
|
||||
callbackData
|
||||
);
|
||||
}
|
||||
|
||||
function proposeLWResult(
|
||||
uint poolIndex,
|
||||
string calldata resultHash,
|
||||
Transfer[] calldata transfers
|
||||
) external {
|
||||
lightweightBench.proposeResult(poolIndex, resultHash, transfers);
|
||||
}
|
||||
|
||||
function stakeOnLWValidationPool(
|
||||
uint poolIndex,
|
||||
string calldata resultHash,
|
||||
uint256 amount,
|
||||
bool inFavor
|
||||
) public {
|
||||
require(
|
||||
balanceOf(msg.sender) >= amount,
|
||||
"Insufficient REP balance to cover stake"
|
||||
);
|
||||
lightweightBench.stakeOnValidationPool(
|
||||
poolIndex,
|
||||
resultHash,
|
||||
msg.sender,
|
||||
amount,
|
||||
inFavor
|
||||
);
|
||||
}
|
||||
|
||||
/// Accept reputation stakes toward a validation pool
|
||||
function delegatedStakeOnLWValidationPool(
|
||||
uint poolIndex,
|
||||
string calldata resultHash,
|
||||
address owner,
|
||||
uint256 amount,
|
||||
bool inFavor
|
||||
) public {
|
||||
if (allowance(owner, msg.sender) < amount) {
|
||||
amount = allowance(owner, msg.sender);
|
||||
}
|
||||
rep.spendAllowance(owner, msg.sender, amount);
|
||||
lightweightBench.stakeOnValidationPool(
|
||||
poolIndex,
|
||||
resultHash,
|
||||
owner,
|
||||
amount,
|
||||
inFavor
|
||||
);
|
||||
}
|
||||
|
||||
function evaluateLWOutcome(uint poolIndex) public returns (bool) {
|
||||
return lightweightBench.evaluateOutcome(poolIndex);
|
||||
}
|
||||
|
||||
function onValidate(
|
||||
address target,
|
||||
bool votePasses,
|
||||
bool quorumMet,
|
||||
uint stakedFor,
|
||||
uint stakedAgainst,
|
||||
bytes calldata callbackData
|
||||
) public {
|
||||
require(
|
||||
msg.sender == address(lightweightBench) ||
|
||||
msg.sender == address(bench),
|
||||
"Only DAO core contracts may call onValidate"
|
||||
);
|
||||
IOnValidate(target).onValidate(
|
||||
votePasses,
|
||||
quorumMet,
|
||||
stakedFor,
|
||||
stakedAgainst,
|
||||
callbackData
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience contract to extend for other contracts that will be initialized to
|
||||
|
|
|
@ -0,0 +1,240 @@
|
|||
// SPDX-License-Identifier: Unlicense
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "./Reputation.sol";
|
||||
|
||||
struct Citation {
|
||||
int weightPPM;
|
||||
string targetPostId;
|
||||
}
|
||||
|
||||
struct Author {
|
||||
uint weightPPM;
|
||||
address authorAddress;
|
||||
}
|
||||
|
||||
struct Post {
|
||||
string id;
|
||||
address sender;
|
||||
Author[] authors;
|
||||
Citation[] citations;
|
||||
uint reputation;
|
||||
// TODO: timestamp
|
||||
}
|
||||
|
||||
contract Forum is Reputation {
|
||||
mapping(string => Post) public posts;
|
||||
string[] public postIds;
|
||||
uint public postCount;
|
||||
mapping(string => mapping(string => int)) _edgeBalances;
|
||||
|
||||
event PostAdded(string id);
|
||||
|
||||
// Forum parameters
|
||||
// TODO: Make depth limit configurable; take as param in _onValidatePost callback
|
||||
uint depthLimit = 3;
|
||||
|
||||
function addPost(
|
||||
Author[] calldata authors,
|
||||
string calldata contentId,
|
||||
Citation[] calldata citations
|
||||
) external {
|
||||
require(authors.length > 0, "Post must include at least one author");
|
||||
postCount++;
|
||||
postIds.push(contentId);
|
||||
Post storage post = posts[contentId];
|
||||
require(
|
||||
post.authors.length == 0,
|
||||
"A post with this contentId already exists"
|
||||
);
|
||||
post.sender = msg.sender;
|
||||
post.id = contentId;
|
||||
uint authorTotalWeightPercent;
|
||||
for (uint i = 0; i < authors.length; i++) {
|
||||
authorTotalWeightPercent += authors[i].weightPPM;
|
||||
post.authors.push(authors[i]);
|
||||
}
|
||||
require(
|
||||
authorTotalWeightPercent == 1000000,
|
||||
"Author weights must sum to 1000000"
|
||||
);
|
||||
for (uint i = 0; i < citations.length; i++) {
|
||||
post.citations.push(citations[i]);
|
||||
}
|
||||
int totalCitationWeightPos;
|
||||
int totalCitationWeightNeg;
|
||||
for (uint i = 0; i < post.citations.length; i++) {
|
||||
int weight = post.citations[i].weightPPM;
|
||||
require(
|
||||
weight >= -1000000,
|
||||
"Each citation weight must be >= -1000000"
|
||||
);
|
||||
require(
|
||||
weight <= 1000000,
|
||||
"Each citation weight must be <= 1000000"
|
||||
);
|
||||
if (weight > 0) totalCitationWeightPos += weight;
|
||||
else totalCitationWeightNeg += weight;
|
||||
}
|
||||
require(
|
||||
totalCitationWeightPos <= 1000000,
|
||||
"Sum of positive citations must be <= 1000000"
|
||||
);
|
||||
require(
|
||||
totalCitationWeightNeg >= -1000000,
|
||||
"Sum of negative citations must be >= -1000000"
|
||||
);
|
||||
emit PostAdded(contentId);
|
||||
}
|
||||
|
||||
function getPostAuthors(
|
||||
string calldata postId
|
||||
) external view returns (Author[] memory) {
|
||||
Post storage post = posts[postId];
|
||||
return post.authors;
|
||||
}
|
||||
|
||||
function _handleCitation(
|
||||
string memory postId,
|
||||
Citation memory citation,
|
||||
int amount,
|
||||
bool initialNegative,
|
||||
uint depth
|
||||
) internal returns (int outboundAmount) {
|
||||
outboundAmount = (amount * citation.weightPPM) / 1000000;
|
||||
if (bytes(citation.targetPostId).length == 0) {
|
||||
// Incineration
|
||||
require(
|
||||
outboundAmount >= 0,
|
||||
"Leaching from incinerator is forbidden"
|
||||
);
|
||||
_burn(address(this), uint(outboundAmount));
|
||||
return outboundAmount;
|
||||
}
|
||||
int balanceToOutbound = _edgeBalances[postId][citation.targetPostId];
|
||||
if (initialNegative) {
|
||||
if (outboundAmount < 0) {
|
||||
outboundAmount = outboundAmount > -balanceToOutbound
|
||||
? outboundAmount
|
||||
: -balanceToOutbound;
|
||||
} else {
|
||||
outboundAmount = outboundAmount < -balanceToOutbound
|
||||
? outboundAmount
|
||||
: -balanceToOutbound;
|
||||
}
|
||||
}
|
||||
int refund = _propagateReputation(
|
||||
citation.targetPostId,
|
||||
outboundAmount,
|
||||
initialNegative || (depth == 0 && citation.weightPPM < 0),
|
||||
depth + 1
|
||||
);
|
||||
outboundAmount -= refund;
|
||||
_edgeBalances[postId][citation.targetPostId] += outboundAmount;
|
||||
}
|
||||
|
||||
function _distributeAmongAuthors(
|
||||
Post memory post,
|
||||
int amount
|
||||
) internal returns (int refund) {
|
||||
int allocated;
|
||||
|
||||
for (uint i = 0; i < post.authors.length; i++) {
|
||||
address authorAddress = post.authors[i].authorAddress;
|
||||
if (!isMember[authorAddress]) {
|
||||
members[memberCount++] = authorAddress;
|
||||
isMember[authorAddress] = true;
|
||||
}
|
||||
}
|
||||
for (uint i = 0; i < post.authors.length; i++) {
|
||||
Author memory author = post.authors[i];
|
||||
int share;
|
||||
if (i < post.authors.length - 1) {
|
||||
share = (amount * int(author.weightPPM)) / 1000000;
|
||||
allocated += share;
|
||||
} else {
|
||||
// For the last author, allocate the remainder.
|
||||
share = amount - allocated;
|
||||
}
|
||||
if (share > 0) {
|
||||
_update(address(this), author.authorAddress, uint(share));
|
||||
if (!isMember[author.authorAddress]) {
|
||||
members[memberCount++] = author.authorAddress;
|
||||
isMember[author.authorAddress] = true;
|
||||
}
|
||||
} else if (balanceOf(author.authorAddress) < uint(-share)) {
|
||||
// Author has already lost some REP gained from this post.
|
||||
// That means other DAO members have earned it for policing.
|
||||
// We need to refund the difference here to ensure accurate bookkeeping
|
||||
refund += share + int(balanceOf(author.authorAddress));
|
||||
_update(
|
||||
author.authorAddress,
|
||||
address(this),
|
||||
balanceOf(author.authorAddress)
|
||||
);
|
||||
} else {
|
||||
_update(author.authorAddress, address(this), uint(-share));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _propagateReputation(
|
||||
string memory postId,
|
||||
int amount,
|
||||
bool initialNegative,
|
||||
uint depth
|
||||
) internal returns (int refundToInbound) {
|
||||
if (depth >= depthLimit) {
|
||||
return amount;
|
||||
}
|
||||
Post storage post = posts[postId];
|
||||
if (post.authors.length == 0) {
|
||||
// We most likely got here via a citation to a post that hasn't been added yet.
|
||||
// We support this scenario so that a citation graph can be imported one post at a time.
|
||||
return amount;
|
||||
}
|
||||
// Propagate negative citations first
|
||||
for (uint i = 0; i < post.citations.length; i++) {
|
||||
if (post.citations[i].weightPPM < 0) {
|
||||
amount -= _handleCitation(
|
||||
postId,
|
||||
post.citations[i],
|
||||
amount,
|
||||
initialNegative,
|
||||
depth
|
||||
);
|
||||
}
|
||||
}
|
||||
// Now propagate positive citations
|
||||
for (uint i = 0; i < post.citations.length; i++) {
|
||||
if (post.citations[i].weightPPM > 0) {
|
||||
amount -= _handleCitation(
|
||||
postId,
|
||||
post.citations[i],
|
||||
amount,
|
||||
initialNegative,
|
||||
depth
|
||||
);
|
||||
}
|
||||
}
|
||||
if (amount > 0) {
|
||||
_distributeAmongAuthors(post, amount);
|
||||
post.reputation += uint(amount);
|
||||
} else {
|
||||
if (int(post.reputation) + amount >= 0) {
|
||||
// Reduce the reputation of each author proportionately;
|
||||
// If any author has insufficient reputation, refund the difference.
|
||||
refundToInbound = _distributeAmongAuthors(post, amount);
|
||||
post.reputation -= uint(-amount);
|
||||
} else {
|
||||
// If we applied the full amount, the post's reputation would decrease below zero.
|
||||
refundToInbound = int(post.reputation) + amount;
|
||||
refundToInbound += _distributeAmongAuthors(
|
||||
post,
|
||||
-int(post.reputation)
|
||||
);
|
||||
post.reputation = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,36 +2,11 @@
|
|||
pragma solidity ^0.8.24;
|
||||
|
||||
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
import "./DAO.sol";
|
||||
|
||||
contract Reputation is ERC20("Reputation", "REP") {
|
||||
DAO dao;
|
||||
|
||||
function registerDAO(DAO dao_) external {
|
||||
require(
|
||||
address(dao) == address(0),
|
||||
"A DAO has already been registered"
|
||||
);
|
||||
dao = dao_;
|
||||
}
|
||||
|
||||
function update(address from, address to, uint256 value) public {
|
||||
require(
|
||||
msg.sender == address(dao),
|
||||
"Only DAO contract may call update"
|
||||
);
|
||||
_update(from, to, value);
|
||||
}
|
||||
|
||||
function mint(address account, uint256 value) public {
|
||||
require(msg.sender == address(dao), "Only DAO contract may call mint");
|
||||
_mint(account, value);
|
||||
}
|
||||
|
||||
function burn(address account, uint256 value) public {
|
||||
require(msg.sender == address(dao), "Only DAO contract may call burn");
|
||||
_burn(account, value);
|
||||
}
|
||||
mapping(uint => address) public members;
|
||||
uint public memberCount;
|
||||
mapping(address => bool) public isMember;
|
||||
|
||||
function decimals() public pure override returns (uint8) {
|
||||
return 9;
|
||||
|
@ -49,24 +24,4 @@ contract Reputation is ERC20("Reputation", "REP") {
|
|||
) public pure override returns (bool) {
|
||||
revert("REP transfer is not allowed");
|
||||
}
|
||||
|
||||
function spendAllowance(
|
||||
address owner,
|
||||
address spender,
|
||||
uint256 value
|
||||
) public {
|
||||
require(
|
||||
msg.sender == address(dao),
|
||||
"Only DAO contract may call spendAllowance"
|
||||
);
|
||||
_spendAllowance(owner, spender, value);
|
||||
}
|
||||
|
||||
function approve(address owner, address spender, uint256 value) public {
|
||||
require(
|
||||
msg.sender == address(dao),
|
||||
"Only DAO contract may call approve"
|
||||
);
|
||||
_approve(owner, spender, value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
// SPDX-License-Identifier: Unlicense
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "./DAO.sol";
|
||||
import "./Reputation.sol";
|
||||
import "./Forum.sol";
|
||||
import "../interfaces/IOnValidate.sol";
|
||||
|
||||
struct LWVPoolParams {
|
||||
struct ValidationPoolStake {
|
||||
uint id;
|
||||
bool inFavor;
|
||||
uint amount;
|
||||
address sender;
|
||||
}
|
||||
|
||||
struct ValidationPoolParams {
|
||||
uint duration;
|
||||
uint[2] quorum; // [ Numerator, Denominator ]
|
||||
uint[2] winRatio; // [ Numerator, Denominator ]
|
||||
|
@ -11,67 +20,78 @@ struct LWVPoolParams {
|
|||
bool redistributeLosingStakes;
|
||||
}
|
||||
|
||||
struct LWVPoolProps {
|
||||
struct ValidationPool {
|
||||
uint id;
|
||||
string postId;
|
||||
uint fee;
|
||||
address sender;
|
||||
uint minted;
|
||||
mapping(uint => ValidationPoolStake) stakes;
|
||||
uint stakeCount;
|
||||
ValidationPoolParams params;
|
||||
uint fee;
|
||||
uint endTime;
|
||||
bool resolved;
|
||||
bool outcome;
|
||||
bool callbackOnValidate;
|
||||
bytes callbackData;
|
||||
}
|
||||
|
||||
struct Transfer {
|
||||
address from;
|
||||
address to;
|
||||
uint amount;
|
||||
}
|
||||
|
||||
contract LightweightBench {
|
||||
struct ProposedResult {
|
||||
Transfer[] transfers;
|
||||
uint stakedFor;
|
||||
}
|
||||
|
||||
struct Stake {
|
||||
uint id;
|
||||
bool inFavor;
|
||||
uint amount;
|
||||
address sender;
|
||||
string resultHash;
|
||||
}
|
||||
|
||||
struct Pool {
|
||||
uint id;
|
||||
address sender;
|
||||
mapping(string => ProposedResult) proposedResults;
|
||||
string[] proposedResultHashes;
|
||||
mapping(uint => Stake) stakes;
|
||||
uint stakeCount;
|
||||
LWVPoolParams params;
|
||||
LWVPoolProps props;
|
||||
bool callbackOnValidate;
|
||||
bytes callbackData;
|
||||
}
|
||||
|
||||
mapping(uint => Pool) public validationPools;
|
||||
contract ValidationPools is Reputation, Forum {
|
||||
mapping(uint => ValidationPool) public validationPools;
|
||||
uint public validationPoolCount;
|
||||
DAO dao;
|
||||
|
||||
uint constant minDuration = 1; // 1 second
|
||||
uint constant maxDuration = 365_000_000 days; // 1 million years
|
||||
uint[2] minQuorum = [1, 10];
|
||||
|
||||
function registerDAO(DAO dao_) external {
|
||||
require(
|
||||
address(dao) == address(0),
|
||||
"A DAO has already been registered"
|
||||
);
|
||||
dao = dao_;
|
||||
event ValidationPoolInitiated(uint poolIndex);
|
||||
event ValidationPoolResolved(
|
||||
uint poolIndex,
|
||||
bool votePasses,
|
||||
bool quorumMet
|
||||
);
|
||||
|
||||
/// Internal function to register a stake for/against a validation pool
|
||||
function _stakeOnValidationPool(
|
||||
ValidationPool storage pool,
|
||||
address sender,
|
||||
uint256 amount,
|
||||
bool inFavor
|
||||
) internal {
|
||||
require(block.timestamp <= pool.endTime, "Pool end time has passed");
|
||||
// We don't call _update here; We defer that until evaluateOutcome.
|
||||
uint stakeIndex = pool.stakeCount++;
|
||||
ValidationPoolStake storage s = pool.stakes[stakeIndex];
|
||||
s.sender = sender;
|
||||
s.inFavor = inFavor;
|
||||
s.amount = amount;
|
||||
s.id = stakeIndex;
|
||||
}
|
||||
|
||||
/// Accept reputation stakes toward a validation pool
|
||||
function stakeOnValidationPool(
|
||||
uint poolIndex,
|
||||
uint256 amount,
|
||||
bool inFavor
|
||||
) public {
|
||||
ValidationPool storage pool = validationPools[poolIndex];
|
||||
_stakeOnValidationPool(pool, msg.sender, amount, inFavor);
|
||||
}
|
||||
|
||||
/// Accept reputation stakes toward a validation pool
|
||||
function delegatedStakeOnValidationPool(
|
||||
uint poolIndex,
|
||||
address owner,
|
||||
uint256 amount,
|
||||
bool inFavor
|
||||
) public {
|
||||
ValidationPool storage pool = validationPools[poolIndex];
|
||||
_spendAllowance(owner, msg.sender, amount);
|
||||
_stakeOnValidationPool(pool, owner, amount, inFavor);
|
||||
}
|
||||
|
||||
/// Accept fee to initiate a validation pool
|
||||
function initiateValidationPool(
|
||||
address sender,
|
||||
string calldata postId,
|
||||
uint duration,
|
||||
uint[2] calldata quorum, // [Numerator, Denominator]
|
||||
|
@ -81,10 +101,7 @@ contract LightweightBench {
|
|||
bool callbackOnValidate,
|
||||
bytes calldata callbackData
|
||||
) external payable returns (uint poolIndex) {
|
||||
require(
|
||||
msg.sender == address(dao),
|
||||
"Only DAO contract may call initiateValidationPool"
|
||||
);
|
||||
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");
|
||||
require(
|
||||
|
@ -94,172 +111,69 @@ contract LightweightBench {
|
|||
require(quorum[0] <= quorum[1], "Quorum is greater than one");
|
||||
require(winRatio[0] <= winRatio[1], "Win ratio is greater than one");
|
||||
require(bindingPercent <= 100, "Binding percent must be <= 100");
|
||||
Post storage post = posts[postId];
|
||||
require(post.authors.length != 0, "Target post not found");
|
||||
poolIndex = validationPoolCount++;
|
||||
Pool storage pool = validationPools[poolIndex];
|
||||
pool.id = poolIndex;
|
||||
pool.sender = sender;
|
||||
pool.props.postId = postId;
|
||||
pool.props.fee = msg.value;
|
||||
pool.props.endTime = block.timestamp + duration;
|
||||
ValidationPool storage pool = validationPools[poolIndex];
|
||||
pool.sender = msg.sender;
|
||||
pool.postId = postId;
|
||||
pool.fee = msg.value;
|
||||
pool.params.quorum = quorum;
|
||||
pool.params.winRatio = winRatio;
|
||||
pool.params.bindingPercent = bindingPercent;
|
||||
pool.params.redistributeLosingStakes = redistributeLosingStakes;
|
||||
pool.params.duration = duration;
|
||||
pool.endTime = block.timestamp + duration;
|
||||
pool.id = poolIndex;
|
||||
pool.callbackOnValidate = callbackOnValidate;
|
||||
pool.callbackData = callbackData;
|
||||
// We use our privilege as the DAO contract to mint reputation in proportion with the fee.
|
||||
// Here we assume a minting ratio of 1
|
||||
// TODO: Make minting ratio an adjustable parameter
|
||||
dao.mint(address(dao), pool.props.fee);
|
||||
pool.props.minted = msg.value;
|
||||
dao.emitLWValidationPoolInitiated(poolIndex);
|
||||
}
|
||||
|
||||
function proposeResult(
|
||||
uint poolIndex,
|
||||
string calldata resultHash,
|
||||
Transfer[] calldata transfers
|
||||
) external {
|
||||
require(
|
||||
transfers.length > 0,
|
||||
"The proposed result contains no transfers"
|
||||
);
|
||||
Pool storage pool = validationPools[poolIndex];
|
||||
require(
|
||||
block.timestamp <= pool.props.endTime,
|
||||
"Pool end time has passed"
|
||||
);
|
||||
ProposedResult storage proposedResult = pool.proposedResults[
|
||||
resultHash
|
||||
];
|
||||
require(
|
||||
proposedResult.transfers.length == 0,
|
||||
"This result hash has already been proposed"
|
||||
);
|
||||
uint resultIndex = pool.proposedResultHashes.length;
|
||||
pool.proposedResultHashes.push(resultHash);
|
||||
for (uint i = 0; i < transfers.length; i++) {
|
||||
proposedResult.transfers.push(transfers[i]);
|
||||
}
|
||||
dao.emitLWResultProposed(poolIndex, resultIndex, resultHash);
|
||||
}
|
||||
|
||||
/// Register a stake for/against a validation pool
|
||||
function stakeOnValidationPool(
|
||||
uint poolIndex,
|
||||
string calldata resultHash,
|
||||
address sender,
|
||||
uint256 amount,
|
||||
bool inFavor
|
||||
) external {
|
||||
require(
|
||||
msg.sender == address(dao),
|
||||
"Only DAO contract may call stakeOnValidationPool"
|
||||
);
|
||||
Pool storage pool = validationPools[poolIndex];
|
||||
require(
|
||||
block.timestamp <= pool.props.endTime,
|
||||
"Pool end time has passed"
|
||||
);
|
||||
if (inFavor) {
|
||||
ProposedResult storage proposedResult = pool.proposedResults[
|
||||
resultHash
|
||||
];
|
||||
require(
|
||||
proposedResult.transfers.length > 0,
|
||||
"This result hash has not been proposed"
|
||||
);
|
||||
}
|
||||
// We don't call _update here; We defer that until evaluateOutcome.
|
||||
uint stakeIndex = pool.stakeCount++;
|
||||
Stake storage s = pool.stakes[stakeIndex];
|
||||
s.sender = sender;
|
||||
s.inFavor = inFavor;
|
||||
s.amount = amount;
|
||||
s.id = stakeIndex;
|
||||
s.resultHash = resultHash;
|
||||
_mint(address(this), msg.value);
|
||||
pool.minted = msg.value;
|
||||
emit ValidationPoolInitiated(poolIndex);
|
||||
}
|
||||
|
||||
/// Evaluate outcome of a validation pool
|
||||
function evaluateOutcome(uint poolIndex) public returns (bool votePasses) {
|
||||
require(
|
||||
msg.sender == address(dao),
|
||||
"Only DAO contract may call evaluateOutcome"
|
||||
);
|
||||
Pool storage pool = validationPools[poolIndex];
|
||||
require(pool.props.resolved == false, "Pool is already resolved");
|
||||
ValidationPool storage pool = validationPools[poolIndex];
|
||||
require(pool.resolved == false, "Pool is already resolved");
|
||||
uint stakedFor;
|
||||
uint stakedAgainst;
|
||||
Stake storage s;
|
||||
ValidationPoolStake storage s;
|
||||
for (uint i = 0; i < pool.stakeCount; i++) {
|
||||
s = pool.stakes[i];
|
||||
// Make sure the sender still has the required balance.
|
||||
// If not, automatically decrease the staked amount.
|
||||
if (dao.balanceOf(s.sender) < s.amount) {
|
||||
s.amount = dao.balanceOf(s.sender);
|
||||
}
|
||||
if (s.inFavor) {
|
||||
ProposedResult storage proposedResult = pool.proposedResults[
|
||||
s.resultHash
|
||||
];
|
||||
proposedResult.stakedFor += s.amount;
|
||||
stakedFor += s.amount;
|
||||
} else {
|
||||
stakedAgainst += s.amount;
|
||||
}
|
||||
}
|
||||
// Determine the winning result hash
|
||||
uint[] memory stakedForResult = new uint[](
|
||||
pool.proposedResultHashes.length
|
||||
);
|
||||
uint winningResult;
|
||||
for (uint i = 0; i < pool.proposedResultHashes.length; i++) {
|
||||
string storage proposedResultHash = pool.proposedResultHashes[i];
|
||||
ProposedResult storage proposedResult = pool.proposedResults[
|
||||
proposedResultHash
|
||||
];
|
||||
stakedForResult[i] += proposedResult.stakedFor;
|
||||
if (stakedForResult[i] > stakedForResult[winningResult]) {
|
||||
winningResult = i;
|
||||
}
|
||||
}
|
||||
// Only count stakes for the winning hash among the total staked in favor of the pool
|
||||
for (uint i = 0; i < pool.stakeCount; i++) {
|
||||
s = pool.stakes[i];
|
||||
if (
|
||||
s.inFavor &&
|
||||
keccak256(bytes(s.resultHash)) ==
|
||||
keccak256(bytes(pool.proposedResultHashes[winningResult]))
|
||||
) {
|
||||
stakedFor += s.amount;
|
||||
}
|
||||
}
|
||||
|
||||
stakedFor += pool.props.minted / 2;
|
||||
stakedAgainst += pool.props.minted / 2;
|
||||
if (pool.props.minted % 2 != 0) {
|
||||
stakedFor += pool.minted / 2;
|
||||
stakedAgainst += pool.minted / 2;
|
||||
if (pool.minted % 2 != 0) {
|
||||
stakedFor += 1;
|
||||
}
|
||||
// Special case for early evaluation if dao.totalSupply has been staked
|
||||
require(
|
||||
block.timestamp > pool.props.endTime ||
|
||||
stakedFor + stakedAgainst == dao.totalSupply(),
|
||||
block.timestamp > pool.endTime ||
|
||||
stakedFor + stakedAgainst == totalSupply(),
|
||||
"Pool end time has not yet arrived"
|
||||
);
|
||||
// Check that quorum is met
|
||||
if (
|
||||
pool.params.quorum[1] * (stakedFor + stakedAgainst) <=
|
||||
dao.totalSupply() * pool.params.quorum[0]
|
||||
totalSupply() * pool.params.quorum[0]
|
||||
) {
|
||||
// TODO: Refund fee
|
||||
// TODO: this could be made available for the sender to withdraw
|
||||
// payable(pool.sender).transfer(pool.props.fee);
|
||||
pool.props.resolved = true;
|
||||
dao.emitValidationPoolResolved(poolIndex, false, false);
|
||||
// payable(pool.sender).transfer(pool.fee);
|
||||
pool.resolved = true;
|
||||
emit ValidationPoolResolved(poolIndex, false, false);
|
||||
// Callback if requested
|
||||
if (pool.callbackOnValidate) {
|
||||
dao.onValidate(
|
||||
pool.sender,
|
||||
IOnValidate(pool.sender).onValidate(
|
||||
votePasses,
|
||||
false,
|
||||
stakedFor,
|
||||
|
@ -270,16 +184,15 @@ contract LightweightBench {
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
// A tie is resolved in favor of the validation pool.
|
||||
// This is especially important so that the DAO's first pool can pass,
|
||||
// when no reputation has yet been minted.
|
||||
votePasses =
|
||||
stakedFor * pool.params.winRatio[1] >=
|
||||
(stakedFor + stakedAgainst) * pool.params.winRatio[0];
|
||||
pool.props.resolved = true;
|
||||
pool.props.outcome = votePasses;
|
||||
dao.emitValidationPoolResolved(poolIndex, votePasses, true);
|
||||
pool.resolved = true;
|
||||
pool.outcome = votePasses;
|
||||
emit ValidationPoolResolved(poolIndex, votePasses, true);
|
||||
|
||||
// Value of losing stakes should be distributed among winners, in proportion to their stakes
|
||||
// Only bindingPercent % should be redistributed
|
||||
|
@ -293,10 +206,10 @@ contract LightweightBench {
|
|||
// Losing stake
|
||||
uint amount = (s.amount * pool.params.bindingPercent) / 100;
|
||||
if (pool.params.redistributeLosingStakes) {
|
||||
dao.update(s.sender, address(dao), amount);
|
||||
_update(s.sender, address(this), amount);
|
||||
totalRewards += amount;
|
||||
} else {
|
||||
dao.burn(s.sender, amount);
|
||||
_burn(s.sender, amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -305,7 +218,7 @@ contract LightweightBench {
|
|||
// If vote passes, reward the author as though they had staked the winning portion of the VP initial stake
|
||||
// Here we assume a stakeForAuthor ratio of 0.5
|
||||
// TODO: Make stakeForAuthor an adjustable parameter
|
||||
totalRewards += pool.props.minted / 2;
|
||||
totalRewards += pool.minted / 2;
|
||||
// Include the losign portion of the VP initial stake
|
||||
// Issue rewards to the winners
|
||||
for (uint i = 0; i < pool.stakeCount; i++) {
|
||||
|
@ -318,30 +231,26 @@ contract LightweightBench {
|
|||
uint reward = (((totalRewards * s.amount) /
|
||||
amountFromWinners) * pool.params.bindingPercent) / 100;
|
||||
totalAllocated += reward;
|
||||
dao.update(address(dao), s.sender, reward);
|
||||
_update(address(this), s.sender, reward);
|
||||
}
|
||||
}
|
||||
// Due to rounding, there may be some excess REP. Award it to the author.
|
||||
uint remainder = totalRewards - totalAllocated;
|
||||
if (pool.props.minted % 2 != 0) {
|
||||
if (pool.minted % 2 != 0) {
|
||||
// We staked the odd remainder in favor of the post, on behalf of the author.
|
||||
remainder += 1;
|
||||
}
|
||||
|
||||
// Execute the transfers from the winning proposed result
|
||||
ProposedResult storage result = pool.proposedResults[
|
||||
pool.proposedResultHashes[winningResult]
|
||||
];
|
||||
for (uint i = 0; i < result.transfers.length; i++) {
|
||||
dao.update(
|
||||
result.transfers[i].from,
|
||||
result.transfers[i].to,
|
||||
result.transfers[i].amount
|
||||
);
|
||||
}
|
||||
// Transfer REP to the forum instead of to the author directly
|
||||
_propagateReputation(
|
||||
pool.postId,
|
||||
int(pool.minted / 2 + remainder),
|
||||
false,
|
||||
0
|
||||
);
|
||||
} else {
|
||||
// If vote does not pass, divide the losing stake among the winners
|
||||
totalRewards += pool.props.minted;
|
||||
totalRewards += pool.minted;
|
||||
for (uint i = 0; i < pool.stakeCount; i++) {
|
||||
s = pool.stakes[i];
|
||||
if (
|
||||
|
@ -350,21 +259,25 @@ contract LightweightBench {
|
|||
) {
|
||||
// Winning stake
|
||||
uint reward = (((totalRewards * s.amount) /
|
||||
(amountFromWinners - pool.props.minted / 2)) *
|
||||
(amountFromWinners - pool.minted / 2)) *
|
||||
pool.params.bindingPercent) / 100;
|
||||
totalAllocated += reward;
|
||||
dao.update(address(dao), s.sender, reward);
|
||||
_update(address(this), s.sender, reward);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Distribute fee proportionately among all reputation holders
|
||||
dao.distributeFeeAmongMembers{value: pool.props.fee}();
|
||||
for (uint i = 0; i < memberCount; i++) {
|
||||
address member = members[i];
|
||||
uint share = (pool.fee * balanceOf(member)) / totalSupply();
|
||||
// TODO: For efficiency this could be modified to hold the funds for recipients to withdraw
|
||||
payable(member).transfer(share);
|
||||
}
|
||||
|
||||
// Callback if requested
|
||||
if (pool.callbackOnValidate) {
|
||||
dao.onValidate(
|
||||
pool.sender,
|
||||
IOnValidate(pool.sender).onValidate(
|
||||
votePasses,
|
||||
true,
|
||||
stakedFor,
|
|
@ -6,5 +6,5 @@ interface IAcceptAvailability {
|
|||
address from,
|
||||
uint256 value,
|
||||
uint duration
|
||||
) external returns (uint refund);
|
||||
) external;
|
||||
}
|
||||
|
|
|
@ -8,5 +8,5 @@ interface IOnValidate {
|
|||
uint stakedFor,
|
||||
uint stakedAgainst,
|
||||
bytes calldata callbackData
|
||||
) external;
|
||||
) external returns (uint);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
const { ethers } = require('hardhat');
|
||||
const { execSync } = require('child_process');
|
||||
const { getContractAddressByNetworkName } = require('./contract-config');
|
||||
const readFromApi = require('./util/read-from-api');
|
||||
|
||||
const network = process.env.HARDHAT_NETWORK;
|
||||
let currentVersionProposalId;
|
||||
|
||||
let dao;
|
||||
let work1;
|
||||
|
@ -14,6 +16,20 @@ let posts;
|
|||
let proposalsContract;
|
||||
let proposals;
|
||||
|
||||
const getCurrentVersion = () => {
|
||||
const currentCommit = execSync('git rev-parse HEAD');
|
||||
return currentCommit.toString();
|
||||
};
|
||||
|
||||
const fetchCurrentVersionProposal = async () => {
|
||||
// const p = await proposalsContract.
|
||||
};
|
||||
|
||||
const getLatestVersion = () => {
|
||||
const latestVersion = 'TBD';
|
||||
return latestVersion;
|
||||
};
|
||||
|
||||
const fetchReputation = async () => {
|
||||
reputation = await dao.balanceOf(account);
|
||||
console.log(`reputation: ${reputation}`);
|
||||
|
@ -21,14 +37,14 @@ const fetchReputation = async () => {
|
|||
|
||||
const fetchPost = async (postIndex) => {
|
||||
const {
|
||||
id, sender, author, postId,
|
||||
id, sender, author, contentId,
|
||||
} = await dao.posts(postIndex);
|
||||
const { content, embeddedData } = await readFromApi(postId);
|
||||
const { content, embeddedData } = await readFromApi(contentId);
|
||||
const post = {
|
||||
id,
|
||||
sender,
|
||||
author,
|
||||
postId,
|
||||
contentId,
|
||||
content,
|
||||
embeddedData,
|
||||
};
|
||||
|
@ -39,7 +55,7 @@ const fetchPost = async (postIndex) => {
|
|||
const fetchValidationPool = async (poolIndex) => {
|
||||
const {
|
||||
id, postIndex, sender, stakeCount, fee, duration, endTime, resolved, outcome,
|
||||
} = await dao.getValidationPool(poolIndex);
|
||||
} = await dao.validationPools(poolIndex);
|
||||
const pool = {
|
||||
id, postIndex, sender, stakeCount, fee, duration, endTime, resolved, outcome,
|
||||
};
|
||||
|
@ -49,7 +65,7 @@ const fetchValidationPool = async (poolIndex) => {
|
|||
};
|
||||
|
||||
const fetchValidationPools = async () => {
|
||||
const count = await dao.getValidationPoolCount();
|
||||
const count = await dao.validationPoolCount();
|
||||
console.log(`validation pool count: ${count}`);
|
||||
const promises = [];
|
||||
validationPools = [];
|
||||
|
@ -59,6 +75,22 @@ const fetchValidationPools = async () => {
|
|||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
const fetchProposal = async (proposalIndex) => {
|
||||
const proposal = await proposalsContract.proposals(proposalIndex);
|
||||
proposals[proposalIndex] = proposal;
|
||||
};
|
||||
|
||||
const fetchProposals = async () => {
|
||||
const count = await proposalsContract.proposalCount();
|
||||
console.log(`proposal count: ${count}`);
|
||||
const promises = [];
|
||||
proposals = [];
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
promises.push(fetchProposal(i));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
const initialize = async () => {
|
||||
const getContract = (name) => ethers.getContractAt(
|
||||
name,
|
||||
|
@ -74,11 +106,12 @@ const initialize = async () => {
|
|||
posts = [];
|
||||
await fetchReputation();
|
||||
await fetchValidationPools();
|
||||
await fetchProposals();
|
||||
};
|
||||
|
||||
const poolIsActive = (pool) => {
|
||||
if (new Date() >= new Date(Number(pool.props.endTime) * 1000)) return false;
|
||||
if (pool.props.resolved) return false;
|
||||
if (new Date() >= new Date(Number(pool.endTime) * 1000)) return false;
|
||||
if (pool.resolved) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
|
@ -87,7 +120,7 @@ const poolIsValidWorkContract = (pool) => {
|
|||
case getContractAddressByNetworkName(network, 'Work1'): {
|
||||
// If this is a valid work evidence
|
||||
// TODO: Can we decode from the post, a reference to the work request?
|
||||
// The work request does have its own postId, the work contract has that
|
||||
// The work request does have its own contentId, the work contract has that
|
||||
// under availabilityStakes
|
||||
const expectedContent = 'This is a work evidence post';
|
||||
return pool.post.content.startsWith(expectedContent);
|
||||
|
@ -105,8 +138,8 @@ const poolIsProposal = (pool) => pool.sender === getContractAddressByNetworkName
|
|||
|
||||
const getPoolStatus = (pool) => {
|
||||
if (poolIsActive(pool)) return 'Active';
|
||||
if (!pool.props.resolved) return 'Ready to Evaluate';
|
||||
if (pool.props.outcome) return 'Accepted';
|
||||
if (!pool.resolved) return 'Ready to Evaluate';
|
||||
if (pool.outcome) return 'Accepted';
|
||||
return 'Rejected';
|
||||
};
|
||||
|
||||
|
@ -146,6 +179,8 @@ const printPool = (pool) => {
|
|||
};
|
||||
|
||||
async function main() {
|
||||
console.log('Current version:', getCurrentVersion());
|
||||
|
||||
await initialize();
|
||||
|
||||
validationPools.forEach(printPool);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const deployDAOCoreContracts = require('./util/deploy-core-contracts');
|
||||
const deployContract = require('./util/deploy-contract');
|
||||
|
||||
async function main() {
|
||||
await deployDAOCoreContracts();
|
||||
await deployContract('DAO', [], true);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
|
|
@ -1,20 +1,12 @@
|
|||
require('dotenv').config();
|
||||
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');
|
||||
const deployDAOCoreContracts = require('./util/deploy-core-contracts');
|
||||
|
||||
const { ROLLUP_INTERVAL } = process.env;
|
||||
|
||||
async function main() {
|
||||
await deployContract('GlobalForum');
|
||||
await deployDAOCoreContracts();
|
||||
await deployDAOContract('Rollup', [ROLLUP_INTERVAL]);
|
||||
await deployContract('DAO', [], true);
|
||||
await deployDAOContract('Proposals');
|
||||
await deployWorkContract('Work1');
|
||||
await deployWorkContract('Onboarding');
|
||||
await deployRollableWorkContract('Work2');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
require('dotenv').config();
|
||||
const deployContract = require('./deploy-contract');
|
||||
const contractAddresses = require('../../contract-addresses.json');
|
||||
|
||||
const network = process.env.HARDHAT_NETWORK;
|
||||
|
||||
const deployDAOCoreContracts = async () => {
|
||||
await deployContract('Reputation', [], true);
|
||||
await deployContract('Bench', [], true);
|
||||
await deployContract('DAO', [
|
||||
contractAddresses[network].Reputation,
|
||||
contractAddresses[network].GlobalForum,
|
||||
contractAddresses[network].Bench,
|
||||
], true);
|
||||
};
|
||||
|
||||
module.exports = deployDAOCoreContracts;
|
|
@ -6,8 +6,8 @@ require('dotenv').config();
|
|||
|
||||
const network = process.env.HARDHAT_NETWORK;
|
||||
|
||||
const deployDAOContract = async (name, args = []) => {
|
||||
await deployContract(name, [contractAddresses[network].DAO, ...args]);
|
||||
const deployDAOContract = async (name) => {
|
||||
await deployContract(name, [contractAddresses[network].DAO]);
|
||||
};
|
||||
|
||||
module.exports = deployDAOContract;
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
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;
|
|
@ -9,11 +9,10 @@ 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,
|
||||
contractAddresses[network].GlobalForum,
|
||||
contractAddresses[network].Proposals,
|
||||
price]);
|
||||
};
|
||||
|
|
|
@ -4,22 +4,20 @@ const {
|
|||
} = require('@nomicfoundation/hardhat-toolbox/network-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { ethers } = require('hardhat');
|
||||
const deployDAO = require('./util/deploy-dao');
|
||||
|
||||
describe('Forum', () => {
|
||||
async function deploy() {
|
||||
const [account1, account2, account3, account4] = await ethers.getSigners();
|
||||
const { dao, forum } = await deployDAO();
|
||||
const [account1, account2, account3] = await ethers.getSigners();
|
||||
const DAO = await ethers.getContractFactory('DAO');
|
||||
const dao = await DAO.deploy();
|
||||
return {
|
||||
dao, forum, account1, account2, account3, account4,
|
||||
dao, account1, account2, account3,
|
||||
};
|
||||
}
|
||||
let dao;
|
||||
let forum;
|
||||
let account1;
|
||||
let account2;
|
||||
let account3;
|
||||
let account4;
|
||||
const POOL_DURATION = 3600; // 1 hour
|
||||
const POOL_FEE = 100;
|
||||
const emptyCallbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
|
||||
|
@ -41,51 +39,57 @@ describe('Forum', () => {
|
|||
{ value: fee ?? POOL_FEE },
|
||||
);
|
||||
|
||||
const addPost = (author, postId, references) => forum.addPost([{
|
||||
const addPost = (author, contentId, citations) => dao.addPost([{
|
||||
weightPPM: 1000000,
|
||||
authorAddress: author,
|
||||
}], postId, references);
|
||||
}], contentId, citations);
|
||||
|
||||
describe('Post', () => {
|
||||
beforeEach(async () => {
|
||||
({
|
||||
dao, forum, account1, account2, account3, account4,
|
||||
dao, account1, account2, account3,
|
||||
} = await loadFixture(deploy));
|
||||
});
|
||||
|
||||
it('should be able to add a post', async () => {
|
||||
const postId = 'some-id';
|
||||
await expect(addPost(account1, postId, [])).to.emit(forum, 'PostAdded').withArgs('some-id');
|
||||
const post = await forum.getPost(postId);
|
||||
const contentId = 'some-id';
|
||||
await expect(addPost(account1, contentId, [])).to.emit(dao, 'PostAdded').withArgs('some-id');
|
||||
const post = await dao.posts(contentId);
|
||||
expect(post.sender).to.equal(account1);
|
||||
expect(post.authors).to.have.length(1);
|
||||
expect(post.authors[0].weightPPM).to.equal(1000000);
|
||||
expect(post.authors[0].authorAddress).to.equal(account1);
|
||||
expect(post.id).to.equal(contentId);
|
||||
const postAuthors = await dao.getPostAuthors(contentId);
|
||||
expect(postAuthors).to.have.length(1);
|
||||
expect(postAuthors[0].weightPPM).to.equal(1000000);
|
||||
expect(postAuthors[0].authorAddress).to.equal(account1);
|
||||
});
|
||||
|
||||
it('should be able to add a post on behalf of another account', async () => {
|
||||
const postId = 'some-id';
|
||||
await addPost(account2, postId, []);
|
||||
const post = await forum.getPost(postId);
|
||||
const contentId = 'some-id';
|
||||
await addPost(account2, contentId, []);
|
||||
const post = await dao.posts(contentId);
|
||||
expect(post.sender).to.equal(account1);
|
||||
expect(post.authors).to.have.length(1);
|
||||
expect(post.authors[0].weightPPM).to.equal(1000000);
|
||||
expect(post.authors[0].authorAddress).to.equal(account2);
|
||||
expect(post.id).to.equal(contentId);
|
||||
const postAuthors = await dao.getPostAuthors(contentId);
|
||||
expect(postAuthors).to.have.length(1);
|
||||
expect(postAuthors[0].weightPPM).to.equal(1000000);
|
||||
expect(postAuthors[0].authorAddress).to.equal(account2);
|
||||
});
|
||||
|
||||
it('should be able to add a post with multiple authors', async () => {
|
||||
const postId = 'some-id';
|
||||
await expect(forum.addPost([
|
||||
const contentId = 'some-id';
|
||||
await expect(dao.addPost([
|
||||
{ weightPPM: 500000, authorAddress: account1 },
|
||||
{ weightPPM: 500000, authorAddress: account2 },
|
||||
], postId, [])).to.emit(forum, 'PostAdded').withArgs('some-id');
|
||||
const post = await forum.getPost(postId);
|
||||
], contentId, [])).to.emit(dao, 'PostAdded').withArgs('some-id');
|
||||
const post = await dao.posts(contentId);
|
||||
expect(post.sender).to.equal(account1);
|
||||
expect(post.authors).to.have.length(2);
|
||||
expect(post.authors[0].weightPPM).to.equal(500000);
|
||||
expect(post.authors[0].authorAddress).to.equal(account1);
|
||||
expect(post.authors[1].weightPPM).to.equal(500000);
|
||||
expect(post.authors[1].authorAddress).to.equal(account2);
|
||||
expect(post.id).to.equal(contentId);
|
||||
const postAuthors = await dao.getPostAuthors(contentId);
|
||||
expect(postAuthors).to.have.length(2);
|
||||
expect(postAuthors[0].weightPPM).to.equal(500000);
|
||||
expect(postAuthors[0].authorAddress).to.equal(account1);
|
||||
expect(postAuthors[1].weightPPM).to.equal(500000);
|
||||
expect(postAuthors[1].authorAddress).to.equal(account2);
|
||||
await initiateValidationPool({ postId: 'some-id' });
|
||||
await time.increase(POOL_DURATION + 1);
|
||||
await dao.evaluateOutcome(0);
|
||||
|
@ -94,48 +98,53 @@ describe('Forum', () => {
|
|||
});
|
||||
|
||||
it('should not be able to add a post with total author weight < 100%', async () => {
|
||||
const postId = 'some-id';
|
||||
await expect(forum.addPost([
|
||||
const contentId = 'some-id';
|
||||
await expect(dao.addPost([
|
||||
{ weightPPM: 500000, authorAddress: account1 },
|
||||
{ weightPPM: 400000, authorAddress: account2 },
|
||||
], postId, [])).to.be.rejectedWith('Author weights must sum to 1000000');
|
||||
], contentId, [])).to.be.rejectedWith('Author weights must sum to 1000000');
|
||||
});
|
||||
|
||||
it('should not be able to add a post with total author weight > 100%', async () => {
|
||||
const postId = 'some-id';
|
||||
await expect(forum.addPost([
|
||||
const contentId = 'some-id';
|
||||
await expect(dao.addPost([
|
||||
{ weightPPM: 500000, authorAddress: account1 },
|
||||
{ weightPPM: 600000, authorAddress: account2 },
|
||||
], postId, [])).to.be.rejectedWith('Author weights must sum to 1000000');
|
||||
], contentId, [])).to.be.rejectedWith('Author weights must sum to 1000000');
|
||||
});
|
||||
|
||||
it('should be able to donate reputation via references', async () => {
|
||||
it('should be able to donate reputation via citations', async () => {
|
||||
await addPost(account1, 'content-id', []);
|
||||
await addPost(account2, 'second-content-id', [{ weightPPM: 500000, targetPostId: 'content-id' }]);
|
||||
await initiateValidationPool({ postId: 'second-content-id' });
|
||||
const pool = await dao.getValidationPool(0);
|
||||
expect(pool.props.postId).to.equal('second-content-id');
|
||||
const pool = await dao.validationPools(0);
|
||||
expect(pool.postId).to.equal('second-content-id');
|
||||
await dao.evaluateOutcome(0);
|
||||
expect(await dao.balanceOf(account1)).to.equal(50);
|
||||
expect(await dao.balanceOf(account2)).to.equal(50);
|
||||
});
|
||||
|
||||
it('should be able to leach reputation via references', async () => {
|
||||
it('should be able to leach reputation via citations', async () => {
|
||||
await addPost(account1, 'content-id', []);
|
||||
expect((await dao.posts('content-id')).reputation).to.equal(0);
|
||||
await initiateValidationPool({ postId: 'content-id' });
|
||||
await dao.evaluateOutcome(0);
|
||||
expect(await dao.balanceOf(account1)).to.equal(100);
|
||||
expect((await dao.posts('content-id')).reputation).to.equal(100);
|
||||
await addPost(account2, 'second-content-id', [{ weightPPM: -500000, targetPostId: 'content-id' }]);
|
||||
expect((await dao.posts('second-content-id')).reputation).to.equal(0);
|
||||
await initiateValidationPool({ postId: 'second-content-id' });
|
||||
const pool = await dao.getValidationPool(1);
|
||||
expect(pool.props.postId).to.equal('second-content-id');
|
||||
const pool = await dao.validationPools(1);
|
||||
expect(pool.postId).to.equal('second-content-id');
|
||||
await time.increase(POOL_DURATION + 1);
|
||||
await dao.evaluateOutcome(1);
|
||||
expect(await dao.balanceOf(account1)).to.equal(50);
|
||||
expect(await dao.balanceOf(account2)).to.equal(150);
|
||||
expect((await dao.posts('content-id')).reputation).to.equal(50);
|
||||
expect((await dao.posts('second-content-id')).reputation).to.equal(150);
|
||||
});
|
||||
|
||||
it('should be able to redistribute power via references', async () => {
|
||||
it('should be able to redistribute power via citations', async () => {
|
||||
await addPost(account1, 'content-id', []);
|
||||
await initiateValidationPool({ postId: 'content-id' });
|
||||
await dao.evaluateOutcome(0);
|
||||
|
@ -147,8 +156,8 @@ describe('Forum', () => {
|
|||
{ weightPPM: 1000000, targetPostId: 'second-content-id' },
|
||||
]);
|
||||
await initiateValidationPool({ postId: 'third-content-id' });
|
||||
const pool = await dao.getValidationPool(1);
|
||||
expect(pool.props.postId).to.equal('third-content-id');
|
||||
const pool = await dao.validationPools(1);
|
||||
expect(pool.postId).to.equal('third-content-id');
|
||||
await time.increase(POOL_DURATION + 1);
|
||||
await dao.evaluateOutcome(1);
|
||||
expect(await dao.balanceOf(account1)).to.equal(0);
|
||||
|
@ -156,7 +165,7 @@ describe('Forum', () => {
|
|||
expect(await dao.balanceOf(account3)).to.equal(0);
|
||||
});
|
||||
|
||||
it('should be able to reverse a negative reference with a negative reference', async () => {
|
||||
it('should be able to reverse a negative citation with a negative citation', async () => {
|
||||
await addPost(account1, 'content-id', []);
|
||||
await initiateValidationPool({ postId: 'content-id' });
|
||||
await dao.evaluateOutcome(0);
|
||||
|
@ -183,8 +192,8 @@ describe('Forum', () => {
|
|||
{ weightPPM: 100000, targetPostId: 'nonexistent-content-id' },
|
||||
]);
|
||||
await initiateValidationPool({ postId: 'second-content-id' });
|
||||
const pool = await dao.getValidationPool(0);
|
||||
expect(pool.props.postId).to.equal('second-content-id');
|
||||
const pool = await dao.validationPools(0);
|
||||
expect(pool.postId).to.equal('second-content-id');
|
||||
await dao.evaluateOutcome(0);
|
||||
expect(await dao.balanceOf(account1)).to.equal(10);
|
||||
expect(await dao.balanceOf(account2)).to.equal(90);
|
||||
|
@ -212,6 +221,7 @@ describe('Forum', () => {
|
|||
});
|
||||
|
||||
it('should limit effects of negative references on prior positive references', async () => {
|
||||
console.log('First post');
|
||||
await addPost(account1, 'content-id', []);
|
||||
await initiateValidationPool({ postId: 'content-id' });
|
||||
await dao.evaluateOutcome(0);
|
||||
|
@ -253,15 +263,21 @@ describe('Forum', () => {
|
|||
|
||||
it('should enforce depth limit', async () => {
|
||||
await addPost(account1, 'content-id-1', []);
|
||||
await addPost(account2, 'content-id-2', [{ weightPPM: 1000000, targetPostId: 'content-id-1' }]);
|
||||
await addPost(account3, 'content-id-3', [{ weightPPM: 1000000, targetPostId: 'content-id-2' }]);
|
||||
await addPost(account4, 'content-id-4', [{ weightPPM: 1000000, targetPostId: 'content-id-3' }]);
|
||||
await addPost(account1, 'content-id-2', [{ weightPPM: 1000000, targetPostId: 'content-id-1' }]);
|
||||
await addPost(account1, 'content-id-3', [{ weightPPM: 1000000, targetPostId: 'content-id-2' }]);
|
||||
await addPost(account1, 'content-id-4', [{ weightPPM: 1000000, targetPostId: 'content-id-3' }]);
|
||||
await initiateValidationPool({ postId: 'content-id-4' });
|
||||
await dao.evaluateOutcome(0);
|
||||
expect(await dao.balanceOf(account1)).to.equal(0);
|
||||
expect(await dao.balanceOf(account2)).to.equal(100);
|
||||
expect(await dao.balanceOf(account3)).to.equal(0);
|
||||
expect(await dao.balanceOf(account4)).to.equal(0);
|
||||
const posts = await Promise.all([
|
||||
await dao.posts('content-id-1'),
|
||||
await dao.posts('content-id-2'),
|
||||
await dao.posts('content-id-3'),
|
||||
await dao.posts('content-id-4'),
|
||||
]);
|
||||
expect(posts[0].reputation).to.equal(0);
|
||||
expect(posts[1].reputation).to.equal(100);
|
||||
expect(posts[2].reputation).to.equal(0);
|
||||
expect(posts[3].reputation).to.equal(0);
|
||||
});
|
||||
|
||||
it('should be able to incinerate reputation', async () => {
|
||||
|
@ -274,16 +290,18 @@ describe('Forum', () => {
|
|||
await initiateValidationPool({ postId: 'content-id-1' });
|
||||
expect(await dao.totalSupply()).to.equal(100);
|
||||
await dao.evaluateOutcome(0);
|
||||
expect((await dao.posts('content-id-1')).reputation).to.equal(50);
|
||||
expect(await dao.totalSupply()).to.equal(50);
|
||||
});
|
||||
|
||||
describe('negative reference of a post, the author having already staked and lost reputation', async () => {
|
||||
describe('negative citation of a post, the author having already staked and lost reputation', async () => {
|
||||
beforeEach(async () => {
|
||||
await addPost(account1, 'content-id', []);
|
||||
await initiateValidationPool({ postId: 'content-id' });
|
||||
await dao.evaluateOutcome(0);
|
||||
expect(await dao.balanceOf(account1)).to.equal(100);
|
||||
expect(await dao.totalSupply()).to.equal(100);
|
||||
expect((await dao.posts('content-id')).reputation).to.equal(100);
|
||||
|
||||
await addPost(account2, 'second-content-id', []);
|
||||
await initiateValidationPool({ postId: 'second-content-id' });
|
||||
|
@ -292,6 +310,8 @@ describe('Forum', () => {
|
|||
expect(await dao.balanceOf(account1)).to.equal(100);
|
||||
expect(await dao.balanceOf(account2)).to.equal(100);
|
||||
expect(await dao.totalSupply()).to.equal(200);
|
||||
expect((await dao.posts('content-id')).reputation).to.equal(100);
|
||||
expect((await dao.posts('second-content-id')).reputation).to.equal(100);
|
||||
|
||||
// account1 stakes and loses
|
||||
await initiateValidationPool({ postId: 'second-content-id' });
|
||||
|
@ -302,6 +322,8 @@ describe('Forum', () => {
|
|||
expect(await dao.balanceOf(account1)).to.equal(50);
|
||||
expect(await dao.balanceOf(account2)).to.equal(250);
|
||||
expect(await dao.totalSupply()).to.equal(300);
|
||||
expect((await dao.posts('content-id')).reputation).to.equal(100);
|
||||
expect((await dao.posts('second-content-id')).reputation).to.equal(100);
|
||||
});
|
||||
|
||||
it('author and post rep can be completely destroyed', async () => {
|
||||
|
@ -314,6 +336,9 @@ describe('Forum', () => {
|
|||
expect(await dao.balanceOf(account2)).to.equal(250);
|
||||
expect(await dao.balanceOf(account3)).to.equal(250);
|
||||
expect(await dao.totalSupply()).to.equal(500);
|
||||
expect((await dao.posts('content-id')).reputation).to.equal(0);
|
||||
expect((await dao.posts('second-content-id')).reputation).to.equal(100);
|
||||
expect((await dao.posts('third-content-id')).reputation).to.equal(250);
|
||||
});
|
||||
|
||||
it('author rep can be destroyed while some post rep remains', async () => {
|
||||
|
@ -326,6 +351,9 @@ describe('Forum', () => {
|
|||
expect(await dao.balanceOf(account1)).to.equal(0);
|
||||
expect(await dao.balanceOf(account2)).to.equal(250);
|
||||
expect(await dao.balanceOf(account3)).to.equal(120);
|
||||
expect((await dao.posts('content-id')).reputation).to.equal(30);
|
||||
expect((await dao.posts('second-content-id')).reputation).to.equal(100);
|
||||
expect((await dao.posts('third-content-id')).reputation).to.equal(120);
|
||||
});
|
||||
|
||||
it('author rep can be destroyed while some post rep remains (odd amount)', async () => {
|
||||
|
@ -338,12 +366,15 @@ describe('Forum', () => {
|
|||
expect(await dao.balanceOf(account1)).to.equal(0);
|
||||
expect(await dao.balanceOf(account2)).to.equal(250);
|
||||
expect(await dao.balanceOf(account3)).to.equal(125);
|
||||
expect((await dao.posts('content-id')).reputation).to.equal(25);
|
||||
expect((await dao.posts('second-content-id')).reputation).to.equal(100);
|
||||
expect((await dao.posts('third-content-id')).reputation).to.equal(125);
|
||||
});
|
||||
});
|
||||
|
||||
describe('negative reference of a post with multiple authors', async () => {
|
||||
describe('negative citation of a post with multiple authors', async () => {
|
||||
beforeEach(async () => {
|
||||
await forum.addPost([
|
||||
await dao.addPost([
|
||||
{ weightPPM: 500000, authorAddress: account1 },
|
||||
{ weightPPM: 500000, authorAddress: account2 },
|
||||
], 'content-id', []);
|
||||
|
@ -352,16 +383,18 @@ describe('Forum', () => {
|
|||
expect(await dao.balanceOf(account1)).to.equal(50);
|
||||
expect(await dao.balanceOf(account2)).to.equal(50);
|
||||
expect(await dao.totalSupply()).to.equal(100);
|
||||
expect((await dao.posts('content-id')).reputation).to.equal(100);
|
||||
|
||||
// account1 stakes and loses
|
||||
await initiateValidationPool({ postId: 'content-id' });
|
||||
await dao.stakeOnValidationPool(1, 25, true);
|
||||
await dao.connect(account2).stakeOnValidationPool(1, 50, false);
|
||||
await dao.connect(account2).stakeOnValidationPool(1, 60, false);
|
||||
await time.increase(POOL_DURATION + 1);
|
||||
await dao.evaluateOutcome(1);
|
||||
expect(await dao.balanceOf(account1)).to.equal(25);
|
||||
expect(await dao.balanceOf(account2)).to.equal(175);
|
||||
expect(await dao.totalSupply()).to.equal(200);
|
||||
expect((await dao.posts('content-id')).reputation).to.equal(100);
|
||||
});
|
||||
|
||||
it('author and post rep can be completely destroyed', async () => {
|
||||
|
@ -371,9 +404,11 @@ describe('Forum', () => {
|
|||
await time.increase(POOL_DURATION + 1);
|
||||
await dao.evaluateOutcome(2);
|
||||
expect(await dao.balanceOf(account1)).to.equal(0);
|
||||
expect(await dao.balanceOf(account2)).to.equal(0);
|
||||
expect(await dao.balanceOf(account3)).to.equal(600);
|
||||
expect(await dao.balanceOf(account2)).to.equal(125);
|
||||
expect(await dao.balanceOf(account3)).to.equal(475);
|
||||
expect(await dao.totalSupply()).to.equal(600);
|
||||
expect((await dao.posts('content-id')).reputation).to.equal(0);
|
||||
expect((await dao.posts('second-content-id')).reputation).to.equal(475);
|
||||
});
|
||||
|
||||
it('author rep can be destroyed while some post rep remains', async () => {
|
||||
|
@ -386,6 +421,8 @@ describe('Forum', () => {
|
|||
expect(await dao.balanceOf(account1)).to.equal(0);
|
||||
expect(await dao.balanceOf(account2)).to.equal(140);
|
||||
expect(await dao.balanceOf(account3)).to.equal(130);
|
||||
expect((await dao.posts('content-id')).reputation).to.equal(30);
|
||||
expect((await dao.posts('second-content-id')).reputation).to.equal(130);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,318 +0,0 @@
|
|||
const {
|
||||
time,
|
||||
loadFixture,
|
||||
} = require('@nomicfoundation/hardhat-toolbox/network-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { ethers } = require('hardhat');
|
||||
const deployDAO = require('./util/deploy-dao');
|
||||
|
||||
describe('Lightweight Validation Pools', () => {
|
||||
async function deploy() {
|
||||
const [account1, account2] = await ethers.getSigners();
|
||||
const { dao, forum } = await deployDAO();
|
||||
return {
|
||||
dao, forum, account1, account2,
|
||||
};
|
||||
}
|
||||
let dao;
|
||||
let forum;
|
||||
let account1;
|
||||
let account2;
|
||||
const POOL_DURATION = 3600; // 1 hour
|
||||
const POOL_FEE = 100;
|
||||
const emptyCallbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
|
||||
|
||||
const initiateValidationPool = ({
|
||||
postId, duration,
|
||||
quorum, winRatio, bindingPercent,
|
||||
redistributeLosingStakes, callbackOnValidate,
|
||||
callbackData, fee,
|
||||
} = {}) => dao.initiateLWValidationPool(
|
||||
postId ?? 'content-id',
|
||||
duration ?? POOL_DURATION,
|
||||
quorum ?? [1, 3],
|
||||
winRatio ?? [1, 2],
|
||||
bindingPercent ?? 100,
|
||||
redistributeLosingStakes ?? true,
|
||||
callbackOnValidate ?? false,
|
||||
callbackData ?? emptyCallbackData,
|
||||
{ value: fee ?? POOL_FEE },
|
||||
);
|
||||
|
||||
beforeEach(async () => {
|
||||
({
|
||||
dao, forum, account1, account2,
|
||||
} = await loadFixture(deploy));
|
||||
await forum.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'content-id', []);
|
||||
const init = () => initiateValidationPool({ fee: POOL_FEE });
|
||||
await expect(init()).to.emit(dao, 'LWValidationPoolInitiated').withArgs(0);
|
||||
expect(await dao.getLWValidationPoolCount()).to.equal(1);
|
||||
expect(await dao.memberCount()).to.equal(0);
|
||||
expect(await dao.balanceOf(account1)).to.equal(0);
|
||||
expect(await dao.totalSupply()).to.equal(POOL_FEE);
|
||||
});
|
||||
|
||||
describe('Initiate', () => {
|
||||
it('should be able to initiate a validation pool without a fee', async () => {
|
||||
const init = () => initiateValidationPool({ fee: 0 });
|
||||
await expect(init()).to.emit(dao, 'LWValidationPoolInitiated');
|
||||
});
|
||||
|
||||
it('should not be able to initiate a validation pool with a quorum below the minimum', async () => {
|
||||
const init = () => initiateValidationPool({ quorum: [1, 11] });
|
||||
await expect(init()).to.be.revertedWith('Quorum is below minimum');
|
||||
});
|
||||
|
||||
it('should not be able to initiate a validation pool with a quorum greater than 1', async () => {
|
||||
const init = () => initiateValidationPool({ quorum: [11, 10] });
|
||||
await expect(init()).to.be.revertedWith('Quorum is greater than one');
|
||||
});
|
||||
|
||||
it('should not be able to initiate a validation pool with duration below minimum', async () => {
|
||||
const init = () => initiateValidationPool({ duration: 0 });
|
||||
await expect(init()).to.be.revertedWith('Duration is too short');
|
||||
});
|
||||
|
||||
it('should not be able to initiate a validation pool with duration above maximum', async () => {
|
||||
const init = () => initiateValidationPool({ duration: 40000000000000 });
|
||||
await expect(init()).to.be.revertedWith('Duration is too long');
|
||||
});
|
||||
|
||||
it('should not be able to initiate a validation pool with bindingPercent above 100', async () => {
|
||||
const init = () => initiateValidationPool({ bindingPercent: 101 });
|
||||
await expect(init()).to.be.revertedWith('Binding percent must be <= 100');
|
||||
});
|
||||
|
||||
it('should be able to initiate a second validation pool', async () => {
|
||||
const init = () => initiateValidationPool();
|
||||
await expect(init()).to.emit(dao, 'LWValidationPoolInitiated').withArgs(1);
|
||||
expect(await dao.getLWValidationPoolCount()).to.equal(2);
|
||||
});
|
||||
|
||||
it('Should be able to fetch pool instance', async () => {
|
||||
const pool = await dao.getLWValidationPool(0);
|
||||
expect(pool).to.exist;
|
||||
expect(pool.params.duration).to.equal(POOL_DURATION);
|
||||
expect(pool.props.postId).to.equal('content-id');
|
||||
expect(pool.props.resolved).to.be.false;
|
||||
expect(pool.sender).to.equal(account1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Propose Result', () => {
|
||||
it('should not be able to propose an empty result', async () => {
|
||||
await expect(dao.proposeLWResult(0, 'some-hash', [])).to.be.revertedWith('The proposed result contains no transfers');
|
||||
});
|
||||
|
||||
it('should be able to propose a result', async () => {
|
||||
await expect(dao.proposeLWResult(0, 'some-hash', [{ from: account1, to: account2, amount: 0 }])).to.emit(dao, 'LWResultProposed').withArgs(0, 0, 'some-hash');
|
||||
await expect(dao.proposeLWResult(0, 'some-other-hash', [{ from: account1, to: account2, amount: 0 }])).to.emit(dao, 'LWResultProposed').withArgs(0, 1, 'some-other-hash');
|
||||
});
|
||||
|
||||
it('should not be able to propose the same result twice', async () => {
|
||||
await expect(dao.proposeLWResult(0, 'some-hash', [{ from: account1, to: account2, amount: 0 }])).to.emit(dao, 'LWResultProposed').withArgs(0, 0, 'some-hash');
|
||||
await expect(dao.proposeLWResult(0, 'some-hash', [{ from: account1, to: account2, amount: 0 }])).to.be.revertedWith('This result hash has already been proposed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Stake', async () => {
|
||||
beforeEach(async () => {
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await dao.evaluateOutcome(0);
|
||||
expect(await dao.balanceOf(account1)).to.equal(100);
|
||||
expect(await dao.balanceOf(dao.target)).to.equal(0);
|
||||
await initiateValidationPool();
|
||||
expect(await dao.balanceOf(account1)).to.equal(100);
|
||||
expect(await dao.balanceOf(dao.target)).to.equal(100);
|
||||
});
|
||||
|
||||
it('should be able to stake before validation pool has elapsed', async () => {
|
||||
await dao.stakeOnLWValidationPool(1, 10, true);
|
||||
expect(await dao.balanceOf(account1)).to.equal(100);
|
||||
expect(await dao.balanceOf(dao.target)).to.equal(100);
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true, true);
|
||||
expect(await dao.balanceOf(account1)).to.equal(200);
|
||||
expect(await dao.balanceOf(dao.target)).to.equal(0);
|
||||
});
|
||||
|
||||
it('should not be able to stake after validation pool has elapsed', async () => {
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await expect(dao.stakeOnValidationPool(1, 10, true)).to.be.revertedWith('Pool end time has passed');
|
||||
});
|
||||
|
||||
it('should be able to stake against a validation pool', async () => {
|
||||
await dao.stakeOnValidationPool(1, 10, false);
|
||||
expect(await dao.balanceOf(account1)).to.equal(100);
|
||||
expect(await dao.balanceOf(dao.target)).to.equal(100);
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, false, true);
|
||||
expect(await dao.balanceOf(account1)).to.equal(200);
|
||||
expect(await dao.balanceOf(dao.target)).to.equal(0);
|
||||
const pool = await dao.getValidationPool(1);
|
||||
expect(pool.props.outcome).to.be.false;
|
||||
});
|
||||
|
||||
it('should not be able to stake more REP than the sender owns', async () => {
|
||||
await expect(dao.stakeOnValidationPool(1, 200, true)).to.be.revertedWith('Insufficient REP balance to cover stake');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delegated stake', () => {
|
||||
it('should stake the lesser of the allowed amount or the owner\'s remaining balance', async () => {
|
||||
// TODO: owner delegates stake and then loses rep
|
||||
});
|
||||
});
|
||||
|
||||
describe('Evaluate outcome', () => {
|
||||
it('should not be able to evaluate outcome before duration has elapsed if not all rep has been staked', async () => {
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await expect(dao.evaluateOutcome(0));
|
||||
await initiateValidationPool({ fee: 100 });
|
||||
await expect(dao.evaluateOutcome(1)).to.be.revertedWith('Pool end time has not yet arrived');
|
||||
});
|
||||
|
||||
it('should not be able to evaluate outcome before duration has elapsed unless all rep has been staked', async () => {
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await expect(dao.evaluateOutcome(0));
|
||||
await initiateValidationPool({ fee: 100 });
|
||||
await dao.stakeOnValidationPool(1, 100, true);
|
||||
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true, true);
|
||||
});
|
||||
|
||||
it('should be able to evaluate outcome after duration has elapsed', async () => {
|
||||
expect(await dao.balanceOf(dao.target)).to.equal(100);
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true, true);
|
||||
expect(await dao.memberCount()).to.equal(1);
|
||||
expect(await dao.balanceOf(account1)).to.equal(100);
|
||||
const pool = await dao.getValidationPool(0);
|
||||
expect(pool.props.resolved).to.be.true;
|
||||
expect(pool.props.outcome).to.be.true;
|
||||
});
|
||||
|
||||
it('should not be able to evaluate outcome more than once', async () => {
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true, true);
|
||||
await expect(dao.evaluateOutcome(0)).to.be.revertedWith('Pool is already resolved');
|
||||
});
|
||||
|
||||
it('should be able to evaluate outcome of second validation pool', async () => {
|
||||
const init = () => initiateValidationPool();
|
||||
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
|
||||
expect(await dao.getValidationPoolCount()).to.equal(2);
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true, true);
|
||||
expect(await dao.balanceOf(account1)).to.equal(100);
|
||||
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true, true);
|
||||
expect(await dao.balanceOf(account1)).to.equal(200);
|
||||
});
|
||||
|
||||
it('should not be able to evaluate outcome if quorum is not met', async () => {
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true, true);
|
||||
|
||||
const init = () => initiateValidationPool({ quorum: [1, 1] });
|
||||
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
|
||||
expect(await dao.getValidationPoolCount()).to.equal(2);
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, false, false);
|
||||
});
|
||||
|
||||
describe('Validation pool options', () => {
|
||||
beforeEach(async () => {
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await dao.evaluateOutcome(0);
|
||||
await forum.addPost([{ weightPPM: 1000000, authorAddress: account2 }], 'content-id-2', []);
|
||||
const init = () => initiateValidationPool({ postId: 'content-id-2' });
|
||||
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await dao.evaluateOutcome(1);
|
||||
});
|
||||
|
||||
it('Binding validation pool should redistribute stakes', async () => {
|
||||
const init = () => initiateValidationPool();
|
||||
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
|
||||
await dao.connect(account1).stakeOnValidationPool(2, 10, true);
|
||||
await dao.connect(account2).stakeOnValidationPool(2, 10, false);
|
||||
expect(await dao.balanceOf(account1)).to.equal(100);
|
||||
expect(await dao.balanceOf(account2)).to.equal(100);
|
||||
expect(await dao.balanceOf(dao.target)).to.equal(100);
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await dao.evaluateOutcome(2);
|
||||
expect(await dao.balanceOf(account1)).to.equal(210);
|
||||
expect(await dao.balanceOf(account2)).to.equal(90);
|
||||
expect(await dao.balanceOf(dao.target)).to.equal(0);
|
||||
});
|
||||
|
||||
it('Non binding validation pool should not redistribute stakes', async () => {
|
||||
const init = () => initiateValidationPool({ bindingPercent: 0 });
|
||||
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
|
||||
await dao.connect(account1).stakeOnValidationPool(2, 10, true);
|
||||
await dao.connect(account2).stakeOnValidationPool(2, 10, false);
|
||||
expect(await dao.balanceOf(account1)).to.equal(100);
|
||||
expect(await dao.balanceOf(account2)).to.equal(100);
|
||||
expect(await dao.balanceOf(dao.target)).to.equal(100);
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await dao.evaluateOutcome(2);
|
||||
expect(await dao.balanceOf(account1)).to.equal(200);
|
||||
expect(await dao.balanceOf(account2)).to.equal(100);
|
||||
expect(await dao.balanceOf(dao.target)).to.equal(0);
|
||||
});
|
||||
|
||||
it('Partially binding validation pool should redistribute some stakes', async () => {
|
||||
const init = () => initiateValidationPool({ bindingPercent: 50 });
|
||||
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
|
||||
await dao.connect(account1).stakeOnValidationPool(2, 10, true);
|
||||
await dao.connect(account2).stakeOnValidationPool(2, 10, false);
|
||||
expect(await dao.balanceOf(account1)).to.equal(100);
|
||||
expect(await dao.balanceOf(account2)).to.equal(100);
|
||||
expect(await dao.balanceOf(dao.target)).to.equal(100);
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await dao.evaluateOutcome(2);
|
||||
expect(await dao.balanceOf(account1)).to.equal(205);
|
||||
expect(await dao.balanceOf(account2)).to.equal(95);
|
||||
expect(await dao.balanceOf(dao.target)).to.equal(0);
|
||||
expect(await dao.totalSupply()).to.equal(300);
|
||||
});
|
||||
|
||||
it('If redistributeLosingStakes is false, validation pool should burn binding portion of losing stakes', async () => {
|
||||
const init = () => initiateValidationPool({
|
||||
bindingPercent: 50,
|
||||
redistributeLosingStakes: false,
|
||||
});
|
||||
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
|
||||
await dao.connect(account1).stakeOnValidationPool(2, 10, true);
|
||||
await dao.connect(account2).stakeOnValidationPool(2, 10, false);
|
||||
expect(await dao.balanceOf(account1)).to.equal(100);
|
||||
expect(await dao.balanceOf(account2)).to.equal(100);
|
||||
expect(await dao.balanceOf(dao.target)).to.equal(100);
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await dao.evaluateOutcome(2);
|
||||
expect(await dao.balanceOf(account1)).to.equal(200);
|
||||
expect(await dao.balanceOf(account2)).to.equal(95);
|
||||
expect(await dao.balanceOf(dao.target)).to.equal(0);
|
||||
expect(await dao.totalSupply()).to.equal(295);
|
||||
});
|
||||
|
||||
it('If redistributeLosingStakes is false and bindingPercent is 0, accounts should recover initial balances', async () => {
|
||||
const init = () => initiateValidationPool({
|
||||
bindingPercent: 0,
|
||||
redistributeLosingStakes: false,
|
||||
});
|
||||
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
|
||||
await dao.connect(account1).stakeOnValidationPool(2, 10, true);
|
||||
await dao.connect(account2).stakeOnValidationPool(2, 10, false);
|
||||
expect(await dao.balanceOf(account1)).to.equal(100);
|
||||
expect(await dao.balanceOf(account2)).to.equal(100);
|
||||
expect(await dao.balanceOf(dao.target)).to.equal(100);
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await dao.evaluateOutcome(2);
|
||||
expect(await dao.balanceOf(account1)).to.equal(200);
|
||||
expect(await dao.balanceOf(account2)).to.equal(100);
|
||||
expect(await dao.balanceOf(dao.target)).to.equal(0);
|
||||
expect(await dao.totalSupply()).to.equal(300);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,7 +4,6 @@ const {
|
|||
} = require('@nomicfoundation/hardhat-toolbox/network-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { ethers } = require('hardhat');
|
||||
const deployDAO = require('./util/deploy-dao');
|
||||
|
||||
describe('Onboarding', () => {
|
||||
const PRICE = 100;
|
||||
|
@ -13,13 +12,14 @@ describe('Onboarding', () => {
|
|||
// Contracts are deployed using the first signer/account by default
|
||||
const [account1, account2] = await ethers.getSigners();
|
||||
|
||||
const { dao, forum } = await deployDAO();
|
||||
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 Onboarding = await ethers.getContractFactory('Onboarding');
|
||||
const onboarding = await Onboarding.deploy(dao.target, forum.target, proposals.target, PRICE);
|
||||
const onboarding = await Onboarding.deploy(dao.target, proposals.target, PRICE);
|
||||
|
||||
await forum.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'content-id', []);
|
||||
await dao.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'content-id', []);
|
||||
const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
|
||||
await dao.initiateValidationPool(
|
||||
'content-id',
|
||||
|
@ -37,7 +37,7 @@ describe('Onboarding', () => {
|
|||
expect(await dao.balanceOf(account1)).to.equal(100);
|
||||
|
||||
return {
|
||||
dao, forum, onboarding, account1, account2,
|
||||
dao, onboarding, account1, account2,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -53,14 +53,13 @@ describe('Onboarding', () => {
|
|||
|
||||
describe('Work approval/disapproval', () => {
|
||||
let dao;
|
||||
let forum;
|
||||
let onboarding;
|
||||
let account1;
|
||||
let account2;
|
||||
|
||||
beforeEach(async () => {
|
||||
({
|
||||
dao, forum, onboarding, account1, account2,
|
||||
dao, onboarding, account1, account2,
|
||||
} = await loadFixture(deploy));
|
||||
await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION);
|
||||
});
|
||||
|
@ -71,14 +70,16 @@ describe('Onboarding', () => {
|
|||
await expect(onboarding.submitWorkApproval(0, true))
|
||||
.to.emit(dao, 'ValidationPoolInitiated').withArgs(1)
|
||||
.to.emit(onboarding, 'WorkApprovalSubmitted').withArgs(0, true);
|
||||
const post = await forum.getPost('evidence-content-id');
|
||||
const post = await dao.posts('evidence-content-id');
|
||||
expect(post.sender).to.equal(onboarding.target);
|
||||
expect(post.authors).to.have.length(1);
|
||||
expect(post.authors[0].weightPPM).to.equal(1000000);
|
||||
expect(post.authors[0].authorAddress).to.equal(account1);
|
||||
const pool = await dao.getValidationPool(1);
|
||||
expect(pool.props.postId).to.equal('evidence-content-id');
|
||||
expect(pool.props.fee).to.equal(PRICE * 0.9);
|
||||
expect(post.id).to.equal('evidence-content-id');
|
||||
const postAuthors = await dao.getPostAuthors('evidence-content-id');
|
||||
expect(postAuthors).to.have.length(1);
|
||||
expect(postAuthors[0].weightPPM).to.equal(1000000);
|
||||
expect(postAuthors[0].authorAddress).to.equal(account1);
|
||||
const pool = await dao.validationPools(1);
|
||||
expect(pool.postId).to.equal('evidence-content-id');
|
||||
expect(pool.fee).to.equal(PRICE * 0.9);
|
||||
expect(pool.sender).to.equal(onboarding.target);
|
||||
});
|
||||
|
||||
|
@ -113,7 +114,7 @@ describe('Onboarding', () => {
|
|||
describe('Onboarding followup', () => {
|
||||
it('resolving the first validation pool should trigger a second pool', async () => {
|
||||
const {
|
||||
dao, forum, onboarding, account2,
|
||||
dao, onboarding, account2,
|
||||
} = await loadFixture(deploy);
|
||||
await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION);
|
||||
await onboarding.connect(account2).requestWork('req-content-id', { value: PRICE });
|
||||
|
@ -121,22 +122,24 @@ describe('Onboarding', () => {
|
|||
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);
|
||||
expect(await forum.postCount()).to.equal(3);
|
||||
const post = await forum.getPost('req-content-id');
|
||||
expect(await dao.postCount()).to.equal(3);
|
||||
const post = await dao.posts('req-content-id');
|
||||
expect(post.sender).to.equal(onboarding.target);
|
||||
expect(post.authors).to.have.length(1);
|
||||
expect(post.authors[0].weightPPM).to.equal(1000000);
|
||||
expect(post.authors[0].authorAddress).to.equal(account2);
|
||||
const pool = await dao.getValidationPool(2);
|
||||
expect(pool.props.postId).to.equal('req-content-id');
|
||||
expect(pool.props.fee).to.equal(PRICE * 0.1);
|
||||
expect(post.id).to.equal('req-content-id');
|
||||
const postAuthors = await dao.getPostAuthors('req-content-id');
|
||||
expect(postAuthors).to.have.length(1);
|
||||
expect(postAuthors[0].weightPPM).to.equal(1000000);
|
||||
expect(postAuthors[0].authorAddress).to.equal(account2);
|
||||
const pool = await dao.validationPools(2);
|
||||
expect(pool.postId).to.equal('req-content-id');
|
||||
expect(pool.fee).to.equal(PRICE * 0.1);
|
||||
expect(pool.sender).to.equal(onboarding.target);
|
||||
expect(pool.props.fee);
|
||||
expect(pool.fee);
|
||||
});
|
||||
|
||||
it('if the first validation pool is rejected it should not trigger a second pool', async () => {
|
||||
const {
|
||||
dao, forum, onboarding, account2,
|
||||
dao, onboarding, account2,
|
||||
} = await loadFixture(deploy);
|
||||
await dao.stakeAvailability(onboarding.target, 40, STAKE_DURATION);
|
||||
await onboarding.connect(account2).requestWork('req-content-id', { value: PRICE });
|
||||
|
@ -145,7 +148,7 @@ describe('Onboarding', () => {
|
|||
await dao.stakeOnValidationPool(1, 60, false);
|
||||
await time.increase(86401);
|
||||
await expect(dao.evaluateOutcome(1)).not.to.emit(dao, 'ValidationPoolInitiated');
|
||||
expect(await forum.postCount()).to.equal(2);
|
||||
expect(await dao.postCount()).to.equal(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,19 +5,19 @@ const {
|
|||
const { expect } = require('chai');
|
||||
const { ethers } = require('hardhat');
|
||||
const { beforeEach } = require('mocha');
|
||||
const deployDAO = require('./util/deploy-dao');
|
||||
|
||||
describe('Proposal', () => {
|
||||
async function deploy() {
|
||||
// Contracts are deployed using the first signer/account by default
|
||||
const [account1, account2] = await ethers.getSigners();
|
||||
|
||||
const { dao, forum } = await deployDAO();
|
||||
const DAO = await ethers.getContractFactory('DAO');
|
||||
const dao = await DAO.deploy();
|
||||
const Proposals = await ethers.getContractFactory('Proposals');
|
||||
const proposals = await Proposals.deploy(dao.target);
|
||||
|
||||
await forum.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'some-content-id', []);
|
||||
await forum.addPost([{ weightPPM: 1000000, authorAddress: account2 }], 'some-other-content-id', []);
|
||||
await dao.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'some-content-id', []);
|
||||
await dao.addPost([{ weightPPM: 1000000, authorAddress: account2 }], 'some-other-content-id', []);
|
||||
const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
|
||||
await dao.initiateValidationPool(
|
||||
'some-content-id',
|
||||
|
@ -46,7 +46,7 @@ describe('Proposal', () => {
|
|||
await dao.evaluateOutcome(1);
|
||||
|
||||
return {
|
||||
dao, forum, proposals, account1, account2,
|
||||
dao, proposals, account1, account2,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,6 @@ describe('Proposal', () => {
|
|||
|
||||
describe('Attestation', () => {
|
||||
let dao;
|
||||
let forum;
|
||||
let proposals;
|
||||
let account1;
|
||||
let account2;
|
||||
|
@ -74,15 +73,13 @@ describe('Proposal', () => {
|
|||
beforeEach(async () => {
|
||||
({
|
||||
dao,
|
||||
forum,
|
||||
proposals,
|
||||
account1,
|
||||
account2,
|
||||
} = await loadFixture(deploy));
|
||||
|
||||
const emptyCallbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
|
||||
await forum.addPost([{ authorAddress: account1, weightPPM: 1000000 }], 'proposal-content-id', []);
|
||||
await proposals.propose('proposal-content-id', [20, 20, 20], false, emptyCallbackData, { value: 100 });
|
||||
await proposals.propose('proposal-content-id', account1, [20, 20, 20], false, emptyCallbackData, { value: 100 });
|
||||
expect(await proposals.proposalCount()).to.equal(1);
|
||||
proposal = await proposals.proposals(0);
|
||||
expect(proposal.postId).to.equal('proposal-content-id');
|
||||
|
@ -90,10 +87,10 @@ describe('Proposal', () => {
|
|||
});
|
||||
|
||||
it('Can submit a proposal', async () => {
|
||||
const post = await forum.getPost('proposal-content-id');
|
||||
expect(post.authors).to.have.length(1);
|
||||
expect(post.authors[0].weightPPM).to.equal(1000000);
|
||||
expect(post.authors[0].authorAddress).to.equal(account1);
|
||||
const postAuthors = await dao.getPostAuthors('proposal-content-id');
|
||||
expect(postAuthors).to.have.length(1);
|
||||
expect(postAuthors[0].weightPPM).to.equal(1000000);
|
||||
expect(postAuthors[0].authorAddress).to.equal(account1);
|
||||
});
|
||||
|
||||
it('Can attest for a proposal', async () => {
|
||||
|
@ -224,8 +221,8 @@ describe('Proposal', () => {
|
|||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const pool = await dao.getValidationPool(3);
|
||||
expect(pool.props.resolved).to.be.true;
|
||||
const pool = await dao.validationPools(3);
|
||||
expect(pool.resolved).to.be.true;
|
||||
});
|
||||
|
||||
it('proposal dies if it fails to meet quorum', async () => {
|
||||
|
@ -310,8 +307,8 @@ describe('Proposal', () => {
|
|||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const pool = await dao.getValidationPool(4);
|
||||
expect(pool.props.resolved).to.be.true;
|
||||
const pool = await dao.validationPools(4);
|
||||
expect(pool.resolved).to.be.true;
|
||||
});
|
||||
|
||||
it('proposal dies if it fails to meet quorum', async () => {
|
||||
|
|
|
@ -4,18 +4,15 @@ const {
|
|||
} = require('@nomicfoundation/hardhat-toolbox/network-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { ethers } = require('hardhat');
|
||||
const deployDAO = require('./util/deploy-dao');
|
||||
|
||||
describe('Validation Pools', () => {
|
||||
async function deploy() {
|
||||
const [account1, account2] = await ethers.getSigners();
|
||||
const { dao, forum } = await deployDAO();
|
||||
return {
|
||||
dao, forum, account1, account2,
|
||||
};
|
||||
const DAO = await ethers.getContractFactory('DAO');
|
||||
const dao = await DAO.deploy();
|
||||
return { dao, account1, account2 };
|
||||
}
|
||||
let dao;
|
||||
let forum;
|
||||
let account1;
|
||||
let account2;
|
||||
const POOL_DURATION = 3600; // 1 hour
|
||||
|
@ -40,22 +37,20 @@ describe('Validation Pools', () => {
|
|||
);
|
||||
|
||||
beforeEach(async () => {
|
||||
({
|
||||
dao, forum, account1, account2,
|
||||
} = await loadFixture(deploy));
|
||||
await forum.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'content-id', []);
|
||||
({ dao, account1, account2 } = await loadFixture(deploy));
|
||||
await dao.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'content-id', []);
|
||||
const init = () => initiateValidationPool({ fee: POOL_FEE });
|
||||
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(0);
|
||||
expect(await dao.getValidationPoolCount()).to.equal(1);
|
||||
expect(await dao.validationPoolCount()).to.equal(1);
|
||||
expect(await dao.memberCount()).to.equal(0);
|
||||
expect(await dao.balanceOf(account1)).to.equal(0);
|
||||
expect(await dao.totalSupply()).to.equal(POOL_FEE);
|
||||
});
|
||||
|
||||
describe('Initiate', () => {
|
||||
it('should be able to initiate a validation pool without a fee', async () => {
|
||||
it('should not be able to initiate a validation pool without a fee', async () => {
|
||||
const init = () => initiateValidationPool({ fee: 0 });
|
||||
await expect(init()).to.emit(dao, 'ValidationPoolInitiated');
|
||||
await expect(init()).to.be.revertedWith('Fee is required to initiate validation pool');
|
||||
});
|
||||
|
||||
it('should not be able to initiate a validation pool with a quorum below the minimum', async () => {
|
||||
|
@ -86,15 +81,15 @@ describe('Validation Pools', () => {
|
|||
it('should be able to initiate a second validation pool', async () => {
|
||||
const init = () => initiateValidationPool();
|
||||
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
|
||||
expect(await dao.getValidationPoolCount()).to.equal(2);
|
||||
expect(await dao.validationPoolCount()).to.equal(2);
|
||||
});
|
||||
|
||||
it('Should be able to fetch pool instance', async () => {
|
||||
const pool = await dao.getValidationPool(0);
|
||||
const pool = await dao.validationPools(0);
|
||||
expect(pool).to.exist;
|
||||
expect(pool.params.duration).to.equal(POOL_DURATION);
|
||||
expect(pool.props.postId).to.equal('content-id');
|
||||
expect(pool.props.resolved).to.be.false;
|
||||
expect(pool.postId).to.equal('content-id');
|
||||
expect(pool.resolved).to.be.false;
|
||||
expect(pool.sender).to.equal(account1);
|
||||
});
|
||||
});
|
||||
|
@ -133,18 +128,8 @@ describe('Validation Pools', () => {
|
|||
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, false, true);
|
||||
expect(await dao.balanceOf(account1)).to.equal(200);
|
||||
expect(await dao.balanceOf(dao.target)).to.equal(0);
|
||||
const pool = await dao.getValidationPool(1);
|
||||
expect(pool.props.outcome).to.be.false;
|
||||
});
|
||||
|
||||
it('should not be able to stake more REP than the sender owns', async () => {
|
||||
await expect(dao.stakeOnValidationPool(1, 200, true)).to.be.revertedWith('Insufficient REP balance to cover stake');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delegated stake', () => {
|
||||
it('should stake the lesser of the allowed amount or the owner\'s remaining balance', async () => {
|
||||
// TODO: owner delegates stake and then loses rep
|
||||
const pool = await dao.validationPools(1);
|
||||
expect(pool.outcome).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -170,9 +155,9 @@ describe('Validation Pools', () => {
|
|||
await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true, true);
|
||||
expect(await dao.memberCount()).to.equal(1);
|
||||
expect(await dao.balanceOf(account1)).to.equal(100);
|
||||
const pool = await dao.getValidationPool(0);
|
||||
expect(pool.props.resolved).to.be.true;
|
||||
expect(pool.props.outcome).to.be.true;
|
||||
const pool = await dao.validationPools(0);
|
||||
expect(pool.resolved).to.be.true;
|
||||
expect(pool.outcome).to.be.true;
|
||||
});
|
||||
|
||||
it('should not be able to evaluate outcome more than once', async () => {
|
||||
|
@ -184,7 +169,7 @@ describe('Validation Pools', () => {
|
|||
it('should be able to evaluate outcome of second validation pool', async () => {
|
||||
const init = () => initiateValidationPool();
|
||||
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
|
||||
expect(await dao.getValidationPoolCount()).to.equal(2);
|
||||
expect(await dao.validationPoolCount()).to.equal(2);
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true, true);
|
||||
expect(await dao.balanceOf(account1)).to.equal(100);
|
||||
|
@ -198,7 +183,7 @@ describe('Validation Pools', () => {
|
|||
|
||||
const init = () => initiateValidationPool({ quorum: [1, 1] });
|
||||
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
|
||||
expect(await dao.getValidationPoolCount()).to.equal(2);
|
||||
expect(await dao.validationPoolCount()).to.equal(2);
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, false, false);
|
||||
});
|
||||
|
@ -207,7 +192,7 @@ describe('Validation Pools', () => {
|
|||
beforeEach(async () => {
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await dao.evaluateOutcome(0);
|
||||
await forum.addPost([{ weightPPM: 1000000, authorAddress: account2 }], 'content-id-2', []);
|
||||
await dao.addPost([{ weightPPM: 1000000, authorAddress: account2 }], 'content-id-2', []);
|
||||
const init = () => initiateValidationPool({ postId: 'content-id-2' });
|
||||
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
|
||||
time.increase(POOL_DURATION + 1);
|
||||
|
|
|
@ -4,7 +4,6 @@ const {
|
|||
} = require('@nomicfoundation/hardhat-toolbox/network-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { ethers } = require('hardhat');
|
||||
const deployDAO = require('./util/deploy-dao');
|
||||
|
||||
describe('Work1', () => {
|
||||
const WORK1_PRICE = 100;
|
||||
|
@ -13,13 +12,14 @@ describe('Work1', () => {
|
|||
// Contracts are deployed using the first signer/account by default
|
||||
const [account1, account2] = await ethers.getSigners();
|
||||
|
||||
const { dao, forum } = await deployDAO();
|
||||
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, forum.target, proposals.target, WORK1_PRICE);
|
||||
const work1 = await Work1.deploy(dao.target, proposals.target, WORK1_PRICE);
|
||||
|
||||
await forum.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'some-content-id', []);
|
||||
await dao.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'some-content-id', []);
|
||||
const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
|
||||
await dao.initiateValidationPool(
|
||||
'some-content-id',
|
||||
|
@ -36,7 +36,7 @@ describe('Work1', () => {
|
|||
await dao.evaluateOutcome(0);
|
||||
|
||||
return {
|
||||
dao, forum, work1, proposals, account1, account2,
|
||||
dao, work1, proposals, account1, account2,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -54,9 +54,12 @@ describe('Work1', () => {
|
|||
let dao;
|
||||
let work1;
|
||||
let account1;
|
||||
let account2;
|
||||
|
||||
beforeEach(async () => {
|
||||
({ dao, work1, account1 } = await loadFixture(deploy));
|
||||
({
|
||||
dao, work1, account1, account2,
|
||||
} = await loadFixture(deploy));
|
||||
await expect(dao.stakeAvailability(work1.target, 50, STAKE_DURATION)).to.emit(work1, 'AvailabilityStaked').withArgs(0);
|
||||
});
|
||||
|
||||
|
@ -69,53 +72,41 @@ describe('Work1', () => {
|
|||
expect(stake.worker).to.equal(account1);
|
||||
expect(stake.amount).to.equal(50);
|
||||
expect(stake.endTime).to.equal(await time.latest() + STAKE_DURATION);
|
||||
expect(await dao.allowance(account1, work1.target)).to.equal(50);
|
||||
});
|
||||
|
||||
it('should not be able to stake availability without reputation value', async () => {
|
||||
await expect(dao.stakeAvailability(work1.target, 0, STAKE_DURATION)).to.be.revertedWith('No stake provided');
|
||||
});
|
||||
|
||||
it('should not be able to call acceptAvailability directly', async () => {
|
||||
await expect(work1.acceptAvailability(account1, 50, STAKE_DURATION)).to.be.revertedWith('acceptAvailability must only be called by DAO contract');
|
||||
});
|
||||
|
||||
it('should be able to extend the duration of an availability stake before it expires', async () => {
|
||||
await time.increase(STAKE_DURATION / 2);
|
||||
expect(await work1.stakeCount()).to.equal(1);
|
||||
await expect(dao.stakeAvailability(work1.target, 50, STAKE_DURATION)).to.emit(work1, 'AvailabilityStaked').withArgs(0);
|
||||
expect(await work1.stakeCount()).to.equal(1);
|
||||
expect(await dao.allowance(account1, work1.target)).to.equal(50);
|
||||
await expect(work1.extendAvailability(0, STAKE_DURATION)).to.emit(work1, '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);
|
||||
expect(await work1.stakeCount()).to.equal(1);
|
||||
await dao.stakeAvailability(work1.target, 50, STAKE_DURATION);
|
||||
expect(await work1.stakeCount()).to.equal(1);
|
||||
expect(await dao.allowance(account1, work1.target)).to.equal(50);
|
||||
await work1.extendAvailability(0, STAKE_DURATION);
|
||||
});
|
||||
|
||||
it('extending a stake before expiration should reset the end time to the new duration from the present', async () => {
|
||||
it('should not be able to extend the duration of another worker\'s availability stake', async () => {
|
||||
await time.increase(STAKE_DURATION * 2);
|
||||
await expect(work1.connect(account2).extendAvailability(0, STAKE_DURATION)).to.be.revertedWith('Worker can only extend their own availability stake');
|
||||
});
|
||||
|
||||
it('extending a stake before expiration should increase the end time by the given duration', async () => {
|
||||
await time.increase(STAKE_DURATION / 2);
|
||||
expect(await work1.stakeCount()).to.equal(1);
|
||||
await dao.stakeAvailability(work1.target, 50, STAKE_DURATION * 2);
|
||||
expect(await work1.stakeCount()).to.equal(1);
|
||||
const expectedEndTime = await time.latest() + 2 * STAKE_DURATION;
|
||||
await work1.extendAvailability(0, STAKE_DURATION * 2);
|
||||
const expectedEndTime = await time.latest() + 2.5 * STAKE_DURATION;
|
||||
const stake = await work1.stakes(0);
|
||||
expect(stake.endTime).to.be.within(expectedEndTime - 1, expectedEndTime);
|
||||
expect(await dao.allowance(account1, work1.target)).to.equal(50);
|
||||
});
|
||||
|
||||
it('extending a stake after expiration should restart the stake for the given duration', async () => {
|
||||
await time.increase(STAKE_DURATION * 2);
|
||||
expect(await work1.stakeCount()).to.equal(1);
|
||||
await dao.stakeAvailability(work1.target, 50, STAKE_DURATION * 2);
|
||||
expect(await work1.stakeCount()).to.equal(1);
|
||||
await work1.extendAvailability(0, STAKE_DURATION * 2);
|
||||
const expectedEndTime = await time.latest() + STAKE_DURATION * 2;
|
||||
const stake = await work1.stakes(0);
|
||||
expect(stake.endTime).to.be.within(expectedEndTime - 1, expectedEndTime);
|
||||
expect(await dao.allowance(account1, work1.target)).to.equal(50);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -130,7 +121,7 @@ describe('Work1', () => {
|
|||
expect(await work1.requestCount()).to.equal(1);
|
||||
const request = await work1.requests(0);
|
||||
expect(request.customer).to.equal(account2);
|
||||
expect(request.requestPostId).to.equal('req-content-id');
|
||||
expect(request.requestContentId).to.equal('req-content-id');
|
||||
});
|
||||
|
||||
it('should not be able to request work if there are no availability stakes', async () => {
|
||||
|
@ -169,30 +160,26 @@ describe('Work1', () => {
|
|||
await expect(requestWork()).to.be.revertedWith('No available worker stakes');
|
||||
});
|
||||
|
||||
it('after a stake has been assigned work, staking again should create a new stake', async () => {
|
||||
it('should not be able to extend a stake that has been assigned work', async () => {
|
||||
const {
|
||||
dao, work1, account1, account2,
|
||||
dao, work1, account2,
|
||||
} = await loadFixture(deploy);
|
||||
await dao.stakeAvailability(work1.target, 50, STAKE_DURATION);
|
||||
await work1.connect(account2).requestWork('req-content-id', { value: WORK1_PRICE });
|
||||
await time.increase(STAKE_DURATION * 2);
|
||||
expect(await work1.stakeCount()).to.equal(1);
|
||||
await dao.stakeAvailability(work1.target, 50, STAKE_DURATION);
|
||||
expect(await work1.stakeCount()).to.equal(2);
|
||||
expect(await dao.allowance(account1, work1.target)).to.equal(100);
|
||||
await expect(work1.extendAvailability(0, STAKE_DURATION)).to.be.revertedWith('Stake has already been assigned work');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Work evidence and approval/disapproval', () => {
|
||||
let dao;
|
||||
let forum;
|
||||
let work1;
|
||||
let account1;
|
||||
let account2;
|
||||
|
||||
beforeEach(async () => {
|
||||
({
|
||||
dao, forum, work1, account1, account2,
|
||||
dao, work1, account1, account2,
|
||||
} = await loadFixture(deploy));
|
||||
await dao.stakeAvailability(work1.target, 50, STAKE_DURATION);
|
||||
});
|
||||
|
@ -224,15 +211,17 @@ describe('Work1', () => {
|
|||
.to.emit(work1, 'WorkApprovalSubmitted').withArgs(0, true);
|
||||
expect(await dao.balanceOf(work1.target)).to.equal(0);
|
||||
expect(await dao.balanceOf(account1)).to.equal(100);
|
||||
const post = await forum.getPost('evidence-content-id');
|
||||
const post = await dao.posts('evidence-content-id');
|
||||
expect(post.sender).to.equal(work1.target);
|
||||
expect(post.authors).to.have.length(1);
|
||||
expect(post.authors[0].weightPPM).to.equal(1000000);
|
||||
expect(post.authors[0].authorAddress).to.equal(account1);
|
||||
const pool = await dao.getValidationPool(1);
|
||||
expect(pool.props.fee).to.equal(WORK1_PRICE);
|
||||
expect(post.id).to.equal('evidence-content-id');
|
||||
const postAuthors = await dao.getPostAuthors('evidence-content-id');
|
||||
expect(postAuthors).to.have.length(1);
|
||||
expect(postAuthors[0].weightPPM).to.equal(1000000);
|
||||
expect(postAuthors[0].authorAddress).to.equal(account1);
|
||||
const pool = await dao.validationPools(1);
|
||||
expect(pool.fee).to.equal(WORK1_PRICE);
|
||||
expect(pool.sender).to.equal(work1.target);
|
||||
expect(pool.props.postId).to.equal('evidence-content-id');
|
||||
expect(pool.postId).to.equal('evidence-content-id');
|
||||
expect(pool.stakeCount).to.equal(1);
|
||||
await time.increase(86401);
|
||||
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true, true);
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
const { ethers } = require('hardhat');
|
||||
|
||||
const deployDAO = async () => {
|
||||
const Reputation = await ethers.getContractFactory('Reputation');
|
||||
const Bench = await ethers.getContractFactory('Bench');
|
||||
const LightweightBench = await ethers.getContractFactory('LightweightBench');
|
||||
const DAO = await ethers.getContractFactory('DAO');
|
||||
const GlobalForum = await ethers.getContractFactory('GlobalForum');
|
||||
const forum = await GlobalForum.deploy();
|
||||
const reputation = await Reputation.deploy();
|
||||
const bench = await Bench.deploy();
|
||||
const lightweightBench = await LightweightBench.deploy();
|
||||
const dao = await DAO.deploy(
|
||||
reputation.target,
|
||||
bench.target,
|
||||
lightweightBench.target,
|
||||
forum.target,
|
||||
);
|
||||
return {
|
||||
forum,
|
||||
dao,
|
||||
reputation,
|
||||
bench,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = deployDAO;
|
|
@ -1,24 +1,14 @@
|
|||
{
|
||||
"localhost": {
|
||||
"DAO": "0x3734B0944ea37694E85AEF60D5b256d19EDA04be",
|
||||
"Work1": "0x8BDA04936887cF11263B87185E4D19e8158c6296",
|
||||
"Onboarding": "0x8688E736D0D72161db4D25f68EF7d0EE4856ba19",
|
||||
"Proposals": "0x3287061aDCeE36C1aae420a06E4a5EaE865Fe3ce",
|
||||
"Rollup": "0x71cb20D63576a0Fa4F620a2E96C73F82848B09e1",
|
||||
"Work2": "0x76Dfe9F47f06112a1b78960bf37d87CfbB6D6133",
|
||||
"Reputation": "0xEAefe601Aad7422307B99be65bbE005aeA966012",
|
||||
"Forum": "0x79e365342329560e8420d7a0f016633d7640cB18",
|
||||
"Bench": "0xC0f00E5915F9abE6476858fD1961EAf79395ea64"
|
||||
"DAO": "0x8d914D38dD301FC4606f5aa9fEcF8A76389020d3",
|
||||
"Work1": "0xfe58B9EB03F75A603de1B286584f5E9532ab8fB5",
|
||||
"Onboarding": "0x1d63FDe5B461106729fE1e5e38A02fc68C518Af5",
|
||||
"Proposals": "0x050C420Cc4995B41217Eba1B54B82Fd5687e9139"
|
||||
},
|
||||
"sepolia": {
|
||||
"DAO": "0xBA2e65ae29667E145343bD5Fd655A72dcf873b08",
|
||||
"Work1": "0x251dB891768ea85DaCA6bb567669F97248D09Fe3",
|
||||
"Onboarding": "0x78FC8b520001560A9D7a61072855218320C71BDC",
|
||||
"Proposals": "0xA888cDC4Bd80d402b14B1FeDE5FF471F1737570c",
|
||||
"Reputation": "0x62cc0035B17F1686cE30320B90373c77fcaA58CD",
|
||||
"Forum": "0x51b5Af12707e0d879B985Cb0216bFAC6dca85501",
|
||||
"Bench": "0x98d9F0e97Af71936747819040ddBE896A548ef4d",
|
||||
"Rollup": "0x678DC2c846bfDCC813ea27DfEE428f1d7f2521ED",
|
||||
"Work2": "0x609102Fb6cA15da80D37E8cA68aBD5e1bD9C855B"
|
||||
"DAO": "0x8e5bd58B2ca8910C5F9be8de847d6883B15c60d2",
|
||||
"Work1": "0x1708A144F284C1a9615C25b674E4a08992CE93e4",
|
||||
"Onboarding": "0xb21D4c986715A1adb5e87F752842613648C20a7B",
|
||||
"Proposals": "0x930c47293F206780E8F166338bDaFF3520306032"
|
||||
}
|
||||
}
|
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
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
|
@ -1,27 +1,263 @@
|
|||
import { useCallback } from 'react';
|
||||
import {
|
||||
useCallback, useEffect, useState, useMemo, 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';
|
||||
import Stack from 'react-bootstrap/Stack';
|
||||
|
||||
import MainTabs from './components/tabs';
|
||||
import useMainContext from './contexts/useMainContext';
|
||||
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';
|
||||
|
||||
function WebApp() {
|
||||
const {
|
||||
sdk, connected, chainId, account, reputation, totalReputation, balanceEther,
|
||||
} = useMainContext();
|
||||
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 ----------------------------------- */
|
||||
/* -------------------------------------------------------------------------------- */
|
||||
|
||||
const connect = useCallback(async () => {
|
||||
try {
|
||||
await sdk?.connect();
|
||||
console.log('connected');
|
||||
console.log('connected', connected);
|
||||
} catch (err) {
|
||||
console.warn('failed to connect..', err);
|
||||
}
|
||||
}, [sdk, connected]);
|
||||
}, [sdk]);
|
||||
|
||||
const disconnect = useCallback(async () => {
|
||||
try {
|
||||
|
@ -31,8 +267,67 @@ 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 && (
|
||||
|
@ -77,11 +372,211 @@ function WebApp() {
|
|||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
<MainTabs />
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</Web3Context.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import {
|
||||
useCallback, useEffect, useRef,
|
||||
useCallback, useEffect, useState, useMemo, 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';
|
||||
|
@ -10,15 +14,234 @@ import {
|
|||
WidgetApi, EventDirection, WidgetEventCapability,
|
||||
} from 'matrix-widget-api';
|
||||
|
||||
import MainTabs from './components/tabs';
|
||||
import useMainContext from './contexts/useMainContext';
|
||||
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';
|
||||
|
||||
function Widget() {
|
||||
const {
|
||||
sdk, connected, provider, chainId, account, balanceEther, reputation, totalReputation,
|
||||
} = useMainContext();
|
||||
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 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);
|
||||
|
@ -63,6 +286,56 @@ 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();
|
||||
|
@ -83,7 +356,12 @@ 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 && (
|
||||
|
@ -131,11 +409,210 @@ function Widget() {
|
|||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
<MainTabs />
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</Web3Context.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ const getProposalStatus = (proposal) => {
|
|||
|
||||
function Proposals() {
|
||||
const {
|
||||
provider, chainId, account, reputation, DAORef,
|
||||
provider, chainId, account, reputation,
|
||||
} = useContext(Web3Context);
|
||||
const [proposals, dispatchProposal] = useList();
|
||||
const proposalsContract = useRef();
|
||||
|
@ -105,18 +105,9 @@ function Proposals() {
|
|||
const onSubmitProposal = useCallback(async (post) => {
|
||||
const web3 = new Web3(provider);
|
||||
const emptyCallbackData = web3.eth.abi.encodeParameter('bytes', '0x00');
|
||||
// First add a post for this proposal
|
||||
await DAORef.current.methods.addPost(
|
||||
[{ 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,
|
||||
account,
|
||||
durations,
|
||||
false,
|
||||
emptyCallbackData,
|
||||
|
@ -125,7 +116,7 @@ function Proposals() {
|
|||
gas: 1000000,
|
||||
value: 10000,
|
||||
});
|
||||
}, [provider, account, proposalsContract, durations, DAORef]);
|
||||
}, [provider, account, proposalsContract, durations]);
|
||||
|
||||
const handleAttest = useCallback(async (proposalIndex) => {
|
||||
await proposalsContract.current.methods.attest(proposalIndex, reputation).send({
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue