Compare commits
103 Commits
forum-netw
...
main
Author | SHA1 | Date |
---|---|---|
Ladd Hoffman | 24c183912a | |
Ladd Hoffman | ad382b5caf | |
Ladd Hoffman | c80f2ee79b | |
Ladd Hoffman | 846eb73cea | |
Ladd Hoffman | 1f3d8a7d1e | |
Ladd Hoffman | 6679b9dedb | |
Ladd Hoffman | 7a8bb0a95e | |
Ladd Hoffman | 4d53f5c70e | |
Ladd Hoffman | 907b99bb65 | |
Ladd Hoffman | 5230f8664b | |
Ladd Hoffman | 9eff884636 | |
Ladd Hoffman | 8d0daf2062 | |
Ladd Hoffman | 72c3bd1663 | |
Ladd Hoffman | ea6e2d4494 | |
Ladd Hoffman | a743a81218 | |
Ladd Hoffman | 07370be4fa | |
Ladd Hoffman | 56132b2fec | |
Ladd Hoffman | f978c20104 | |
Ladd Hoffman | ae5ab09e16 | |
Ladd Hoffman | dd582c4d20 | |
Ladd Hoffman | 77d6698899 | |
Ladd Hoffman | 498b5c106f | |
Ladd Hoffman | 9eb3451451 | |
Ladd Hoffman | 82e026f327 | |
Ladd Hoffman | 1f1f1f0c1d | |
Ladd Hoffman | 8bb188ff13 | |
Ladd Hoffman | 9974712aa9 | |
Ladd Hoffman | a8544dfd39 | |
Ladd Hoffman | 36acc56fa2 | |
Ladd Hoffman | 7e74773242 | |
Ladd Hoffman | e602466800 | |
Ladd Hoffman | 7eddd66385 | |
Ladd Hoffman | 21a0ef6bda | |
Ladd Hoffman | ce4f78aa97 | |
Ladd Hoffman | 81823cd009 | |
Ladd Hoffman | b02efb66ad | |
Ladd Hoffman | 36c827a40f | |
Ladd Hoffman | 92bbab2a5b | |
Ladd Hoffman | d4bdb1c435 | |
Ladd Hoffman | f3037a766d | |
Ladd Hoffman | 5ca884686b | |
Ladd Hoffman | 2ed07b7f5e | |
Ladd Hoffman | 353190fdcd | |
Ladd Hoffman | 0a8b170115 | |
Ladd Hoffman | 5988950857 | |
Ladd Hoffman | 63b43a0f4d | |
Ladd Hoffman | e6a0c22d3f | |
Ladd Hoffman | dddee70365 | |
Ladd Hoffman | b943daf28c | |
Ladd Hoffman | fa7f0620b6 | |
Ladd Hoffman | c4a528283c | |
Ladd Hoffman | 3072dbae28 | |
Ladd Hoffman | aba7cc6870 | |
Ladd Hoffman | 77ae33ce5a | |
Ladd Hoffman | 602ce75871 | |
Ladd Hoffman | a2a1035da8 | |
Ladd Hoffman | 9e5b6a4064 | |
Ladd Hoffman | 6012cb2d1a | |
Ladd Hoffman | 7d89df7a61 | |
Ladd Hoffman | 87f04bd7d3 | |
Ladd Hoffman | 90eded96e5 | |
Ladd Hoffman | 1181aca9d7 | |
Ladd Hoffman | 92cfdaaa63 | |
Ladd Hoffman | fc27cda81d | |
Ladd Hoffman | 4eced65dbf | |
Ladd Hoffman | 50df9efabc | |
Ladd Hoffman | 30ebe04db7 | |
Ladd Hoffman | d136e6ff45 | |
Ladd Hoffman | e63d4b9b21 | |
Ladd Hoffman | f57c38e322 | |
Ladd Hoffman | be71d4f3cd | |
Ladd Hoffman | 3541802d93 | |
Ladd Hoffman | c8ce74cf13 | |
Ladd Hoffman | a48d14905c | |
Ladd Hoffman | 5fc5bbe0b5 | |
Ladd Hoffman | ab63ad1868 | |
Ladd Hoffman | c24952497d | |
Ladd Hoffman | e7ff4254a3 | |
Ladd Hoffman | 3cf24c6fa1 | |
Ladd Hoffman | 721718ac13 | |
Ladd Hoffman | 329af5c640 | |
Ladd Hoffman | 7cda474d20 | |
Ladd Hoffman | 0c98ae2505 | |
Ladd Hoffman | 23070ae381 | |
Ladd Hoffman | e26afa1eb4 | |
Ladd Hoffman | f475742cbf | |
Ladd Hoffman | e20a864738 | |
Ladd Hoffman | 1668ceeda3 | |
Ladd Hoffman | b7280d9946 | |
Ladd Hoffman | 5834f89882 | |
Ladd Hoffman | d1570e7672 | |
Ladd Hoffman | 667a051c13 | |
Ladd Hoffman | 94200b59d4 | |
Ladd Hoffman | d04b280645 | |
Ladd Hoffman | 4c9cee9963 | |
Ladd Hoffman | acda73fff4 | |
Ladd Hoffman | db8a8ca346 | |
Ladd Hoffman | 4789ed7499 | |
Ladd Hoffman | 0671ad7b64 | |
Ladd Hoffman | a0d5611ca4 | |
Ladd Hoffman | 68d04117c9 | |
Ladd Hoffman | ff7d6134f1 | |
Ladd Hoffman | 43462e84ea |
|
@ -1,7 +0,0 @@
|
||||||
pages:
|
|
||||||
script:
|
|
||||||
- mkdir public
|
|
||||||
- cp -r forum-network/src/* public/
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- public
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Science Publishing DAO
|
# DAO Governance Framework
|
||||||
|
|
||||||
## Subprojects
|
## Subprojects
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [forum-network](./forum-network) | Javascript prototyping forum architecture |
|
| [semantic-scholar-client](./semantic-scholar-client) | Rust utility for reading data from the [Semantic Scholar API](https://api.semanticscholar.org/api-docs) |
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
es2021: true,
|
|
||||||
},
|
|
||||||
extends: ['airbnb-base'],
|
|
||||||
overrides: [],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest',
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
'import',
|
|
||||||
'html',
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
'import/extensions': ['error', 'always'],
|
|
||||||
'import/prefer-default-export': ['off'],
|
|
||||||
'import/no-unresolved': ['error', { ignore: ['^http'] }],
|
|
||||||
'import/no-absolute-path': ['off'],
|
|
||||||
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
||||||
'max-classes-per-file': ['off'],
|
|
||||||
'no-param-reassign': ['off'],
|
|
||||||
'no-plusplus': ['off'],
|
|
||||||
'no-restricted-syntax': ['off'],
|
|
||||||
'max-len': ['warn', 120],
|
|
||||||
'no-console': ['off'],
|
|
||||||
'no-return-assign': ['off'],
|
|
||||||
'no-multi-assign': ['off'],
|
|
||||||
'no-constant-condition': ['off'],
|
|
||||||
'no-await-in-loop': ['off'],
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,3 +0,0 @@
|
||||||
ssl/
|
|
||||||
node_modules/
|
|
||||||
git/
|
|
|
@ -1,30 +0,0 @@
|
||||||
We've considered implementing this validation pool + forum structure as smart contracts.
|
|
||||||
However, we expect that such contracts would be expensive to run, because the recursive algorithm for distributing reputation via the forum will incur a lot of computation, consuming a lot of gas.
|
|
||||||
|
|
||||||
Can we bake this reputation algorithm into the core protocol of our blockchain?
|
|
||||||
|
|
||||||
The structure seems to be similar to proof-of-stake. A big difference is that what is staked and awarded is reputation rather than currency.
|
|
||||||
The idea with reputation is that it entitles you to a proportional share of revenue earned by the network.
|
|
||||||
So what does that look like in this context?
|
|
||||||
|
|
||||||
Let's say we are extending Ethereum. ETH would continue to be the currency that users must spend in order to execute transactions.
|
|
||||||
So when a user wants to execute a transaction, they must pay a fee.
|
|
||||||
A portion of this fee could then be distributed to reputation holders.
|
|
||||||
|
|
||||||
- https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/
|
|
||||||
- https://ethereum.org/en/developers/docs/nodes-and-clients/
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
execution client
|
|
||||||
execution gossip network
|
|
||||||
|
|
||||||
consensus client
|
|
||||||
consensus gossip network
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
cardano -- "dynamic availability"?
|
|
||||||
staking pools -- does it make sense with reputation?
|
|
||||||
what about for governance voting --
|
|
||||||
do we want a representative republic or a democracy?
|
|
|
@ -1,99 +0,0 @@
|
||||||
# Challenges
|
|
||||||
|
|
||||||
- Receiving payments
|
|
||||||
- Distributing payments to participants
|
|
||||||
- Computing updates to forum graph
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Receiving payments
|
|
||||||
|
|
||||||
Business SC will need to implement a financial model.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Excerpts from DeSciPubDAOArchit22July19PrintCut.pdf
|
|
||||||
|
|
||||||
> With today’s prices, however, we will begin by programming this all off-chain and simplify the reputation tokens to be less dynamic in their evaluation. Next iteration improves the decentralization commensurate with practical realities.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Validation pool termination
|
|
||||||
|
|
||||||
How do we want to handle this?
|
|
||||||
The validation pool specifies a duration.
|
|
||||||
We don't want to compute results until the end of this duration.
|
|
||||||
We're currently supporting anonymous voting.
|
|
||||||
With anonymous voting, we need to wait until the end of the vote duration,
|
|
||||||
and then have a separate interval in which voters reveal their identities.
|
|
||||||
For now, we can let anonymous voters reveal their identities at any time
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Bench.totalReputation is a very important quantity, isn't it? Basically determines inflation for reputation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Should availability registration encumber reputation?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
- Is a particular availability stake amount required?
|
|
||||||
|
|
||||||
Currently we support updating the staked amount.
|
|
||||||
Seems like a soft protocol thing.
|
|
||||||
A given DAO can have a formula for deciding appropriate amounts.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
The following was a code comment on `Business.submitRequest(fee, ...)`:
|
|
||||||
|
|
||||||
> Fee should be held in escrow.
|
|
||||||
> That means there should be specific conditions under which the fee will be refunded.
|
|
||||||
> That means the submission should include some time value to indicate when it expires.
|
|
||||||
> There could be separate thresholds to indicate the earliest that the job may be cancelled,
|
|
||||||
> and the time at which the job will be automatically cancelled.
|
|
||||||
|
|
||||||
# Implementing forum
|
|
||||||
|
|
||||||
Does the following make sense?
|
|
||||||
We will link the forum to the bench
|
|
||||||
An author of a forum post /_ ? is always? can be? _/ a reputation holder.
|
|
||||||
This is what we call a expert. Let's update that terminology to be `reputationHolder`.
|
|
||||||
That's too long, though. Let's rename it to `expert`.
|
|
||||||
So we want to aim for the situation where the author of a forum post is an expert.
|
|
||||||
For now let's try thinking of them as experts no matter what;
|
|
||||||
The strength of their expertise is meant to be represented by reputation tokens.
|
|
||||||
So each reputation token must be a contract.
|
|
||||||
Minting a reputation token means to construct an instance of such a contract.
|
|
||||||
The reputation contract then has its own lifecycle.
|
|
||||||
We can support dynamic reevaluation if the reputation contract
|
|
||||||
|
|
||||||
- has an interface that allows (securely) updating
|
|
||||||
- Define secure :: passes validation pool
|
|
||||||
- How shall it know the operation is occurring as part of an "official" validation pool?
|
|
||||||
It can verify a signature...
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Tokens staked for and against a post.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Token loss ratio
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
parameter q_4 -- what is c_n?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
what is reputation?
|
|
||||||
valuable evidence that you're going to do what you say you'll do in the future
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
for now, combine c2 and c3
|
|
||||||
|
|
||||||
validation pool should compute rewards for author,
|
|
||||||
then send that to the forum to distribute.
|
|
|
@ -1,50 +0,0 @@
|
||||||
Reputation Tokens
|
|
||||||
|
|
||||||
Minting
|
|
||||||
|
|
||||||
Suppose it's possible to mint a reputation token.
|
|
||||||
Say we have a contract that keeps track of all the reputation tokens.
|
|
||||||
Suppose the reputation contract implements ERC720 (NFT).
|
|
||||||
Assume a validation pool always corresponds to a post.
|
|
||||||
A single token could be minted for each validation pool.
|
|
||||||
That token could be subdivided so that each winning voter gets some.
|
|
||||||
Perhaps voters get tokens that are specifically identifiable as governance reputation tokens.
|
|
||||||
Then the main token can be awarded to the post author.
|
|
||||||
Each token should carry a specific value.
|
|
||||||
The forum will update the value of the token for the post author as well as posts affected by its chain of references.
|
|
||||||
|
|
||||||
Then, when participants wish to stake reputation (for voting or for availability),
|
|
||||||
they must specify the amount and the token address which carries that reputation.
|
|
||||||
The token should probably then lock that reputation, preventing it from being staked concurrently for another purpose.
|
|
||||||
|
|
||||||
Perhaps our interface can allow staking reputation from multiple tokens at the same time.
|
|
||||||
And/or we can provide a mechanism for consolidating tokens.
|
|
||||||
|
|
||||||
Or maybe, if reputation is staked via a given token, then the reputation awards should go to that same token.
|
|
||||||
In that case, when should new tokens be minted?
|
|
||||||
|
|
||||||
Maybe a token should be minted for each validation pool, but not subdivided.
|
|
||||||
Voter rewards can just add value to the existing tokens from which reputation was staked.
|
|
||||||
|
|
||||||
Maybe a new token should only be minted if the author did not provide a token from which to stake reputation on their own post.
|
|
||||||
This supports the case of a new author earning their first reputation.
|
|
||||||
In that case the author may need to pay a fee to buy in to the DAO.
|
|
||||||
Or perhaps they can be sponsored by one or more existing reputation token holders.
|
|
||||||
Existing reputation holders could grant some reputation to a new member.
|
|
||||||
Perhaps there could be a contract that allows sponsoring a new member, such that whatever fee is given,
|
|
||||||
that fee will automatically be repaid from the new member's earnings, before the new member starts receiving their share of earnings.
|
|
||||||
This could be a multi-party action, or could just be a generic operation that can be performed multiple times.
|
|
||||||
|
|
||||||
However, this effectively allows buying reputation, which goes against the core concept of reputation as evidence of performance.
|
|
||||||
|
|
||||||
It could make more sense for new members to submit some sort of petition, i.e. to make a post.
|
|
||||||
|
|
||||||
Maybe rather than submitting fees, existing members can grant some of their own reputation to a new member, and receive some sort of compensation if the new member does well.
|
|
||||||
|
|
||||||
So far the only workable model seems to be the idea that a new member must submit a post along with a fee, in order to be considered, and if the post is approved, they gain their first reputation.
|
|
||||||
The amount of this fee can be up to the applicant, and/or can be regulated by soft protocols within the DAO.
|
|
||||||
|
|
||||||
If this is the only scenario in which new rep tokens are minted, and from then on their value is updated as a result of each validation pool,
|
|
||||||
then we probably want each token to store information about the history of its value.
|
|
||||||
At a minimum this can be a list where each item includes the identifier of the corresponding validation pool, and the resulting increment/decrement of token value.
|
|
||||||
Each validation pool can then keep a record of the reputation staked by each voter, and the identifier of the corresponding post.
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"name": "forum-network",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"devDependencies": {
|
|
||||||
"eslint": "^8.27.0",
|
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
|
||||||
"eslint-plugin-html": "^7.1.0",
|
|
||||||
"eslint-plugin-import": "^2.26.0",
|
|
||||||
"prettier": "^2.7.1",
|
|
||||||
"prettier-eslint": "^15.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
export class Action {
|
|
||||||
constructor(name, scene) {
|
|
||||||
this.name = name;
|
|
||||||
this.scene = scene;
|
|
||||||
}
|
|
||||||
|
|
||||||
async log(src, dest, msg, obj, symbol = '->>') {
|
|
||||||
const logObj = false;
|
|
||||||
await this.scene.sequence.log(
|
|
||||||
`${src.name} ${symbol} ${dest.name} : ${this.name} ${msg ?? ''} ${
|
|
||||||
logObj && obj ? JSON.stringify(obj) : ''
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
export class Actor {
|
|
||||||
constructor(name, scene) {
|
|
||||||
this.name = name;
|
|
||||||
this.scene = scene;
|
|
||||||
this.callbacks = new Map();
|
|
||||||
this.status = this.scene.addDisplayValue(`${this.name} status`);
|
|
||||||
this.status.set('Created');
|
|
||||||
this.values = new Map();
|
|
||||||
this.active = 0;
|
|
||||||
this.scene.registerActor(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
activate() {
|
|
||||||
this.active += 1;
|
|
||||||
this.scene.sequence.log(`activate ${this.name}`, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deactivate() {
|
|
||||||
if (!this.active) {
|
|
||||||
throw new Error(`${this.name} is not active, can not deactivate`);
|
|
||||||
}
|
|
||||||
this.active -= 1;
|
|
||||||
await this.scene.sequence.log(`deactivate ${this.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async send(dest, action, detail) {
|
|
||||||
await action.log(this, dest, detail ? JSON.stringify(detail) : '');
|
|
||||||
await dest.recv(this, action, detail);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
async recv(src, action, detail) {
|
|
||||||
const cb = this.callbacks.get(action.name);
|
|
||||||
if (!cb) {
|
|
||||||
throw new Error(
|
|
||||||
`[${this.scene.name} actor ${this.name} does not have a callback registered for ${action.name}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await cb(src, detail);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
on(action, cb) {
|
|
||||||
this.callbacks.set(action.name, cb);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatus(status) {
|
|
||||||
this.status.set(status);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
addValue(label) {
|
|
||||||
this.values.set(label, this.scene.addDisplayValue(`${this.name} ${label}`));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setValue(label, value) {
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
value = value.toFixed(2);
|
|
||||||
}
|
|
||||||
let displayValue = this.values.get(label);
|
|
||||||
if (!displayValue) {
|
|
||||||
displayValue = this.scene.addDisplayValue(`${this.name} ${label}`);
|
|
||||||
this.values.set(label, displayValue);
|
|
||||||
}
|
|
||||||
if (value !== displayValue.get()) {
|
|
||||||
await this.scene.sequence.log(`note over ${this.name} : ${label} = ${value}`);
|
|
||||||
}
|
|
||||||
displayValue.set(value);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
import { Action } from './action.js';
|
|
||||||
import { Actor } from './actor.js';
|
|
||||||
import { CryptoUtil } from './crypto.js';
|
|
||||||
|
|
||||||
class Worker {
|
|
||||||
constructor(reputationPublicKey, tokenId, stakeAmount, duration) {
|
|
||||||
this.reputationPublicKey = reputationPublicKey;
|
|
||||||
this.tokenId = tokenId;
|
|
||||||
this.stakeAmount = stakeAmount;
|
|
||||||
this.duration = duration;
|
|
||||||
this.available = true;
|
|
||||||
this.assignedRequestId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Purpose: Enable staking reputation to enter the pool of workers
|
|
||||||
*/
|
|
||||||
export class Availability extends Actor {
|
|
||||||
constructor(bench, name, scene) {
|
|
||||||
super(name, scene);
|
|
||||||
this.bench = bench;
|
|
||||||
|
|
||||||
this.actions = {
|
|
||||||
assignWork: new Action('assign work', scene),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.workers = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
register(reputationPublicKey, { stakeAmount, tokenId, duration }) {
|
|
||||||
// TODO: Should be signed by token owner
|
|
||||||
this.bench.reputation.lock(tokenId, stakeAmount, duration);
|
|
||||||
const workerId = CryptoUtil.randomUUID();
|
|
||||||
this.workers.set(workerId, new Worker(reputationPublicKey, tokenId, stakeAmount, duration));
|
|
||||||
return workerId;
|
|
||||||
}
|
|
||||||
|
|
||||||
get availableWorkers() {
|
|
||||||
return Array.from(this.workers.values()).filter(({ available }) => !!available);
|
|
||||||
}
|
|
||||||
|
|
||||||
async assignWork(requestId) {
|
|
||||||
const totalAmountStaked = this.availableWorkers
|
|
||||||
.reduce((total, { stakeAmount }) => total += stakeAmount, 0);
|
|
||||||
// Imagine all these amounts layed end-to-end along a number line.
|
|
||||||
// To weight choice by amount staked, pick a stake by choosing a number at random
|
|
||||||
// from within that line segment.
|
|
||||||
const randomChoice = Math.random() * totalAmountStaked;
|
|
||||||
let index = 0;
|
|
||||||
let acc = 0;
|
|
||||||
for (const { stakeAmount } of this.workers.values()) {
|
|
||||||
acc += stakeAmount;
|
|
||||||
if (acc >= randomChoice) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
const worker = this.availableWorkers[index];
|
|
||||||
worker.available = false;
|
|
||||||
worker.assignedRequestId = requestId;
|
|
||||||
|
|
||||||
// TODO: Notify assignee
|
|
||||||
return worker;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAssignedWork(workerId) {
|
|
||||||
const worker = this.workers.get(workerId);
|
|
||||||
return worker.assignedRequestId;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
import { Actor } from './actor.js';
|
|
||||||
import { ValidationPool } from './validation-pool.js';
|
|
||||||
import params from '../params.js';
|
|
||||||
import { Action } from './action.js';
|
|
||||||
import { ReputationTokenContract } from './reputation-token.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Purpose: Keep track of reputation holders
|
|
||||||
*/
|
|
||||||
export class Bench extends Actor {
|
|
||||||
constructor(forum, name, scene) {
|
|
||||||
super(name, scene);
|
|
||||||
this.forum = forum;
|
|
||||||
this.validationPools = new Map();
|
|
||||||
this.voters = new Map();
|
|
||||||
this.reputation = new ReputationTokenContract();
|
|
||||||
|
|
||||||
this.actions = {
|
|
||||||
createValidationPool: new Action('create validation pool', scene),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
listValidationPools() {
|
|
||||||
Array.from(this.validationPools.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
listActiveVoters() {
|
|
||||||
return Array.from(this.voters.values()).filter((voter) => {
|
|
||||||
const hasVoted = !!voter.dateLastVote;
|
|
||||||
const withinThreshold = !params.activeVoterThreshold
|
|
||||||
|| new Date() - voter.dateLastVote >= params.activeVoterThreshold;
|
|
||||||
return hasVoted && withinThreshold;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getActiveReputation() {
|
|
||||||
return this.listActiveVoters()
|
|
||||||
.map(({ reputationPublicKey }) => this.reputation.valueOwnedBy(reputationPublicKey))
|
|
||||||
.reduce((acc, cur) => (acc += cur), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
getActiveAvailableReputation() {
|
|
||||||
return this.listActiveVoters()
|
|
||||||
.map(({ reputationPublicKey }) => this.reputation.availableValueOwnedBy(reputationPublicKey))
|
|
||||||
.reduce((acc, cur) => (acc += cur), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
async initiateValidationPool(poolOptions, stakeOptions) {
|
|
||||||
const validationPoolNumber = this.validationPools.size + 1;
|
|
||||||
const name = `Pool${validationPoolNumber}`;
|
|
||||||
const pool = new ValidationPool(this, this.forum, poolOptions, name, this.scene);
|
|
||||||
this.validationPools.set(pool.id, pool);
|
|
||||||
await this.actions.createValidationPool.log(this, pool);
|
|
||||||
pool.activate();
|
|
||||||
|
|
||||||
if (stakeOptions) {
|
|
||||||
const { reputationPublicKey, tokenId, authorStakeAmount } = stakeOptions;
|
|
||||||
await pool.stake(reputationPublicKey, {
|
|
||||||
tokenId,
|
|
||||||
position: true,
|
|
||||||
amount: authorStakeAmount,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return pool;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
import { DisplayValue } from './display-value.js';
|
|
||||||
import { CryptoUtil } from './crypto.js';
|
|
||||||
|
|
||||||
export class Box {
|
|
||||||
constructor(name, parentEl, elementType = 'div') {
|
|
||||||
this.name = name;
|
|
||||||
this.el = document.createElement(elementType);
|
|
||||||
this.el.id = `box_${CryptoUtil.randomUUID().replaceAll('-', '').slice(0, 8)}`;
|
|
||||||
this.el.classList.add('box');
|
|
||||||
if (name) {
|
|
||||||
this.el.setAttribute('box-name', name);
|
|
||||||
}
|
|
||||||
if (parentEl) {
|
|
||||||
parentEl.appendChild(this.el);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flex() {
|
|
||||||
this.addClass('flex');
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
monospace() {
|
|
||||||
this.addClass('monospace');
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
hidden() {
|
|
||||||
this.addClass('hidden');
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
addClass(className) {
|
|
||||||
this.el.classList.add(className);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
addBox(name, elementType) {
|
|
||||||
const box = new Box(name, this.el, elementType);
|
|
||||||
return box;
|
|
||||||
}
|
|
||||||
|
|
||||||
addDisplayValue(value) {
|
|
||||||
const box = this.addBox(value.name).flex();
|
|
||||||
return new DisplayValue(value, box);
|
|
||||||
}
|
|
||||||
|
|
||||||
setInnerHTML(html) {
|
|
||||||
this.el.innerHTML = html;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
getInnerText() {
|
|
||||||
return this.el.innerText;
|
|
||||||
}
|
|
||||||
|
|
||||||
getId() {
|
|
||||||
return this.el.id;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
import { Action } from './action.js';
|
|
||||||
import { Actor } from './actor.js';
|
|
||||||
import { CryptoUtil } from './crypto.js';
|
|
||||||
import { PostContent } from './post-content.js';
|
|
||||||
|
|
||||||
class Request {
|
|
||||||
constructor(fee, content) {
|
|
||||||
this.id = CryptoUtil.randomUUID();
|
|
||||||
this.fee = fee;
|
|
||||||
this.content = content;
|
|
||||||
this.worker = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Purpose: Enable fee-driven work requests, to be completed by workers from the availability pool
|
|
||||||
*/
|
|
||||||
export class Business extends Actor {
|
|
||||||
constructor(bench, forum, availability, name, scene) {
|
|
||||||
super(name, scene);
|
|
||||||
this.bench = bench;
|
|
||||||
this.forum = forum;
|
|
||||||
this.availability = availability;
|
|
||||||
|
|
||||||
this.actions = {
|
|
||||||
assignWork: new Action('assign work', scene),
|
|
||||||
submitPost: new Action('submit post', scene),
|
|
||||||
initiateValidationPool: new Action('initiate validation pool', scene),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.requests = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
async submitRequest(fee, content) {
|
|
||||||
const request = new Request(fee, content);
|
|
||||||
this.requests.set(request.id, request);
|
|
||||||
await this.actions.assignWork.log(this, this.availability);
|
|
||||||
const worker = await this.availability.assignWork(request.id);
|
|
||||||
request.worker = worker;
|
|
||||||
return request.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getRequest(requestId) {
|
|
||||||
const request = this.requests.get(requestId);
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
async submitWork(reputationPublicKey, requestId, workEvidence, { tokenLossRatio, duration }) {
|
|
||||||
const request = this.requests.get(requestId);
|
|
||||||
if (!request) {
|
|
||||||
throw new Error(`Request not found! id: ${requestId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reputationPublicKey !== request.worker.reputationPublicKey) {
|
|
||||||
throw new Error('Work evidence must be submitted by the assigned worker!');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a post representing this submission.
|
|
||||||
const post = new PostContent({
|
|
||||||
requestId,
|
|
||||||
workEvidence,
|
|
||||||
});
|
|
||||||
const requestIndex = Array.from(this.requests.values())
|
|
||||||
.findIndex(({ id }) => id === request.id);
|
|
||||||
post.setTitle(`Work Evidence ${requestIndex + 1}`);
|
|
||||||
|
|
||||||
await this.actions.submitPost.log(this, this.forum);
|
|
||||||
const postId = await this.forum.addPost(reputationPublicKey, post);
|
|
||||||
|
|
||||||
// Initiate a validation pool for this work evidence.
|
|
||||||
await this.actions.initiateValidationPool.log(this, this.bench);
|
|
||||||
const pool = await this.bench.initiateValidationPool({
|
|
||||||
postId,
|
|
||||||
fee: request.fee,
|
|
||||||
duration,
|
|
||||||
tokenLossRatio,
|
|
||||||
}, {
|
|
||||||
reputationPublicKey,
|
|
||||||
authorStakeAmount: request.worker.stakeAmount,
|
|
||||||
tokenId: request.worker.tokenId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// When the validation pool concludes,
|
|
||||||
// reputation should be awarded and fees should be distributed.
|
|
||||||
return pool;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
export class CryptoUtil {
|
|
||||||
static algorithm = 'RSASSA-PKCS1-v1_5';
|
|
||||||
|
|
||||||
static hash = 'SHA-256';
|
|
||||||
|
|
||||||
static async generateAsymmetricKey() {
|
|
||||||
return window.crypto.subtle.generateKey(
|
|
||||||
{
|
|
||||||
name: CryptoUtil.algorithm,
|
|
||||||
hash: CryptoUtil.hash,
|
|
||||||
modulusLength: 2048,
|
|
||||||
publicExponent: new Uint8Array([1, 0, 1]),
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
['sign', 'verify'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async sign(content, privateKey) {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const encoded = encoder.encode(content);
|
|
||||||
const signature = await window.crypto.subtle.sign(CryptoUtil.algorithm, privateKey, encoded);
|
|
||||||
// Return base64-encoded signature
|
|
||||||
return btoa(String.fromCharCode(...new Uint8Array(signature)));
|
|
||||||
}
|
|
||||||
|
|
||||||
static async verify(content, b64publicKey, b64signature) {
|
|
||||||
// Convert base64 javascript web key to CryptoKey
|
|
||||||
const publicKey = await CryptoUtil.importKey(b64publicKey);
|
|
||||||
// Convert base64 signature to an ArrayBuffer
|
|
||||||
const signature = Uint8Array.from(atob(b64signature), (c) => c.charCodeAt(0));
|
|
||||||
// TODO: make a single TextEncoder instance and reuse it
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const encoded = encoder.encode(content);
|
|
||||||
return window.crypto.subtle.verify(CryptoUtil.algorithm, publicKey, signature, encoded);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async exportKey(publicKey) {
|
|
||||||
// Store public key as base64 javascript web key
|
|
||||||
const jwk = await window.crypto.subtle.exportKey('jwk', publicKey);
|
|
||||||
return btoa(JSON.stringify(jwk));
|
|
||||||
}
|
|
||||||
|
|
||||||
static async importKey(b64jwk) {
|
|
||||||
// Convert base64 javascript web key to CryptoKey
|
|
||||||
const jwk = JSON.parse(atob(b64jwk));
|
|
||||||
return window.crypto.subtle.importKey(
|
|
||||||
'jwk',
|
|
||||||
jwk,
|
|
||||||
{
|
|
||||||
name: CryptoUtil.algorithm,
|
|
||||||
hash: CryptoUtil.hash,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
['verify'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static randomUUID() {
|
|
||||||
return window.crypto.randomUUID();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
export class DisplayValue {
|
|
||||||
constructor(name, box) {
|
|
||||||
this.value = undefined;
|
|
||||||
this.name = name;
|
|
||||||
this.box = box;
|
|
||||||
this.nameBox = this.box.addBox(`${this.name}-name`).addClass('name');
|
|
||||||
this.valueBox = this.box.addBox(`${this.name}-value`).addClass('value');
|
|
||||||
this.nameBox.setInnerHTML(this.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.valueBox.setInnerHTML(this.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
set(value) {
|
|
||||||
this.value = value;
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
get() {
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
/**
|
|
||||||
* ERC-721 Non-Fungible Token Standard
|
|
||||||
* See https://eips.ethereum.org/EIPS/eip-721
|
|
||||||
* and https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol
|
|
||||||
*
|
|
||||||
* This implementation is currently incomplete. It lacks the following:
|
|
||||||
* - Token approvals
|
|
||||||
* - Operator approvals
|
|
||||||
* - Emitting events
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class ERC721 /* is ERC165 */ {
|
|
||||||
constructor(name, symbol) {
|
|
||||||
this.name = name;
|
|
||||||
this.symbol = symbol;
|
|
||||||
this.balances = new Map(); // owner address --> token count
|
|
||||||
this.owners = new Map(); // token id --> owner address
|
|
||||||
// this.tokenApprovals = new Map(); // token id --> approved addresses
|
|
||||||
// this.operatorApprovals = new Map(); // owner --> operator approvals
|
|
||||||
|
|
||||||
this.events = {
|
|
||||||
// Transfer: (_from, _to, _tokenId) => {},
|
|
||||||
// Approval: (_owner, _approved, _tokenId) => {},
|
|
||||||
// ApprovalForAll: (_owner, _operator, _approved) => {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
incrementBalance(owner, increment) {
|
|
||||||
const balance = this.balances.get(owner) ?? 0;
|
|
||||||
this.balances.set(owner, balance + increment);
|
|
||||||
}
|
|
||||||
|
|
||||||
mint(to, tokenId) {
|
|
||||||
if (this.owners.get(tokenId)) {
|
|
||||||
throw new Error('ERC721: token already minted');
|
|
||||||
}
|
|
||||||
this.incrementBalance(to, 1);
|
|
||||||
this.owners.set(tokenId, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
burn(tokenId) {
|
|
||||||
const owner = this.owners.get(tokenId);
|
|
||||||
this.incrementBalance(owner, -1);
|
|
||||||
this.owners.delete(tokenId);
|
|
||||||
}
|
|
||||||
|
|
||||||
balanceOf(owner) {
|
|
||||||
if (!owner) {
|
|
||||||
throw new Error('ERC721: address zero is not a valid owner');
|
|
||||||
}
|
|
||||||
return this.balances.get(owner) ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ownerOf(tokenId) {
|
|
||||||
const owner = this.owners.get(tokenId);
|
|
||||||
if (!owner) {
|
|
||||||
throw new Error(`ERC721: invalid token ID: ${tokenId}`);
|
|
||||||
}
|
|
||||||
return owner;
|
|
||||||
}
|
|
||||||
|
|
||||||
transferFrom(from, to, tokenId) {
|
|
||||||
const owner = this.owners.get(tokenId);
|
|
||||||
if (owner !== from) {
|
|
||||||
throw new Error('ERC721: transfer from incorrect owner');
|
|
||||||
}
|
|
||||||
this.incrementBalance(from, -1);
|
|
||||||
this.incrementBalance(to, 1);
|
|
||||||
this.owners.set(tokenId, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Enable or disable approval for a third party ("operator") to manage
|
|
||||||
/// all of `msg.sender`'s assets
|
|
||||||
/// @dev Emits the ApprovalForAll event. The contract MUST allow
|
|
||||||
/// multiple operators per owner.
|
|
||||||
/// @param _operator Address to add to the set of authorized operators
|
|
||||||
/// @param _approved True if the operator is approved, false to revoke approval
|
|
||||||
// setApprovalForAll(_operator, _approved) {}
|
|
||||||
|
|
||||||
/// @notice Get the approved address for a single NFT
|
|
||||||
/// @dev Throws if `_tokenId` is not a valid NFT.
|
|
||||||
/// @param _tokenId The NFT to find the approved address for
|
|
||||||
/// @return The approved address for this NFT, or the zero address if there is none
|
|
||||||
// getApproved(_tokenId) {}
|
|
||||||
|
|
||||||
/// @notice Query if an address is an authorized operator for another address
|
|
||||||
/// @param _owner The address that owns the NFTs
|
|
||||||
/// @param _operator The address that acts on behalf of the owner
|
|
||||||
/// @return True if `_operator` is an approved operator for `_owner`, false otherwise
|
|
||||||
// isApprovedForAll(_owner, _operator) {}
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
import { Action } from './action.js';
|
|
||||||
import { PostMessage } from './message.js';
|
|
||||||
import { CryptoUtil } from './crypto.js';
|
|
||||||
import { ReputationHolder } from './reputation-holder.js';
|
|
||||||
|
|
||||||
export class Expert extends ReputationHolder {
|
|
||||||
constructor(name, scene) {
|
|
||||||
super(undefined, name, scene);
|
|
||||||
this.actions = {
|
|
||||||
submitPostViaNetwork: new Action('submit post via network', scene),
|
|
||||||
submitPost: new Action('submit post', scene),
|
|
||||||
initiateValidationPool: new Action('initiate validation pool', scene),
|
|
||||||
stake: new Action('stake on post', scene),
|
|
||||||
registerAvailability: new Action('register availability', scene),
|
|
||||||
getAssignedWork: new Action('get assigned work', scene),
|
|
||||||
submitWork: new Action('submit work evidence', scene),
|
|
||||||
};
|
|
||||||
this.validationPools = new Map();
|
|
||||||
this.tokens = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async initialize() {
|
|
||||||
this.reputationKey = await CryptoUtil.generateAsymmetricKey();
|
|
||||||
// this.reputationPublicKey = await CryptoUtil.exportKey(this.reputationKey.publicKey);
|
|
||||||
this.reputationPublicKey = this.name;
|
|
||||||
this.status.set('Initialized');
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
async submitPostViaNetwork(forumNode, post, stake) {
|
|
||||||
// TODO: Include fee
|
|
||||||
const postMessage = new PostMessage({ post, stake });
|
|
||||||
await postMessage.sign(this.reputationKey);
|
|
||||||
await this.actions.submitPostViaNetwork.log(this, forumNode, null, { id: post.id });
|
|
||||||
// For now, directly call forumNode.receiveMessage();
|
|
||||||
await forumNode.receiveMessage(JSON.stringify(postMessage.toJSON()));
|
|
||||||
}
|
|
||||||
|
|
||||||
async submitPost(forum, postContent) {
|
|
||||||
// TODO: Include fee
|
|
||||||
await this.actions.submitPost.log(this, forum);
|
|
||||||
return forum.addPost(this.reputationPublicKey, postContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
async submitPostWithFee(bench, forum, postContent, poolOptions) {
|
|
||||||
await this.actions.submitPost.log(this, forum);
|
|
||||||
const postId = await forum.addPost(this.reputationPublicKey, postContent);
|
|
||||||
const pool = await this.initiateValidationPool(bench, { ...poolOptions, postId });
|
|
||||||
this.tokens.push(pool.tokenId);
|
|
||||||
return { postId, pool };
|
|
||||||
}
|
|
||||||
|
|
||||||
async initiateValidationPool(bench, poolOptions) {
|
|
||||||
// For now, directly call bench.initiateValidationPool();
|
|
||||||
poolOptions.reputationPublicKey = this.reputationPublicKey;
|
|
||||||
await this.actions.initiateValidationPool.log(
|
|
||||||
this,
|
|
||||||
bench,
|
|
||||||
`(fee: ${poolOptions.fee}, stake: ${poolOptions.authorStakeAmount ?? 0})`,
|
|
||||||
);
|
|
||||||
const pool = await bench.initiateValidationPool(poolOptions);
|
|
||||||
this.tokens.push(pool.tokenId);
|
|
||||||
this.validationPools.set(pool.id, poolOptions);
|
|
||||||
return pool;
|
|
||||||
}
|
|
||||||
|
|
||||||
async stake(validationPool, {
|
|
||||||
position, amount, lockingTime,
|
|
||||||
}) {
|
|
||||||
// TODO: encrypt stake
|
|
||||||
// TODO: sign message
|
|
||||||
await this.actions.stake.log(
|
|
||||||
this,
|
|
||||||
validationPool,
|
|
||||||
`(${position ? 'for' : 'against'}, stake: ${amount})`,
|
|
||||||
);
|
|
||||||
return validationPool.stake(this.reputationPublicKey, {
|
|
||||||
position, amount, lockingTime, tokenId: this.tokens[0],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async registerAvailability(availability, stakeAmount, duration) {
|
|
||||||
await this.actions.registerAvailability.log(this, availability, `(stake: ${stakeAmount}, duration: ${duration})`);
|
|
||||||
this.workerId = await availability.register(this.reputationPublicKey, {
|
|
||||||
stakeAmount,
|
|
||||||
tokenId: this.tokens[0],
|
|
||||||
duration,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAssignedWork(availability, business) {
|
|
||||||
const requestId = await availability.getAssignedWork(this.workerId);
|
|
||||||
const request = await business.getRequest(requestId);
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
async submitWork(business, requestId, evidence, { tokenLossRatio, duration }) {
|
|
||||||
await this.actions.submitWork.log(this, business);
|
|
||||||
return business.submitWork(this.reputationPublicKey, requestId, evidence, { tokenLossRatio, duration });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
export class Token {
|
|
||||||
constructor(ownerPublicKey) {
|
|
||||||
this.ownerPublicKey = ownerPublicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
transfer(newOwnerPublicKey) {
|
|
||||||
// TODO: Current owner must sign this request
|
|
||||||
this.ownerPublicKey = newOwnerPublicKey;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
export class ForumNetwork {
|
|
||||||
constructor() {
|
|
||||||
this.nodes = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
addNode(node) {
|
|
||||||
this.nodes.set(node.keyPair.publicKey, node);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
listNodes() {
|
|
||||||
return Array.from(this.nodes.values());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
import { Actor } from './actor.js';
|
|
||||||
import { Action } from './action.js';
|
|
||||||
import {
|
|
||||||
Message, PostMessage, PeerMessage, messageFromJSON,
|
|
||||||
} from './message.js';
|
|
||||||
import { CryptoUtil } from './crypto.js';
|
|
||||||
import { ForumView } from './forum-view.js';
|
|
||||||
import { PrioritizedQueue } from './prioritized-queue.js';
|
|
||||||
|
|
||||||
export class ForumNode extends Actor {
|
|
||||||
constructor(name, scene) {
|
|
||||||
super(name, scene);
|
|
||||||
this.forumView = new ForumView();
|
|
||||||
this.queue = new PrioritizedQueue();
|
|
||||||
this.actions = {
|
|
||||||
storePost: new Action('store post', scene),
|
|
||||||
peerMessage: new Action('peer message', scene),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a signing key pair and connect to the network
|
|
||||||
async initialize(forumNetwork) {
|
|
||||||
this.keyPair = await CryptoUtil.generateAsymmetricKey();
|
|
||||||
this.forumNetwork = forumNetwork.addNode(this);
|
|
||||||
this.status.set('Initialized');
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send a message to all other nodes in the network
|
|
||||||
async broadcast(message) {
|
|
||||||
await message.sign(this.keyPair);
|
|
||||||
const otherForumNodes = this.forumNetwork
|
|
||||||
.listNodes()
|
|
||||||
.filter((forumNode) => forumNode.keyPair.publicKey !== this.keyPair.publicKey);
|
|
||||||
for (const forumNode of otherForumNodes) {
|
|
||||||
// For now just call receiveMessage on the target node
|
|
||||||
await this.actions.peerMessage.log(this, forumNode, null, message.content);
|
|
||||||
await forumNode.receiveMessage(JSON.stringify(message.toJSON()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform minimal processing to ingest a message.
|
|
||||||
// Enqueue it for further processing.
|
|
||||||
async receiveMessage(messageStr) {
|
|
||||||
const messageJson = JSON.parse(messageStr);
|
|
||||||
const senderReputation = this.forumView.getReputation(messageJson.publicKey) || 0;
|
|
||||||
this.queue.add(messageJson, senderReputation);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process next highest priority message in the queue
|
|
||||||
async processNextMessage() {
|
|
||||||
const messageJson = this.queue.pop();
|
|
||||||
if (!messageJson) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return this.processMessage(messageJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process a message from the queue
|
|
||||||
async processMessage(messageJson) {
|
|
||||||
try {
|
|
||||||
await Message.verify(messageJson);
|
|
||||||
} catch (e) {
|
|
||||||
await this.actions.processMessage.log(this, this, 'invalid signature', messageJson, '-x');
|
|
||||||
console.log(`${this.name}: received message with invalid signature`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { publicKey } = messageJson;
|
|
||||||
const message = messageFromJSON(messageJson);
|
|
||||||
console.log(`${this.name}: processMessage`, message);
|
|
||||||
|
|
||||||
if (message instanceof PostMessage) {
|
|
||||||
await this.processPostMessage(publicKey, message.content);
|
|
||||||
} else if (message instanceof PeerMessage) {
|
|
||||||
await this.processPeerMessage(publicKey, message.content);
|
|
||||||
} else {
|
|
||||||
// Unknown message type
|
|
||||||
// Penalize sender for wasting our time
|
|
||||||
console.log(`${this.name}: penalizing sender for unknown message type ${message.type}`);
|
|
||||||
this.forumView.incrementReputation(message.publicKey, -1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process an incoming post, received by whatever means
|
|
||||||
async processPost(authorId, post, stake) {
|
|
||||||
if (!post.id) {
|
|
||||||
post.id = CryptoUtil.randomUUID();
|
|
||||||
}
|
|
||||||
await this.actions.storePost.log(this, this, null, { authorId, post, stake });
|
|
||||||
this.forumView.addPost(authorId, post.id, post, stake);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process a post we received in a message
|
|
||||||
async processPostMessage(authorId, { post, stake }) {
|
|
||||||
this.processPost(authorId, post, stake);
|
|
||||||
await this.broadcast(
|
|
||||||
new PeerMessage({
|
|
||||||
posts: [{ authorId, post, stake }],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process a message we receive from a peer
|
|
||||||
async processPeerMessage(peerId, { posts }) {
|
|
||||||
// We are trusting that the peer verified the signatures of the posts they're forwarding.
|
|
||||||
// We could instead have the peer forward the signed messages and re-verify them.
|
|
||||||
for (const { authorId, post, stake } of posts) {
|
|
||||||
this.processPost(authorId, post, stake);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,130 +0,0 @@
|
||||||
import { Graph } from './graph.js';
|
|
||||||
|
|
||||||
class Author {
|
|
||||||
constructor() {
|
|
||||||
this.posts = new Map();
|
|
||||||
this.reputation = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PostVertex {
|
|
||||||
constructor(id, author, stake, content, citations) {
|
|
||||||
this.id = id;
|
|
||||||
this.author = author;
|
|
||||||
this.content = content;
|
|
||||||
this.stake = stake;
|
|
||||||
this.citations = citations;
|
|
||||||
this.reputation = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ForumView {
|
|
||||||
constructor() {
|
|
||||||
this.reputations = new Map();
|
|
||||||
this.posts = new Graph();
|
|
||||||
this.authors = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
getReputation(id) {
|
|
||||||
return this.reputations.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
setReputation(id, reputation) {
|
|
||||||
this.reputations.set(id, reputation);
|
|
||||||
}
|
|
||||||
|
|
||||||
incrementReputation(publicKey, increment, _reason) {
|
|
||||||
const reputation = this.getReputation(publicKey) || 0;
|
|
||||||
return this.reputations.set(publicKey, reputation + increment);
|
|
||||||
}
|
|
||||||
|
|
||||||
getOrInitializeAuthor(authorId) {
|
|
||||||
let author = this.authors.get(authorId);
|
|
||||||
if (!author) {
|
|
||||||
author = new Author(authorId);
|
|
||||||
this.authors.set(authorId, author);
|
|
||||||
}
|
|
||||||
return author;
|
|
||||||
}
|
|
||||||
|
|
||||||
addPost(authorId, postId, postContent, stake) {
|
|
||||||
const { citations = [], content } = postContent;
|
|
||||||
const author = this.getOrInitializeAuthor(authorId);
|
|
||||||
const postVertex = new PostVertex(postId, author, stake, content, citations);
|
|
||||||
console.log('addPost', { id: postId, postContent });
|
|
||||||
this.posts.addVertex(postId, postVertex);
|
|
||||||
for (const citation of citations) {
|
|
||||||
this.posts.addEdge('citation', postId, citation.postId, citation);
|
|
||||||
}
|
|
||||||
this.applyNonbindingReputationEffects(postVertex);
|
|
||||||
}
|
|
||||||
|
|
||||||
getPost(postId) {
|
|
||||||
return this.posts.getVertexData(postId);
|
|
||||||
}
|
|
||||||
|
|
||||||
getPosts() {
|
|
||||||
return this.posts.getVertices();
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll start with naieve implementations of the computations we need.
|
|
||||||
|
|
||||||
// We want to derive a value -- maybe call it a reputation score -- for each post.
|
|
||||||
// This value is a recursive sum of contributions from citations.
|
|
||||||
|
|
||||||
// There should be a diminishment of effect upon each recursion,
|
|
||||||
// perhaps following a geometric progression.
|
|
||||||
|
|
||||||
// Each post gets some initial score due to the reputation that the author stakes.
|
|
||||||
|
|
||||||
// Citations are weighted, and can be positive or negative.
|
|
||||||
|
|
||||||
// So each post has a reputation score. Each author also has a reputation score.
|
|
||||||
// The value of the author's reputation score could be a factor in the magnitude of the effects of their citations.
|
|
||||||
// Post_rep = (Author_rep * stake);
|
|
||||||
//
|
|
||||||
|
|
||||||
// Options:
|
|
||||||
// - update a state model incrementally with each action in the history (/unfolding present) of the forum,
|
|
||||||
// in order to arrive at the current view.
|
|
||||||
|
|
||||||
// When an author stakes reputation on a post, if it's a non-binding stake, then it merely expresses opinion.
|
|
||||||
// If it's a binding stake, then they may lose the staked reputation as a result of other posts
|
|
||||||
// staking reputation against theirs.
|
|
||||||
|
|
||||||
citationFraction = 0.3;
|
|
||||||
|
|
||||||
applyNonbindingReputationEffects(newPost) {
|
|
||||||
this.distributeNonbindingReputation(newPost, newPost, newPost.stake);
|
|
||||||
}
|
|
||||||
|
|
||||||
distributeNonbindingReputation(newPost, post, amount, depth = 0) {
|
|
||||||
console.log('distributeNonbindingReputation', { post, amount, depth });
|
|
||||||
// Some of the incoming reputation goes to this post
|
|
||||||
post.reputation += amount * (1 - this.citationFraction);
|
|
||||||
this.setReputation(post.id, post.reputation);
|
|
||||||
// Some of the incoming reputation gets distributed among cited posts
|
|
||||||
const distributeAmongCitations = amount * this.citationFraction;
|
|
||||||
|
|
||||||
// citation weights can be interpreted as a ratio, or we can somehow constrain the input
|
|
||||||
// to add up to some specified total.
|
|
||||||
// It's easy enough to let them be on any arbitrary scale and just compute the ratios here.
|
|
||||||
const totalWeight = post.citations
|
|
||||||
?.map(({ weight }) => weight)
|
|
||||||
.reduce((acc, cur) => (acc += cur), 0);
|
|
||||||
|
|
||||||
post.citations?.forEach((citation) => {
|
|
||||||
const citedPost = this.getPost(citation.postId);
|
|
||||||
if (!citedPost) {
|
|
||||||
// TODO: Here is where we may want to engage our peer protocol to query for possible missing records
|
|
||||||
throw new Error(`Post ${post.postId} cites unknown post ${citation.postId}`);
|
|
||||||
}
|
|
||||||
this.distributeNonbindingReputation(
|
|
||||||
newPost,
|
|
||||||
citedPost,
|
|
||||||
(citation.weight / totalWeight) * distributeAmongCitations,
|
|
||||||
depth + 1,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,150 +0,0 @@
|
||||||
import { Actor } from './actor.js';
|
|
||||||
import { Graph } from './graph.js';
|
|
||||||
import { Action } from './action.js';
|
|
||||||
import { CryptoUtil } from './crypto.js';
|
|
||||||
import params from '../params.js';
|
|
||||||
import { ReputationHolder } from './reputation-holder.js';
|
|
||||||
|
|
||||||
class Post extends Actor {
|
|
||||||
constructor(forum, authorPublicKey, postContent) {
|
|
||||||
const index = forum.posts.countVertices();
|
|
||||||
const name = `Post${index + 1}`;
|
|
||||||
super(name, forum.scene);
|
|
||||||
this.id = postContent.id ?? `post_${CryptoUtil.randomUUID()}`;
|
|
||||||
this.authorPublicKey = authorPublicKey;
|
|
||||||
this.value = 0;
|
|
||||||
this.citations = postContent.citations;
|
|
||||||
this.title = postContent.title;
|
|
||||||
const revaluationTotal = this.citations.reduce((total, { weight }) => total += Math.abs(weight), 0);
|
|
||||||
if (revaluationTotal > params.revaluationLimit) {
|
|
||||||
throw new Error('Post revaluation total exceeds revaluation limit '
|
|
||||||
+ `(${revaluationTotal} > ${params.revaluationLimit})`);
|
|
||||||
}
|
|
||||||
if (this.citations.some(({ weight }) => Math.abs(weight) > 1)) {
|
|
||||||
throw new Error('Each citation weight must be in the range [-1, 1]');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Purpose: Maintain a directed, acyclic, weighted graph of posts referencing other posts
|
|
||||||
*/
|
|
||||||
export class Forum extends ReputationHolder {
|
|
||||||
constructor(name, scene) {
|
|
||||||
super(`forum_${CryptoUtil.randomUUID()}`, name, scene);
|
|
||||||
this.id = this.reputationPublicKey;
|
|
||||||
this.posts = new Graph(scene);
|
|
||||||
this.actions = {
|
|
||||||
addPost: new Action('add post', scene),
|
|
||||||
propagateValue: new Action('propagate value', this.scene),
|
|
||||||
transfer: new Action('transfer', this.scene),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async addPost(authorId, postContent) {
|
|
||||||
const post = new Post(this, authorId, postContent);
|
|
||||||
await this.actions.addPost.log(this, post);
|
|
||||||
this.posts.addVertex(post.id, post, post.title);
|
|
||||||
if (this.scene.flowchart) {
|
|
||||||
this.scene.flowchart.log(`${post.id} -- value --> ${post.id}_value[0]`);
|
|
||||||
}
|
|
||||||
for (const { postId: citedPostId, weight } of post.citations) {
|
|
||||||
this.posts.addEdge('citation', post.id, citedPostId, { weight });
|
|
||||||
if (this.scene.flowchart) {
|
|
||||||
this.scene.flowchart.log(`${post.id} -- ${weight} --> ${citedPostId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return post.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPost(postId) {
|
|
||||||
return this.posts.getVertexData(postId);
|
|
||||||
}
|
|
||||||
|
|
||||||
getPosts() {
|
|
||||||
return this.posts.getVerticesData();
|
|
||||||
}
|
|
||||||
|
|
||||||
async setPostValue(post, value) {
|
|
||||||
post.value = value;
|
|
||||||
await post.setValue('value', value);
|
|
||||||
if (this.scene.flowchart) {
|
|
||||||
this.scene.flowchart.log(`${post.id}_value[${value}]`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getTotalValue() {
|
|
||||||
return this.getPosts().reduce((total, { value }) => total += value, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onValidate({
|
|
||||||
bench, pool, postId, tokenId,
|
|
||||||
}) {
|
|
||||||
this.activate();
|
|
||||||
const initialValue = bench.reputation.valueOf(tokenId);
|
|
||||||
|
|
||||||
if (this.scene.flowchart) {
|
|
||||||
this.scene.flowchart.log(`${postId}_initial_value[${initialValue}] -- initial value --> ${postId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const post = this.getPost(postId);
|
|
||||||
post.setStatus('Validated');
|
|
||||||
|
|
||||||
// Store a reference to the reputation token associated with this post,
|
|
||||||
// so that its value can be updated by future validated posts.
|
|
||||||
post.tokenId = tokenId;
|
|
||||||
|
|
||||||
// Compute rewards
|
|
||||||
const rewardsAccumulator = new Map();
|
|
||||||
await this.propagateValue(rewardsAccumulator, pool, post, initialValue);
|
|
||||||
|
|
||||||
// Apply computed rewards to update values of tokens
|
|
||||||
for (const [id, value] of rewardsAccumulator) {
|
|
||||||
bench.reputation.transferValueFrom(post.tokenId, id, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transfer ownership of the minted/staked token, from the forum to the post author
|
|
||||||
bench.reputation.transferFrom(this.id, post.authorPublicKey, post.tokenId);
|
|
||||||
const toActor = this.scene.findActor((actor) => actor.reputationPublicKey === post.authorPublicKey);
|
|
||||||
const value = bench.reputation.valueOf(post.tokenId);
|
|
||||||
this.actions.transfer.log(this, toActor, `(value: ${value})`);
|
|
||||||
this.deactivate();
|
|
||||||
}
|
|
||||||
|
|
||||||
async propagateValue(rewardsAccumulator, fromActor, post, increment, depth = 0) {
|
|
||||||
if (params.referenceChainLimit >= 0 && depth > params.referenceChainLimit) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.actions.propagateValue.log(fromActor, post, `(${increment})`);
|
|
||||||
|
|
||||||
// Recursively distribute reputation to citations, according to weights
|
|
||||||
let totalOutboundAmount = 0;
|
|
||||||
let refundFromOutbound = 0;
|
|
||||||
for (const { postId: citedPostId, weight } of post.citations) {
|
|
||||||
const citedPost = this.getPost(citedPostId);
|
|
||||||
const outboundAmount = weight * increment;
|
|
||||||
totalOutboundAmount += outboundAmount;
|
|
||||||
refundFromOutbound += await this.propagateValue(rewardsAccumulator, post, citedPost, outboundAmount, depth + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply leaching value
|
|
||||||
const incrementAfterLeaching = increment - (totalOutboundAmount - refundFromOutbound) * params.leachingValue;
|
|
||||||
|
|
||||||
// Prevent value from decreasing below zero
|
|
||||||
const rawNewValue = post.value + incrementAfterLeaching;
|
|
||||||
const newValue = Math.max(0, rawNewValue);
|
|
||||||
// We "refund" the amount that could not be applied.
|
|
||||||
// Note that this will always be a negative quantity, because this situation only arises when increment is negative.
|
|
||||||
const refundToInbound = rawNewValue - newValue;
|
|
||||||
const appliedIncrement = newValue - post.value;
|
|
||||||
|
|
||||||
// Award reputation to post author
|
|
||||||
rewardsAccumulator.set(post.tokenId, appliedIncrement);
|
|
||||||
|
|
||||||
// Increment the value of the post
|
|
||||||
await this.setPostValue(post, newValue);
|
|
||||||
|
|
||||||
return refundToInbound;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,105 +0,0 @@
|
||||||
export class Vertex {
|
|
||||||
constructor(data) {
|
|
||||||
this.data = data;
|
|
||||||
this.edges = {
|
|
||||||
from: [],
|
|
||||||
to: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getEdges(label, away) {
|
|
||||||
return this.edges[away ? 'from' : 'to'].filter(
|
|
||||||
(edge) => edge.label === label,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Edge {
|
|
||||||
constructor(label, from, to, data) {
|
|
||||||
this.from = from;
|
|
||||||
this.to = to;
|
|
||||||
this.label = label;
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CategorizedEdges {}
|
|
||||||
|
|
||||||
export class Graph {
|
|
||||||
constructor(scene) {
|
|
||||||
this.scene = scene;
|
|
||||||
this.vertices = new Map();
|
|
||||||
this.edgeLabels = new Map();
|
|
||||||
this.nextVertexId = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
addVertex(id, data, label) {
|
|
||||||
// Support simple case of auto-incremented numeric ids
|
|
||||||
if (typeof id === 'object') {
|
|
||||||
data = id;
|
|
||||||
id = this.nextVertexId++;
|
|
||||||
}
|
|
||||||
if (this.vertices.has(id)) {
|
|
||||||
throw new Error(`Vertex already exists with id: ${id}`);
|
|
||||||
}
|
|
||||||
const vertex = new Vertex(data);
|
|
||||||
this.vertices.set(id, vertex);
|
|
||||||
if (this.scene.flowchart) {
|
|
||||||
this.scene.flowchart.log(`${id}[${label ?? id}]`);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
getVertex(id) {
|
|
||||||
return this.vertices.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
getVertexData(id) {
|
|
||||||
return this.getVertex(id)?.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
getVerticesData() {
|
|
||||||
return Array.from(this.vertices.values()).map(({ data }) => data);
|
|
||||||
}
|
|
||||||
|
|
||||||
getEdge(label, from, to) {
|
|
||||||
const edges = this.edgeLabels.get(label);
|
|
||||||
return edges?.get(JSON.stringify({ from, to }));
|
|
||||||
}
|
|
||||||
|
|
||||||
setEdge(label, from, to, edge) {
|
|
||||||
let edges = this.edgeLabels.get(label);
|
|
||||||
if (!edges) {
|
|
||||||
edges = new Map();
|
|
||||||
this.edgeLabels.set(label, edges);
|
|
||||||
}
|
|
||||||
edges.set(JSON.stringify({ from, to }), edge);
|
|
||||||
}
|
|
||||||
|
|
||||||
addEdge(label, from, to, data) {
|
|
||||||
if (this.getEdge(label, from, to)) {
|
|
||||||
throw new Error(`Edge ${label} from ${from} to ${to} already exists`);
|
|
||||||
}
|
|
||||||
const edge = new Edge(label, from, to, data);
|
|
||||||
this.setEdge(label, from, to, edge);
|
|
||||||
this.getVertex(from).edges.from.push(edge);
|
|
||||||
this.getVertex(to).edges.to.push(edge);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
getEdges(label, from, to) {
|
|
||||||
const edgeLabels = label ? [label] : Array.from(this.edgeLabels.keys());
|
|
||||||
return edgeLabels.flatMap((edgeLabel) => {
|
|
||||||
const edges = this.edgeLabels.get(edgeLabel);
|
|
||||||
return Array.from(edges?.values() || []).filter((edge) => {
|
|
||||||
const matchFrom = from === null || from === undefined || from === edge.from;
|
|
||||||
const matchTo = to === null || to === undefined || to === edge.to;
|
|
||||||
return matchFrom && matchTo;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
countVertices() {
|
|
||||||
return this.vertices.size;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
import { CryptoUtil } from './crypto.js';
|
|
||||||
import { PostContent } from './post-content.js';
|
|
||||||
|
|
||||||
export class Message {
|
|
||||||
constructor(content) {
|
|
||||||
this.content = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
async sign({ publicKey, privateKey }) {
|
|
||||||
this.publicKey = await CryptoUtil.exportKey(publicKey);
|
|
||||||
// Call toJSON before signing, to match what we'll later send
|
|
||||||
this.signature = await CryptoUtil.sign(this.contentToJSON(), privateKey);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async verify({ content, publicKey, signature }) {
|
|
||||||
return CryptoUtil.verify(content, publicKey, signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
contentToJSON() {
|
|
||||||
return this.content;
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
return {
|
|
||||||
type: this.type,
|
|
||||||
content: this.contentToJSON(),
|
|
||||||
publicKey: this.publicKey,
|
|
||||||
signature: this.signature,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PostMessage extends Message {
|
|
||||||
type = 'post';
|
|
||||||
|
|
||||||
constructor({ post, stake }) {
|
|
||||||
super({
|
|
||||||
post: PostContent.fromJSON(post),
|
|
||||||
stake,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
contentToJSON() {
|
|
||||||
return {
|
|
||||||
post: this.content.post.toJSON(),
|
|
||||||
stakeAmount: this.content.stake,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PeerMessage extends Message {
|
|
||||||
type = 'peer';
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageTypes = new Map([
|
|
||||||
['post', PostMessage],
|
|
||||||
['peer', PeerMessage],
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const messageFromJSON = ({ type, content }) => {
|
|
||||||
const MessageType = messageTypes.get(type) || Message;
|
|
||||||
// const messageContent = MessageType.contentFromJSON(content);
|
|
||||||
return new MessageType(content);
|
|
||||||
};
|
|
|
@ -1,54 +0,0 @@
|
||||||
export class Citation {
|
|
||||||
constructor(postId, weight) {
|
|
||||||
this.postId = postId;
|
|
||||||
this.weight = weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
return {
|
|
||||||
postId: this.postId,
|
|
||||||
weight: this.weight,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromJSON({ postId, weight }) {
|
|
||||||
return new Citation(postId, weight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PostContent {
|
|
||||||
constructor(content) {
|
|
||||||
this.content = content;
|
|
||||||
this.citations = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
addCitation(postId, weight) {
|
|
||||||
const citation = new Citation(postId, weight);
|
|
||||||
this.citations.push(citation);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTitle(title) {
|
|
||||||
this.title = title;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
return {
|
|
||||||
content: this.content,
|
|
||||||
citations: this.citations.map((citation) => citation.toJSON()),
|
|
||||||
...(this.id ? { id: this.id } : {}),
|
|
||||||
title: this.title,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromJSON({
|
|
||||||
id, content, citations, title,
|
|
||||||
}) {
|
|
||||||
const post = new PostContent(content);
|
|
||||||
post.citations = citations.map((citation) => Citation.fromJSON(citation));
|
|
||||||
post.id = id;
|
|
||||||
post.title = title;
|
|
||||||
return post;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
export class PrioritizedQueue {
|
|
||||||
constructor() {
|
|
||||||
this.buffer = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add an item to the buffer, ahead of the next lowest priority item
|
|
||||||
add(message, priority) {
|
|
||||||
const idx = this.buffer.findIndex((item) => item.priority < priority);
|
|
||||||
if (idx < 0) {
|
|
||||||
this.buffer.push({ message, priority });
|
|
||||||
} else {
|
|
||||||
this.buffer.splice(idx, 0, { message, priority });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the highest priority item in the buffer
|
|
||||||
pop() {
|
|
||||||
if (!this.buffer.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const item = this.buffer.shift();
|
|
||||||
return item.message;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { Action } from './action.js';
|
|
||||||
import { Actor } from './actor.js';
|
|
||||||
|
|
||||||
export class Public extends Actor {
|
|
||||||
constructor(name, scene) {
|
|
||||||
super(name, scene);
|
|
||||||
this.actions = {
|
|
||||||
submitRequest: new Action('submit work request', scene),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async submitRequest(business, { fee }, content) {
|
|
||||||
this.actions.submitRequest.log(this, business, `(fee: ${fee})`);
|
|
||||||
return business.submitRequest(fee, content);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { Actor } from './actor.js';
|
|
||||||
|
|
||||||
export class ReputationHolder extends Actor {
|
|
||||||
constructor(reputationPublicKey, name, scene) {
|
|
||||||
super(name, scene);
|
|
||||||
this.reputationPublicKey = reputationPublicKey;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
import { ERC721 } from './erc721.js';
|
|
||||||
import { CryptoUtil } from './crypto.js';
|
|
||||||
|
|
||||||
const EPSILON = 2.23e-16;
|
|
||||||
|
|
||||||
class Lock {
|
|
||||||
constructor(tokenId, amount, duration) {
|
|
||||||
this.dateCreated = new Date();
|
|
||||||
this.tokenId = tokenId;
|
|
||||||
this.amount = amount;
|
|
||||||
this.duration = duration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ReputationTokenContract extends ERC721 {
|
|
||||||
constructor() {
|
|
||||||
super('Reputation', 'REP');
|
|
||||||
this.histories = new Map(); // token id --> {increment, context (i.e. validation pool id)}
|
|
||||||
this.values = new Map(); // token id --> current value
|
|
||||||
this.locks = new Set(); // {tokenId, amount, start, duration}
|
|
||||||
}
|
|
||||||
|
|
||||||
mint(to, value, context) {
|
|
||||||
const tokenId = `token_${CryptoUtil.randomUUID()}`;
|
|
||||||
super.mint(to, tokenId);
|
|
||||||
this.values.set(tokenId, value);
|
|
||||||
this.histories.set(tokenId, [{ increment: value, context }]);
|
|
||||||
return tokenId;
|
|
||||||
}
|
|
||||||
|
|
||||||
incrementValue(tokenId, increment, context) {
|
|
||||||
const value = this.values.get(tokenId);
|
|
||||||
const newValue = value + increment;
|
|
||||||
const history = this.histories.get(tokenId) || [];
|
|
||||||
|
|
||||||
if (newValue < -EPSILON) {
|
|
||||||
throw new Error(`Token value can not become negative. Attempted to set value = ${newValue}`);
|
|
||||||
}
|
|
||||||
this.values.set(tokenId, newValue);
|
|
||||||
history.push({ increment, context });
|
|
||||||
this.histories.set(tokenId, history);
|
|
||||||
}
|
|
||||||
|
|
||||||
transferValueFrom(fromTokenId, toTokenId, amount) {
|
|
||||||
if (amount === undefined) {
|
|
||||||
throw new Error('Transfer value: amount is undefined!');
|
|
||||||
}
|
|
||||||
const sourceAvailable = this.availableValueOf(fromTokenId);
|
|
||||||
const targetAvailable = this.availableValueOf(toTokenId);
|
|
||||||
if (sourceAvailable < amount - EPSILON) {
|
|
||||||
throw new Error('Token value transfer: source has insufficient available value. '
|
|
||||||
+ `Needs ${amount}; has ${sourceAvailable}.`);
|
|
||||||
}
|
|
||||||
if (targetAvailable < -amount + EPSILON) {
|
|
||||||
throw new Error('Token value transfer: target has insufficient available value. '
|
|
||||||
+ `Needs ${-amount}; has ${targetAvailable}.`);
|
|
||||||
}
|
|
||||||
this.incrementValue(fromTokenId, -amount);
|
|
||||||
this.incrementValue(toTokenId, amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
lock(tokenId, amount, duration) {
|
|
||||||
const lock = new Lock(tokenId, amount, duration);
|
|
||||||
this.locks.add(lock);
|
|
||||||
}
|
|
||||||
|
|
||||||
historyOf(tokenId) {
|
|
||||||
return this.histories.get(tokenId);
|
|
||||||
}
|
|
||||||
|
|
||||||
valueOf(tokenId) {
|
|
||||||
return this.values.get(tokenId);
|
|
||||||
}
|
|
||||||
|
|
||||||
availableValueOf(tokenId) {
|
|
||||||
const amountLocked = Array.from(this.locks.values())
|
|
||||||
.filter(({ tokenId: lockTokenId }) => lockTokenId === tokenId)
|
|
||||||
.filter(({ dateCreated, duration }) => new Date() - dateCreated < duration)
|
|
||||||
.reduce((total, { amount }) => total += amount, 0);
|
|
||||||
|
|
||||||
return this.valueOf(tokenId) - amountLocked;
|
|
||||||
}
|
|
||||||
|
|
||||||
valueOwnedBy(ownerId) {
|
|
||||||
return Array.from(this.owners.entries())
|
|
||||||
.filter(([__, owner]) => owner === ownerId)
|
|
||||||
.map(([tokenId, __]) => this.valueOf(tokenId))
|
|
||||||
.reduce((total, value) => total += value, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
availableValueOwnedBy(ownerId) {
|
|
||||||
return Array.from(this.owners.entries())
|
|
||||||
.filter(([__, owner]) => owner === ownerId)
|
|
||||||
.map(([tokenId, __]) => this.availableValueOf(tokenId))
|
|
||||||
.reduce((total, value) => total += value, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
getTotal() {
|
|
||||||
return Array.from(this.values.values()).reduce((total, value) => total += value, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
getTotalAvailable() {
|
|
||||||
const amountLocked = Array.from(this.locks.values())
|
|
||||||
.filter(({ dateCreated, duration }) => new Date() - dateCreated < duration)
|
|
||||||
.reduce((total, { amount }) => total += amount, 0);
|
|
||||||
|
|
||||||
return this.getTotal() - amountLocked;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,88 +0,0 @@
|
||||||
class Lock {
|
|
||||||
constructor(tokens, duration) {
|
|
||||||
this.dateCreated = new Date();
|
|
||||||
this.tokens = tokens;
|
|
||||||
this.duration = duration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Reputation {
|
|
||||||
constructor() {
|
|
||||||
this.tokens = 0;
|
|
||||||
this.locks = new Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
addTokens(tokens) {
|
|
||||||
if (this.tokens + tokens < 0) {
|
|
||||||
throw new Error('Token balance can not become negative');
|
|
||||||
}
|
|
||||||
this.tokens += tokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
lockTokens(tokens, duration) {
|
|
||||||
if (tokens > this.getAvailableTokens()) {
|
|
||||||
throw new Error('Can not lock more tokens than are available');
|
|
||||||
}
|
|
||||||
const lock = new Lock(tokens, duration);
|
|
||||||
this.locks.add(lock);
|
|
||||||
// TODO: Prune locks once expired
|
|
||||||
}
|
|
||||||
|
|
||||||
getTokens() {
|
|
||||||
return this.tokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAvailableTokens() {
|
|
||||||
const now = new Date();
|
|
||||||
const tokensLocked = Array.from(this.locks.values())
|
|
||||||
.filter(({ dateCreated, duration }) => now - dateCreated < duration)
|
|
||||||
.reduce((acc, cur) => acc += cur.tokens, 0);
|
|
||||||
if (tokensLocked > this.tokens) {
|
|
||||||
throw new Error('Assertion failure. tokensLocked > tokens');
|
|
||||||
}
|
|
||||||
return this.tokens - tokensLocked;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Reputations extends Map {
|
|
||||||
getTokens(reputationPublicKey) {
|
|
||||||
const reputation = this.get(reputationPublicKey);
|
|
||||||
if (!reputation) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return reputation.getTokens();
|
|
||||||
}
|
|
||||||
|
|
||||||
getAvailableTokens(reputationPublicKey) {
|
|
||||||
const reputation = this.get(reputationPublicKey);
|
|
||||||
if (!reputation) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return reputation.getAvailableTokens();
|
|
||||||
}
|
|
||||||
|
|
||||||
addTokens(reputationPublicKey, tokens) {
|
|
||||||
const reputation = this.get(reputationPublicKey) ?? new Reputation();
|
|
||||||
reputation.addTokens(tokens);
|
|
||||||
this.set(reputationPublicKey, reputation);
|
|
||||||
}
|
|
||||||
|
|
||||||
lockTokens(reputationPublicKey, tokens, duration) {
|
|
||||||
if (!tokens || !duration) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const reputation = this.get(reputationPublicKey);
|
|
||||||
if (!reputation) {
|
|
||||||
throw new Error(`${reputationPublicKey} has no tokens to lock`);
|
|
||||||
}
|
|
||||||
reputation.lockTokens(tokens, duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
getTotal() {
|
|
||||||
return Array.from(this.values()).reduce((acc, cur) => acc += cur.getTokens(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
getTotalAvailable() {
|
|
||||||
return Array.from(this.values()).reduce((acc, cur) => acc += cur.getAvailableTokens(), 0);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,133 +0,0 @@
|
||||||
import mermaid from 'https://unpkg.com/mermaid@9.2.2/dist/mermaid.esm.min.mjs';
|
|
||||||
import { Actor } from './actor.js';
|
|
||||||
import { Action } from './action.js';
|
|
||||||
import { debounce, hexToRGB } from '../util.js';
|
|
||||||
|
|
||||||
class MermaidDiagram {
|
|
||||||
constructor(box, logBox) {
|
|
||||||
this.box = box;
|
|
||||||
this.container = this.box.addBox('Container');
|
|
||||||
this.element = this.box.addBox('Element');
|
|
||||||
this.renderBox = this.box.addBox('Render');
|
|
||||||
this.box.addBox('Spacer').setInnerHTML(' ');
|
|
||||||
this.logBox = logBox;
|
|
||||||
this.inSection = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async log(msg, render = true) {
|
|
||||||
this.logBox.addBox().setInnerHTML(msg).monospace();
|
|
||||||
if (render) {
|
|
||||||
await this.render();
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
async render() {
|
|
||||||
const render = async () => {
|
|
||||||
let innerText = this.logBox.getInnerText();
|
|
||||||
for (let i = 0; i < this.inSection; i++) {
|
|
||||||
innerText += '\nend';
|
|
||||||
}
|
|
||||||
const graph = await mermaid.mermaidAPI.render(
|
|
||||||
this.element.getId(),
|
|
||||||
innerText,
|
|
||||||
);
|
|
||||||
this.renderBox.setInnerHTML(graph);
|
|
||||||
};
|
|
||||||
await debounce(render, 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Scene {
|
|
||||||
constructor(name, rootBox) {
|
|
||||||
this.name = name;
|
|
||||||
this.box = rootBox.addBox(name);
|
|
||||||
this.titleBox = this.box.addBox('Title').setInnerHTML(name);
|
|
||||||
this.box.addBox('Spacer').setInnerHTML(' ');
|
|
||||||
this.topSection = this.box.addBox('Top section').flex();
|
|
||||||
this.displayValuesBox = this.topSection.addBox('Values');
|
|
||||||
this.box.addBox('Spacer').setInnerHTML(' ');
|
|
||||||
this.actors = new Set();
|
|
||||||
|
|
||||||
mermaid.mermaidAPI.initialize({
|
|
||||||
startOnLoad: false,
|
|
||||||
theme: 'base',
|
|
||||||
themeVariables: {
|
|
||||||
darkMode: true,
|
|
||||||
primaryColor: '#2a5b6c',
|
|
||||||
primaryTextColor: '#b6b6b6',
|
|
||||||
// lineColor: '#349cbd',
|
|
||||||
lineColor: '#57747d',
|
|
||||||
signalColor: '#57747d',
|
|
||||||
// signalColor: '#349cbd',
|
|
||||||
noteBkgColor: '#516f77',
|
|
||||||
noteTextColor: '#cecece',
|
|
||||||
activationBkgColor: '#1d3f49',
|
|
||||||
activationBorderColor: '#569595',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
withSequenceDiagram() {
|
|
||||||
const box = this.box.addBox('Sequence diagram');
|
|
||||||
this.box.addBox('Spacer').setInnerHTML(' ');
|
|
||||||
const logBox = this.box.addBox('Sequence diagram text');
|
|
||||||
this.sequence = new MermaidDiagram(box, logBox);
|
|
||||||
this.sequence.log('sequenceDiagram', false);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
withFlowchart(direction = 'BT') {
|
|
||||||
const box = this.topSection.addBox('Flowchart');
|
|
||||||
this.box.addBox('Spacer').setInnerHTML(' ');
|
|
||||||
const logBox = this.box.addBox('Flowchart text');
|
|
||||||
this.flowchart = new MermaidDiagram(box, logBox);
|
|
||||||
this.flowchart.log(`graph ${direction}`, false);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
async addActor(name) {
|
|
||||||
const actor = new Actor(name, this);
|
|
||||||
if (this.sequence) {
|
|
||||||
await this.scene.sequence.log(`participant ${name}`);
|
|
||||||
}
|
|
||||||
return actor;
|
|
||||||
}
|
|
||||||
|
|
||||||
registerActor(actor) {
|
|
||||||
this.actors.add(actor);
|
|
||||||
}
|
|
||||||
|
|
||||||
findActor(fn) {
|
|
||||||
return Array.from(this.actors.values()).find(fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
addAction(name) {
|
|
||||||
const action = new Action(name, this);
|
|
||||||
return action;
|
|
||||||
}
|
|
||||||
|
|
||||||
addDisplayValue(name) {
|
|
||||||
const dv = this.displayValuesBox.addDisplayValue(name);
|
|
||||||
return dv;
|
|
||||||
}
|
|
||||||
|
|
||||||
async deactivateAll() {
|
|
||||||
for (const actor of this.actors.values()) {
|
|
||||||
while (actor.active) {
|
|
||||||
await actor.deactivate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async startSection(color = '#08252c') {
|
|
||||||
const { r, g, b } = hexToRGB(color);
|
|
||||||
this.sequence.inSection++;
|
|
||||||
this.sequence.log(`rect rgb(${r}, ${g}, ${b})`, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async endSection() {
|
|
||||||
this.sequence.inSection--;
|
|
||||||
this.sequence.log('end');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
import params from '../params.js';
|
|
||||||
|
|
||||||
export class Stake {
|
|
||||||
constructor({
|
|
||||||
tokenId, position, amount, lockingTime,
|
|
||||||
}) {
|
|
||||||
this.tokenId = tokenId;
|
|
||||||
this.position = position;
|
|
||||||
this.amount = amount;
|
|
||||||
this.lockingTime = lockingTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
getStakeValue() {
|
|
||||||
return this.amount * this.lockingTime ** params.lockingTimeExponent;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,270 +0,0 @@
|
||||||
import { CryptoUtil } from './crypto.js';
|
|
||||||
import { ReputationHolder } from './reputation-holder.js';
|
|
||||||
import { Stake } from './stake.js';
|
|
||||||
import { Voter } from './voter.js';
|
|
||||||
import params from '../params.js';
|
|
||||||
import { Action } from './action.js';
|
|
||||||
|
|
||||||
const ValidationPoolStates = Object.freeze({
|
|
||||||
OPEN: 'OPEN',
|
|
||||||
CLOSED: 'CLOSED',
|
|
||||||
RESOLVED: 'RESOLVED',
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Purpose: Enable voting
|
|
||||||
*/
|
|
||||||
export class ValidationPool extends ReputationHolder {
|
|
||||||
constructor(
|
|
||||||
bench,
|
|
||||||
forum,
|
|
||||||
{
|
|
||||||
postId,
|
|
||||||
reputationPublicKey,
|
|
||||||
fee,
|
|
||||||
duration,
|
|
||||||
tokenLossRatio,
|
|
||||||
contentiousDebate = false,
|
|
||||||
},
|
|
||||||
name,
|
|
||||||
scene,
|
|
||||||
) {
|
|
||||||
super(`pool_${CryptoUtil.randomUUID()}`, name, scene);
|
|
||||||
this.id = this.reputationPublicKey;
|
|
||||||
|
|
||||||
this.actions = {
|
|
||||||
reward: new Action('reward', scene),
|
|
||||||
transfer: new Action('transfer', scene),
|
|
||||||
};
|
|
||||||
|
|
||||||
// If contentiousDebate = true, we will follow the progression defined by getTokenLossRatio()
|
|
||||||
if (
|
|
||||||
!contentiousDebate
|
|
||||||
&& (tokenLossRatio < 0
|
|
||||||
|| tokenLossRatio > 1
|
|
||||||
|| [null, undefined].includes(tokenLossRatio))
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
`Token loss ratio must be in the range [0, 1]; got ${tokenLossRatio}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
duration < params.voteDuration.min
|
|
||||||
|| (params.voteDuration.max && duration > params.voteDuration.max)
|
|
||||||
|| [null, undefined].includes(duration)
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
`Duration must be in the range [${params.voteDuration.min}, ${
|
|
||||||
params.voteDuration.max ?? 'Inf'
|
|
||||||
}]; got ${duration}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.bench = bench;
|
|
||||||
this.forum = forum;
|
|
||||||
this.postId = postId;
|
|
||||||
this.state = ValidationPoolStates.OPEN;
|
|
||||||
this.setStatus('Open');
|
|
||||||
this.stakes = new Set();
|
|
||||||
this.dateStart = new Date();
|
|
||||||
this.authorReputationPublicKey = reputationPublicKey;
|
|
||||||
this.fee = fee;
|
|
||||||
this.duration = duration;
|
|
||||||
this.tokenLossRatio = tokenLossRatio;
|
|
||||||
this.contentiousDebate = contentiousDebate;
|
|
||||||
this.mintedValue = fee * params.mintingRatio();
|
|
||||||
this.tokenId = this.bench.reputation.mint(this.id, this.mintedValue);
|
|
||||||
// Tokens minted "for" the post go toward stake of author voting for their own post.
|
|
||||||
// Also, author can provide additional stakes, e.g. availability stakes for work evidence post.
|
|
||||||
this.stake(this.id, {
|
|
||||||
position: true,
|
|
||||||
amount: this.mintedValue * params.stakeForAuthor,
|
|
||||||
tokenId: this.tokenId,
|
|
||||||
});
|
|
||||||
this.stake(this.id, {
|
|
||||||
position: false,
|
|
||||||
amount: this.mintedValue * (1 - params.stakeForAuthor),
|
|
||||||
tokenId: this.tokenId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getTokenLossRatio() {
|
|
||||||
if (!this.contentiousDebate) {
|
|
||||||
return this.tokenLossRatio;
|
|
||||||
}
|
|
||||||
const elapsed = new Date() - this.dateStart;
|
|
||||||
let stageDuration = params.contentiousDebate.period / 2;
|
|
||||||
let stage = 0;
|
|
||||||
let t = 0;
|
|
||||||
while (true) {
|
|
||||||
t += stageDuration;
|
|
||||||
stageDuration /= 2;
|
|
||||||
if (t > elapsed) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
stage += 1;
|
|
||||||
if (stage >= params.contentiousDebate.stages - 1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return stage / (params.contentiousDebate.stages - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
|
|
||||||
* @param {boolean} options.excludeSystem: Whether to exclude votes cast during pool initialization
|
|
||||||
* @returns stake[]
|
|
||||||
*/
|
|
||||||
getStakes(outcome, { excludeSystem }) {
|
|
||||||
return Array.from(this.stakes.values())
|
|
||||||
.filter(({ tokenId }) => !excludeSystem || tokenId !== this.tokenId)
|
|
||||||
.filter(({ position }) => outcome === null || position === outcome);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
|
|
||||||
* @returns number
|
|
||||||
*/
|
|
||||||
getTotalStakedOnPost(outcome) {
|
|
||||||
return this.getStakes(outcome, { excludeSystem: false })
|
|
||||||
.map((stake) => stake.getStakeValue())
|
|
||||||
.reduce((acc, cur) => (acc += cur), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
|
|
||||||
* @returns number
|
|
||||||
*/
|
|
||||||
getTotalValueOfStakesForOutcome(outcome) {
|
|
||||||
return this.getStakes(outcome, { excludeSystem: false })
|
|
||||||
.reduce((total, { amount }) => (total += amount), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This can be handled as a hook on receipt of reputation token transfer
|
|
||||||
async stake(reputationPublicKey, {
|
|
||||||
tokenId, position, amount, lockingTime = 0,
|
|
||||||
}) {
|
|
||||||
if (this.state === ValidationPoolStates.CLOSED) {
|
|
||||||
throw new Error(`Validation pool ${this.id} is closed.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.duration && new Date() - this.dateStart > this.duration) {
|
|
||||||
throw new Error(
|
|
||||||
`Validation pool ${this.id} has expired, no new votes may be cast.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reputationPublicKey !== this.bench.reputation.ownerOf(tokenId)) {
|
|
||||||
throw new Error('Reputation may only be staked by its owner!');
|
|
||||||
}
|
|
||||||
|
|
||||||
const stake = new Stake({
|
|
||||||
tokenId, position, amount, lockingTime,
|
|
||||||
});
|
|
||||||
this.stakes.add(stake);
|
|
||||||
|
|
||||||
// Transfer staked amount from the sender to the validation pool
|
|
||||||
this.bench.reputation.transferValueFrom(tokenId, this.tokenId, amount);
|
|
||||||
|
|
||||||
// Keep a record of voters and their votes
|
|
||||||
if (tokenId !== this.tokenId) {
|
|
||||||
const voter = this.bench.voters.get(reputationPublicKey) ?? new Voter(reputationPublicKey);
|
|
||||||
voter.addVoteRecord(this);
|
|
||||||
this.bench.voters.set(reputationPublicKey, voter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applyTokenLocking() {
|
|
||||||
// Before evaluating the winning conditions,
|
|
||||||
// we need to make sure any staked tokens are locked for the
|
|
||||||
// specified amounts of time.
|
|
||||||
for (const { tokenId, amount, lockingTime } of this.stakes.values()) {
|
|
||||||
this.bench.reputation.lock(tokenId, amount, lockingTime);
|
|
||||||
// TODO: If there is an exception here, the voter may have voted incorrectly. Consider penalties.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async evaluateWinningConditions() {
|
|
||||||
if (this.state === ValidationPoolStates.RESOLVED) {
|
|
||||||
throw new Error('Validation pool has already been resolved!');
|
|
||||||
}
|
|
||||||
const elapsed = new Date() - this.dateStart;
|
|
||||||
if (elapsed < this.duration) {
|
|
||||||
throw new Error(`Validation pool duration has not yet elapsed! ${this.duration - elapsed} ms remaining.`);
|
|
||||||
}
|
|
||||||
// Now we can evaluate winning conditions
|
|
||||||
this.state = ValidationPoolStates.CLOSED;
|
|
||||||
this.setStatus('Closed');
|
|
||||||
|
|
||||||
const upvoteValue = this.getTotalValueOfStakesForOutcome(true);
|
|
||||||
const downvoteValue = this.getTotalValueOfStakesForOutcome(false);
|
|
||||||
const activeAvailableReputation = this.bench.getActiveAvailableReputation();
|
|
||||||
const votePasses = upvoteValue >= params.winningRatio * downvoteValue;
|
|
||||||
const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation;
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
votePasses,
|
|
||||||
upvoteValue,
|
|
||||||
downvoteValue,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (quorumMet) {
|
|
||||||
this.setStatus(`Resolved - ${votePasses ? 'Won' : 'Lost'}`);
|
|
||||||
this.scene.sequence.log(`note over ${this.name} : ${votePasses ? 'Win' : 'Lose'}`);
|
|
||||||
this.applyTokenLocking();
|
|
||||||
await this.distributeReputation({ votePasses });
|
|
||||||
// TODO: distribute fees
|
|
||||||
} else {
|
|
||||||
this.setStatus('Resolved - Quorum not met');
|
|
||||||
this.scene.sequence.log(`note over ${this.name} : Quorum not met`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.deactivate();
|
|
||||||
this.state = ValidationPoolStates.RESOLVED;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async distributeReputation({ votePasses }) {
|
|
||||||
// For now we assume a tightly binding pool, where all staked reputation is lost
|
|
||||||
// TODO: Take tokenLossRatio into account
|
|
||||||
// TODO: revoke staked reputation from losing voters
|
|
||||||
|
|
||||||
// In a tightly binding validation pool, losing voter stakes are transferred to winning voters.
|
|
||||||
const tokensForWinners = this.getTotalStakedOnPost(!votePasses);
|
|
||||||
const winningEntries = this.getStakes(votePasses, { excludeSystem: true });
|
|
||||||
const totalValueOfStakesForWin = this.getTotalValueOfStakesForOutcome(votePasses);
|
|
||||||
|
|
||||||
// Compute rewards for the winning voters, in proportion to the value of their stakes.
|
|
||||||
for (const stake of winningEntries) {
|
|
||||||
const { tokenId, amount } = stake;
|
|
||||||
const value = stake.getStakeValue();
|
|
||||||
const reward = tokensForWinners * (value / totalValueOfStakesForWin);
|
|
||||||
// Also return each winning voter their staked amount
|
|
||||||
const reputationPublicKey = this.bench.reputation.ownerOf(tokenId);
|
|
||||||
console.log(`reward for winning stake by ${reputationPublicKey}: ${reward}`);
|
|
||||||
this.bench.reputation.transferValueFrom(this.tokenId, tokenId, reward + amount);
|
|
||||||
const toActor = this.scene.findActor((actor) => actor.reputationPublicKey === reputationPublicKey);
|
|
||||||
this.actions.reward.log(this, toActor, `(${reward})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (votePasses && !!this.forum) {
|
|
||||||
// Distribute awards to author via the forum
|
|
||||||
// const tokensForAuthor = this.mintedValue * params.stakeForAuthor + rewards.get(this.tokenId);
|
|
||||||
console.log(`sending reward for author stake to forum: ${this.bench.reputation.valueOf(this.tokenId)}`);
|
|
||||||
|
|
||||||
// Transfer ownership of the minted token, from the pool to the forum
|
|
||||||
this.bench.reputation.transferFrom(this.id, this.forum.id, this.tokenId);
|
|
||||||
const value = this.bench.reputation.valueOf(this.tokenId);
|
|
||||||
this.actions.transfer.log(this, this.forum, `(value: ${value})`);
|
|
||||||
|
|
||||||
// Recurse through forum to determine reputation effects
|
|
||||||
await this.forum.onValidate({
|
|
||||||
bench: this.bench,
|
|
||||||
pool: this,
|
|
||||||
postId: this.postId,
|
|
||||||
tokenId: this.tokenId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('pool complete');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
export class Voter {
|
|
||||||
constructor(reputationPublicKey) {
|
|
||||||
this.reputationPublicKey = reputationPublicKey;
|
|
||||||
this.voteHistory = [];
|
|
||||||
this.dateLastVote = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
addVoteRecord(stake) {
|
|
||||||
this.voteHistory.push(stake);
|
|
||||||
if (!this.dateLastVote || stake.dateStart > this.dateLastVote) {
|
|
||||||
this.dateLastVote = stake.dateStart;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 3.5 KiB |
|
@ -1,38 +0,0 @@
|
||||||
body {
|
|
||||||
background-color: #09343f;
|
|
||||||
color: #b6b6b6;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 8pt;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #c6f4ff;
|
|
||||||
}
|
|
||||||
a:visited {
|
|
||||||
color: #85b7c3;
|
|
||||||
}
|
|
||||||
.box {
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
.box .name {
|
|
||||||
width: 15em;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: right;
|
|
||||||
margin-right: 6pt;
|
|
||||||
}
|
|
||||||
.box .value {
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
.flex {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.monospace {
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 8pt;
|
|
||||||
}
|
|
||||||
svg {
|
|
||||||
width: 800px;
|
|
||||||
}
|
|
||||||
.hidden {
|
|
||||||
/* display: none; */
|
|
||||||
/* visibility: hidden; */
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<head>
|
|
||||||
<title>Forum Network</title>
|
|
||||||
<link type="text/css" rel="stylesheet" href="/index.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h2>Tests</h2>
|
|
||||||
<h3>Primary</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="/tests/validation-pool.html">Validation Pool</a></li>
|
|
||||||
<li><a href="/tests/availability.html">Availability + Business</a></li>
|
|
||||||
<li><a href="/tests/forum.html">Forum</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Secondary</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="/tests/forum-network.html">Forum Network</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Tertiary</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="/tests/basic.html">Basic</a></li>
|
|
||||||
<li><a href="/tests/mermaid.html">Mermaid</a></li>
|
|
||||||
<li><a href="/tests/graph.html">Graph</a></li>
|
|
||||||
<li><a href="/tests/debounce.html">Debounce</a></li>
|
|
||||||
<li><a href="/tests/flowchart.html">Flowchart</a></li>
|
|
||||||
</ul>
|
|
||||||
</body>
|
|
|
@ -1,28 +0,0 @@
|
||||||
const params = {
|
|
||||||
/* Validation Pool parameters */
|
|
||||||
mintingRatio: () => 1, // c1
|
|
||||||
// NOTE: c2 overlaps with c3 and adds excess complexity, so we omit it for now
|
|
||||||
stakeForAuthor: 0.5, // c3
|
|
||||||
winningRatio: 0.5, // c4
|
|
||||||
quorum: 0, // c5
|
|
||||||
activeVoterThreshold: null, // c6
|
|
||||||
voteDuration: {
|
|
||||||
// c7
|
|
||||||
min: 0,
|
|
||||||
max: null,
|
|
||||||
},
|
|
||||||
// NOTE: c8 is the token loss ratio, which is specified as a runtime argument
|
|
||||||
contentiousDebate: {
|
|
||||||
period: 5000, // c9
|
|
||||||
stages: 3, // c10
|
|
||||||
},
|
|
||||||
lockingTimeExponent: 0, // c11
|
|
||||||
|
|
||||||
/* Forum parameters */
|
|
||||||
initialPostValue: () => 1, // q1
|
|
||||||
revaluationLimit: 1, // q2
|
|
||||||
referenceChainLimit: 3, // q3
|
|
||||||
leachingValue: 1, // q4
|
|
||||||
};
|
|
||||||
|
|
||||||
export default params;
|
|
|
@ -1,189 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<head>
|
|
||||||
<title>Availability test</title>
|
|
||||||
<link type="text/css" rel="stylesheet" href="/index.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="availability-test"></div>
|
|
||||||
</body>
|
|
||||||
<script type="module">
|
|
||||||
import { Box } from '../classes/box.js';
|
|
||||||
import { Scene } from '../classes/scene.js';
|
|
||||||
import { Expert } from '../classes/expert.js';
|
|
||||||
import { Bench } from '../classes/bench.js';
|
|
||||||
import { Business } from '../classes/business.js';
|
|
||||||
import { Availability } from '../classes/availability.js';
|
|
||||||
import { delay } from '../util.js';
|
|
||||||
import { Forum } from '../classes/forum.js';
|
|
||||||
import { Public } from '../classes/public.js';
|
|
||||||
import { PostContent } from '../classes/post-content.js';
|
|
||||||
|
|
||||||
const DELAY_INTERVAL = 500;
|
|
||||||
|
|
||||||
const rootElement = document.getElementById('availability-test');
|
|
||||||
const rootBox = new Box('rootBox', rootElement).flex();
|
|
||||||
|
|
||||||
const scene = (window.scene = new Scene('Availability test', rootBox));
|
|
||||||
scene.withSequenceDiagram();
|
|
||||||
scene.withFlowchart();
|
|
||||||
|
|
||||||
const experts = (window.experts = []);
|
|
||||||
const newExpert = async () => {
|
|
||||||
const index = experts.length;
|
|
||||||
const name = `Expert${index + 1}`;
|
|
||||||
const expert = await new Expert(name, scene).initialize();
|
|
||||||
experts.push(expert);
|
|
||||||
return expert;
|
|
||||||
};
|
|
||||||
|
|
||||||
const expert1 = await newExpert();
|
|
||||||
const expert2 = await newExpert();
|
|
||||||
const forum = (window.forum = new Forum('Forum', scene));
|
|
||||||
const bench = (window.bench = new Bench(forum, 'Bench', scene));
|
|
||||||
const availability = (window.availability = new Availability(
|
|
||||||
bench,
|
|
||||||
'Availability',
|
|
||||||
scene,
|
|
||||||
));
|
|
||||||
const business = (window.business = new Business(
|
|
||||||
bench,
|
|
||||||
forum,
|
|
||||||
availability,
|
|
||||||
'Business',
|
|
||||||
scene,
|
|
||||||
));
|
|
||||||
const requestor = new Public('Public', scene);
|
|
||||||
|
|
||||||
const updateDisplayValues = async () => {
|
|
||||||
for (const expert of experts) {
|
|
||||||
await expert.setValue(
|
|
||||||
'rep',
|
|
||||||
bench.reputation.valueOwnedBy(expert.reputationPublicKey),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await bench.setValue('total rep', bench.reputation.getTotal());
|
|
||||||
await scene.sequence.render();
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateDisplayValuesAndDelay = async (delayMs = DELAY_INTERVAL) => {
|
|
||||||
await updateDisplayValues();
|
|
||||||
await delay(delayMs);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getActiveWorker = async () => {
|
|
||||||
let worker;
|
|
||||||
let request;
|
|
||||||
for (const expert of experts) {
|
|
||||||
request = await expert.getAssignedWork(availability, business);
|
|
||||||
if (request) {
|
|
||||||
worker = expert;
|
|
||||||
await worker.actions.getAssignedWork.log(worker, availability);
|
|
||||||
worker.activate();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { worker, request };
|
|
||||||
};
|
|
||||||
|
|
||||||
const voteForWorkEvidence = async (worker, pool) => {
|
|
||||||
for (const expert of experts) {
|
|
||||||
if (expert !== worker) {
|
|
||||||
await expert.stake(pool, {
|
|
||||||
position: true,
|
|
||||||
amount: 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await updateDisplayValuesAndDelay();
|
|
||||||
|
|
||||||
// Experts gain initial reputation by submitting a post with fee
|
|
||||||
const { postId: postId1, pool: pool1 } = await expert1.submitPostWithFee(
|
|
||||||
bench,
|
|
||||||
forum,
|
|
||||||
new PostContent({ hello: 'there' }).setTitle('Post 1'),
|
|
||||||
{
|
|
||||||
fee: 10,
|
|
||||||
duration: 1000,
|
|
||||||
tokenLossRatio: 1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await updateDisplayValuesAndDelay(1000);
|
|
||||||
|
|
||||||
await pool1.evaluateWinningConditions();
|
|
||||||
await updateDisplayValuesAndDelay();
|
|
||||||
|
|
||||||
const { pool: pool2 } = await expert2.submitPostWithFee(
|
|
||||||
bench,
|
|
||||||
forum,
|
|
||||||
new PostContent({ hello: 'to you as well' })
|
|
||||||
.setTitle('Post 2')
|
|
||||||
.addCitation(postId1, 0.5),
|
|
||||||
{
|
|
||||||
fee: 10,
|
|
||||||
duration: 1000,
|
|
||||||
tokenLossRatio: 1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await updateDisplayValuesAndDelay(1000);
|
|
||||||
|
|
||||||
await pool2.evaluateWinningConditions();
|
|
||||||
await updateDisplayValuesAndDelay();
|
|
||||||
|
|
||||||
// Populate availability pool
|
|
||||||
await expert1.registerAvailability(availability, 1, 10000);
|
|
||||||
await expert2.registerAvailability(availability, 1, 10000);
|
|
||||||
await updateDisplayValuesAndDelay();
|
|
||||||
|
|
||||||
// Submit work request
|
|
||||||
await requestor.submitRequest(
|
|
||||||
business,
|
|
||||||
{ fee: 100 },
|
|
||||||
{ please: 'do some work' },
|
|
||||||
);
|
|
||||||
await updateDisplayValuesAndDelay();
|
|
||||||
|
|
||||||
// Receive work request
|
|
||||||
const { worker, request } = await getActiveWorker();
|
|
||||||
|
|
||||||
// Submit work evidence
|
|
||||||
const pool = await worker.submitWork(
|
|
||||||
business,
|
|
||||||
request.id,
|
|
||||||
{
|
|
||||||
here: 'is some evidence of work product',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tokenLossRatio: 1,
|
|
||||||
duration: 1000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
worker.deactivate();
|
|
||||||
await updateDisplayValuesAndDelay();
|
|
||||||
|
|
||||||
// Stake on work evidence
|
|
||||||
await voteForWorkEvidence(worker, pool);
|
|
||||||
await updateDisplayValuesAndDelay();
|
|
||||||
|
|
||||||
// Wait for validation pool duration to elapse
|
|
||||||
await delay(1000);
|
|
||||||
|
|
||||||
// Distribute reputation awards and fees
|
|
||||||
await pool.evaluateWinningConditions();
|
|
||||||
await updateDisplayValuesAndDelay();
|
|
||||||
|
|
||||||
// This should throw an exception since the pool is already resolved
|
|
||||||
try {
|
|
||||||
await pool.evaluateWinningConditions();
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message.match(/Validation pool has already been resolved/)) {
|
|
||||||
console.log(
|
|
||||||
'Caught expected error: Validation pool has already been resolved',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error('Unexpected error');
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,203 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<head>
|
|
||||||
<title>Forum Network</title>
|
|
||||||
<link type="text/css" rel="stylesheet" href="/index.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="basic"></div>
|
|
||||||
</body>
|
|
||||||
<script type="module">
|
|
||||||
import { Box } from '../classes/box.js';
|
|
||||||
import { Scene } from '../classes/scene.js';
|
|
||||||
|
|
||||||
const rootElement = document.getElementById('basic');
|
|
||||||
const rootBox = new Box('rootBox', rootElement).flex();
|
|
||||||
|
|
||||||
function randomDelay(min, max) {
|
|
||||||
const delayMs = min + Math.random() * max;
|
|
||||||
return delayMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
function delay(min, max = min) {
|
|
||||||
const delayMs = min + Math.random() * (max - min);
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(resolve, delayMs);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (true) {
|
|
||||||
const scene = new Scene('Scene 1', rootBox);
|
|
||||||
const webClientStatus = scene.addDisplayValue('WebClient Status');
|
|
||||||
const node1Status = scene.addDisplayValue('Node 1 Status');
|
|
||||||
const blockchainStatus = scene.addDisplayValue('Blockchain Status');
|
|
||||||
|
|
||||||
const webClient = scene.addActor('web client');
|
|
||||||
const node1 = scene.addActor('node 1');
|
|
||||||
const blockchain = scene.addActor('blockchain');
|
|
||||||
const requestForumPage = scene.addAction('requestForumPage');
|
|
||||||
const readBlockchainData = scene.addAction('readBlockchainData');
|
|
||||||
const blockchainData = scene.addAction('blockchainData');
|
|
||||||
const forumPage = scene.addAction('forumPage');
|
|
||||||
|
|
||||||
webClientStatus.set('Initialized');
|
|
||||||
node1Status.set('Idle');
|
|
||||||
blockchainStatus.set('Idle');
|
|
||||||
|
|
||||||
node1.on(requestForumPage, (src, detail) => {
|
|
||||||
node1Status.set('Processing request');
|
|
||||||
node1.on(blockchainData, (_src, data) => {
|
|
||||||
node1Status.set('Processing response');
|
|
||||||
setTimeout(() => {
|
|
||||||
node1.send(src, forumPage, data);
|
|
||||||
node1Status.set('Idle');
|
|
||||||
}, randomDelay(500, 1000));
|
|
||||||
});
|
|
||||||
setTimeout(() => {
|
|
||||||
node1.send(blockchain, readBlockchainData, detail);
|
|
||||||
}, randomDelay(500, 1500));
|
|
||||||
});
|
|
||||||
|
|
||||||
blockchain.on(readBlockchainData, (src, _detail) => {
|
|
||||||
blockchainStatus.set('Processing request');
|
|
||||||
setTimeout(() => {
|
|
||||||
blockchain.send(src, blockchainData, {});
|
|
||||||
blockchainStatus.set('Idle');
|
|
||||||
}, randomDelay(500, 1500));
|
|
||||||
});
|
|
||||||
|
|
||||||
webClient.on(forumPage, (_src, _detail) => {
|
|
||||||
webClientStatus.set('Received forum page');
|
|
||||||
});
|
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
webClient.send(node1, requestForumPage);
|
|
||||||
webClientStatus.set('Requested forum page');
|
|
||||||
}, randomDelay(6000, 12000));
|
|
||||||
}
|
|
||||||
|
|
||||||
(async function run() {
|
|
||||||
const scene = new Scene('Scene 2', rootBox);
|
|
||||||
|
|
||||||
const webClient = scene.addActor('webClient');
|
|
||||||
|
|
||||||
const nodes = [];
|
|
||||||
const memories = [];
|
|
||||||
const storages = [];
|
|
||||||
|
|
||||||
function addNode() {
|
|
||||||
const idx = nodes.length;
|
|
||||||
const node = scene.addActor(`node${idx}`);
|
|
||||||
const memory = scene.addActor(`memory${idx}`);
|
|
||||||
const storage = scene.addActor(`storage${idx}`);
|
|
||||||
node.memory = memory;
|
|
||||||
node.storage = storage;
|
|
||||||
nodes.push(node);
|
|
||||||
memories.push(memory);
|
|
||||||
storages.push(storage);
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPeer(node) {
|
|
||||||
const peers = nodes.filter((peer) => peer !== node);
|
|
||||||
const idx = Math.floor(Math.random() * peers.length);
|
|
||||||
return peers[idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
addNode();
|
|
||||||
addNode();
|
|
||||||
|
|
||||||
const [
|
|
||||||
seekTruth,
|
|
||||||
considerInfo,
|
|
||||||
evaluateConfidence,
|
|
||||||
chooseResponse,
|
|
||||||
qualifiedOpinions,
|
|
||||||
requestMemoryData,
|
|
||||||
memoryData,
|
|
||||||
requestStorageData,
|
|
||||||
storageData,
|
|
||||||
] = [
|
|
||||||
'seek truth',
|
|
||||||
'consider available information',
|
|
||||||
'evaluate confidence',
|
|
||||||
'choose response',
|
|
||||||
'qualified opinions',
|
|
||||||
'request in-memory data',
|
|
||||||
'in-memory data',
|
|
||||||
'request storage data',
|
|
||||||
'storage data',
|
|
||||||
].map((name) => scene.addAction(name));
|
|
||||||
|
|
||||||
memories.forEach((memory) => {
|
|
||||||
memory.setStatus('Idle');
|
|
||||||
memory.on(requestMemoryData, async (src, _detail) => {
|
|
||||||
memory.setStatus('Retrieving data');
|
|
||||||
await delay(1000);
|
|
||||||
memory.send(src, memoryData, {});
|
|
||||||
memory.setStatus('Idle');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
storages.forEach((storage) => {
|
|
||||||
storage.setStatus('Idle');
|
|
||||||
storage.on(requestStorageData, async (src, _detail) => {
|
|
||||||
storage.setStatus('Retrieving data');
|
|
||||||
await delay(1000);
|
|
||||||
storage.send(src, storageData, {});
|
|
||||||
storage.setStatus('Idle');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
nodes.forEach((node) => {
|
|
||||||
node.setStatus('Idle');
|
|
||||||
node.on(seekTruth, async (seeker, detail) => {
|
|
||||||
node.setStatus('Processing request');
|
|
||||||
|
|
||||||
node.on(chooseResponse, async (_src, _info) => {
|
|
||||||
node.setStatus('Choosing response');
|
|
||||||
await delay(1000);
|
|
||||||
node.send(seeker, qualifiedOpinions, {});
|
|
||||||
node.setStatus('Idle');
|
|
||||||
});
|
|
||||||
|
|
||||||
node.on(evaluateConfidence, async (_src, _info) => {
|
|
||||||
node.setStatus('Evaluating confidence');
|
|
||||||
await delay(1000);
|
|
||||||
node.send(node, chooseResponse);
|
|
||||||
});
|
|
||||||
|
|
||||||
node.on(considerInfo, async (_src, _info) => {
|
|
||||||
node.setStatus('Considering info');
|
|
||||||
await delay(1000);
|
|
||||||
node.send(node, evaluateConfidence);
|
|
||||||
});
|
|
||||||
|
|
||||||
node.on(memoryData, (_src, _data) => {
|
|
||||||
node.on(storageData, (__src, __data) => {
|
|
||||||
if (detail?.readConcern === 'single') {
|
|
||||||
node.send(node, considerInfo, {});
|
|
||||||
} else {
|
|
||||||
const peer = getPeer(node);
|
|
||||||
node.on(qualifiedOpinions, (___src, info) => {
|
|
||||||
node.send(node, considerInfo, info);
|
|
||||||
});
|
|
||||||
node.send(peer, seekTruth, { readConcern: 'single' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
node.send(node.storage, requestStorageData);
|
|
||||||
});
|
|
||||||
|
|
||||||
await delay(1000);
|
|
||||||
node.send(node.memory, requestMemoryData);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
webClient.on(qualifiedOpinions, (_src, _detail) => {
|
|
||||||
webClient.setStatus('Received opinions and qualifications');
|
|
||||||
});
|
|
||||||
|
|
||||||
await delay(1000);
|
|
||||||
webClient.setStatus('Seek truth');
|
|
||||||
webClient.send(nodes[0], seekTruth);
|
|
||||||
}());
|
|
||||||
</script>
|
|
|
@ -1,33 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<head>
|
|
||||||
<title>Debounce test</title>
|
|
||||||
<link type="text/css" rel="stylesheet" href="/index.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="debounce-test"></div>
|
|
||||||
</body>
|
|
||||||
<script type="module">
|
|
||||||
import { Box } from '../classes/box.js';
|
|
||||||
import { Scene } from '../classes/scene.js';
|
|
||||||
import { debounce, delay } from '../util.js';
|
|
||||||
|
|
||||||
const rootElement = document.getElementById('debounce-test');
|
|
||||||
const rootBox = new Box('rootBox', rootElement).flex();
|
|
||||||
|
|
||||||
window.scene = new Scene('Debounce test', rootBox);
|
|
||||||
|
|
||||||
let eventCount = 0;
|
|
||||||
const event = () => {
|
|
||||||
eventCount++;
|
|
||||||
console.log(`event ${eventCount}`);
|
|
||||||
};
|
|
||||||
await debounce(event, 500);
|
|
||||||
await debounce(event, 500);
|
|
||||||
await delay(500);
|
|
||||||
await debounce(event, 500);
|
|
||||||
await debounce(event, 500);
|
|
||||||
if (eventCount !== 2) {
|
|
||||||
throw new Error(`Expected 2 events, got ${eventCount}`);
|
|
||||||
}
|
|
||||||
console.log(`eventCount: ${eventCount}`);
|
|
||||||
</script>
|
|
|
@ -1,38 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<head>
|
|
||||||
<title>Flowchart test</title>
|
|
||||||
<link type="text/css" rel="stylesheet" href="/index.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="flowchart-test"></div>
|
|
||||||
</body>
|
|
||||||
<script type="module">
|
|
||||||
import { Box } from '../classes/box.js';
|
|
||||||
import { Scene } from '../classes/scene.js';
|
|
||||||
import { Actor } from '../classes/actor.js';
|
|
||||||
import { Action } from '../classes/action.js';
|
|
||||||
import { delay } from '../util.js';
|
|
||||||
|
|
||||||
const DEFAULT_DELAY_INTERVAL = 500;
|
|
||||||
|
|
||||||
const rootElement = document.getElementById('flowchart-test');
|
|
||||||
const rootBox = new Box('rootBox', rootElement).flex();
|
|
||||||
|
|
||||||
const scene = (window.scene = new Scene('Flowchart test', rootBox));
|
|
||||||
scene.withSequenceDiagram();
|
|
||||||
|
|
||||||
const actor1 = new Actor('A', scene);
|
|
||||||
const actor2 = new Actor('B', scene);
|
|
||||||
const action1 = new Action('Action 1', scene);
|
|
||||||
await action1.log(actor1, actor2);
|
|
||||||
await actor1.setValue('value', 1);
|
|
||||||
|
|
||||||
await scene.withFlowchart();
|
|
||||||
await scene.flowchart.log('A --> B');
|
|
||||||
|
|
||||||
await delay(DEFAULT_DELAY_INTERVAL);
|
|
||||||
action1.log(actor1, actor2);
|
|
||||||
|
|
||||||
await delay(DEFAULT_DELAY_INTERVAL);
|
|
||||||
await scene.flowchart.log('A --> C');
|
|
||||||
</script>
|
|
|
@ -1,73 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<head>
|
|
||||||
<title>Forum Network test</title>
|
|
||||||
<link type="text/css" rel="stylesheet" href="/index.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="forum-network"></div>
|
|
||||||
</body>
|
|
||||||
<script type="module">
|
|
||||||
import { Box } from '../classes/box.js';
|
|
||||||
import { Scene } from '../classes/scene.js';
|
|
||||||
import { PostContent } from '../classes/post-content.js';
|
|
||||||
import { Expert } from '../classes/expert.js';
|
|
||||||
import { ForumNode } from '../classes/forum-node.js';
|
|
||||||
import { ForumNetwork } from '../classes/forum-network.js';
|
|
||||||
import { CryptoUtil } from '../classes/crypto.js';
|
|
||||||
import { delay } from '../util.js';
|
|
||||||
|
|
||||||
const rootElement = document.getElementById('forum-network');
|
|
||||||
const rootBox = new Box('rootBox', rootElement).flex();
|
|
||||||
|
|
||||||
window.scene = new Scene('Forum Network test', rootBox).log(
|
|
||||||
'sequenceDiagram',
|
|
||||||
);
|
|
||||||
|
|
||||||
window.author1 = await new Expert('author1', window.scene).initialize();
|
|
||||||
window.author2 = await new Expert('author2', window.scene).initialize();
|
|
||||||
|
|
||||||
window.forumNetwork = new ForumNetwork();
|
|
||||||
|
|
||||||
window.forumNode1 = await new ForumNode('node1', window.scene).initialize(
|
|
||||||
window.forumNetwork,
|
|
||||||
);
|
|
||||||
window.forumNode2 = await new ForumNode('node2', window.scene).initialize(
|
|
||||||
window.forumNetwork,
|
|
||||||
);
|
|
||||||
window.forumNode3 = await new ForumNode('node3', window.scene).initialize(
|
|
||||||
window.forumNetwork,
|
|
||||||
);
|
|
||||||
|
|
||||||
const processInterval = setInterval(async () => {
|
|
||||||
await window.forumNode1.processNextMessage();
|
|
||||||
await window.forumNode2.processNextMessage();
|
|
||||||
await window.forumNode3.processNextMessage();
|
|
||||||
|
|
||||||
await window.scene.sequence.render();
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
// const blockchain = new Blockchain();
|
|
||||||
|
|
||||||
window.post1 = new PostContent({ message: 'hi' });
|
|
||||||
window.post1.id = CryptoUtil.randomUUID();
|
|
||||||
window.post2 = new PostContent({ message: 'hello' }).addCitation(
|
|
||||||
window.post1.id,
|
|
||||||
1.0,
|
|
||||||
);
|
|
||||||
|
|
||||||
await delay(1000);
|
|
||||||
await window.author1.submitPostViaNetwork(
|
|
||||||
window.forumNode1,
|
|
||||||
window.post1,
|
|
||||||
50,
|
|
||||||
);
|
|
||||||
await delay(1000);
|
|
||||||
await window.author2.submitPostViaNetwork(
|
|
||||||
window.forumNode2,
|
|
||||||
window.post2,
|
|
||||||
100,
|
|
||||||
);
|
|
||||||
|
|
||||||
await delay(1000);
|
|
||||||
clearInterval(processInterval);
|
|
||||||
</script>
|
|
|
@ -1,134 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<head>
|
|
||||||
<title>Forum test</title>
|
|
||||||
<link type="text/css" rel="stylesheet" href="/index.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="forum-test"></div>
|
|
||||||
</body>
|
|
||||||
<script type="module">
|
|
||||||
import { Box } from '../classes/box.js';
|
|
||||||
import { Scene } from '../classes/scene.js';
|
|
||||||
import { Expert } from '../classes/expert.js';
|
|
||||||
import { Bench } from '../classes/bench.js';
|
|
||||||
import { delay } from '../util.js';
|
|
||||||
import { Forum } from '../classes/forum.js';
|
|
||||||
import { PostContent } from '../classes/post-content.js';
|
|
||||||
import params from '../params.js';
|
|
||||||
|
|
||||||
const DEFAULT_DELAY_INTERVAL = 500;
|
|
||||||
|
|
||||||
const rootElement = document.getElementById('forum-test');
|
|
||||||
const rootBox = new Box('rootBox', rootElement).flex();
|
|
||||||
|
|
||||||
const scene = (window.scene = new Scene('Forum test', rootBox));
|
|
||||||
scene.withSequenceDiagram();
|
|
||||||
scene.withFlowchart();
|
|
||||||
|
|
||||||
scene.addDisplayValue('c3. stakeForAuthor').set(params.stakeForAuthor);
|
|
||||||
scene.addDisplayValue('q2. revaluationLimit').set(params.revaluationLimit);
|
|
||||||
scene
|
|
||||||
.addDisplayValue('q3. referenceChainLimit')
|
|
||||||
.set(params.referenceChainLimit);
|
|
||||||
scene.addDisplayValue('q4. leachingValue').set(params.leachingValue);
|
|
||||||
scene.addDisplayValue(' ');
|
|
||||||
|
|
||||||
const experts = (window.experts = []);
|
|
||||||
const newExpert = async () => {
|
|
||||||
const index = experts.length;
|
|
||||||
const name = `Expert${index + 1}`;
|
|
||||||
const expert = await new Expert(name, scene).initialize();
|
|
||||||
experts.push(expert);
|
|
||||||
return expert;
|
|
||||||
};
|
|
||||||
|
|
||||||
const forum = (window.forum = new Forum('Forum', scene));
|
|
||||||
const bench = (window.bench = new Bench(forum, 'Bench', scene));
|
|
||||||
const expert1 = await newExpert();
|
|
||||||
const expert2 = await newExpert();
|
|
||||||
const expert3 = await newExpert();
|
|
||||||
|
|
||||||
const updateDisplayValues = async () => {
|
|
||||||
for (const expert of experts) {
|
|
||||||
await expert.setValue(
|
|
||||||
'rep',
|
|
||||||
bench.reputation.valueOwnedBy(expert.reputationPublicKey),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await bench.setValue('total rep', bench.reputation.getTotal());
|
|
||||||
await forum.setValue('total value', forum.getTotalValue());
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateDisplayValuesAndDelay = async (delayMs) => {
|
|
||||||
await updateDisplayValues();
|
|
||||||
await delay(delayMs ?? DEFAULT_DELAY_INTERVAL);
|
|
||||||
};
|
|
||||||
|
|
||||||
await updateDisplayValuesAndDelay();
|
|
||||||
|
|
||||||
await scene.startSection();
|
|
||||||
|
|
||||||
const { postId: postId1, pool: pool1 } = await expert1.submitPostWithFee(
|
|
||||||
bench,
|
|
||||||
forum,
|
|
||||||
new PostContent({ hello: 'there' }).setTitle('Post 1'),
|
|
||||||
{
|
|
||||||
fee: 10,
|
|
||||||
duration: 1000,
|
|
||||||
tokenLossRatio: 1,
|
|
||||||
// authorStakeAmount: 10,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await updateDisplayValuesAndDelay(1000);
|
|
||||||
|
|
||||||
await pool1.evaluateWinningConditions();
|
|
||||||
await updateDisplayValuesAndDelay();
|
|
||||||
|
|
||||||
await scene.endSection();
|
|
||||||
await scene.startSection();
|
|
||||||
|
|
||||||
const { postId: postId2, pool: pool2 } = await expert2.submitPostWithFee(
|
|
||||||
bench,
|
|
||||||
forum,
|
|
||||||
new PostContent({ hello: 'to you as well' })
|
|
||||||
.setTitle('Post 2')
|
|
||||||
.addCitation(postId1, 0.5),
|
|
||||||
{
|
|
||||||
fee: 10,
|
|
||||||
duration: 1000,
|
|
||||||
tokenLossRatio: 1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await updateDisplayValuesAndDelay(1000);
|
|
||||||
|
|
||||||
// await expert1.stake(pool2, { position: true, amount 1});
|
|
||||||
// await updateDisplayValuesAndDelay();
|
|
||||||
|
|
||||||
await pool2.evaluateWinningConditions();
|
|
||||||
await updateDisplayValuesAndDelay();
|
|
||||||
|
|
||||||
await scene.endSection();
|
|
||||||
await scene.startSection();
|
|
||||||
|
|
||||||
const { pool: pool3 } = await expert3.submitPostWithFee(
|
|
||||||
bench,
|
|
||||||
forum,
|
|
||||||
new PostContent({ hello: "y'all" })
|
|
||||||
.setTitle('Post 3')
|
|
||||||
.addCitation(postId2, -0.5),
|
|
||||||
{
|
|
||||||
fee: 100,
|
|
||||||
duration: 1000,
|
|
||||||
tokenLossRatio: 1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await updateDisplayValuesAndDelay(1000);
|
|
||||||
|
|
||||||
// await expert1.stake(pool3, { position: true, amount 1});
|
|
||||||
// await updateDisplayValuesAndDelay();
|
|
||||||
|
|
||||||
await pool3.evaluateWinningConditions();
|
|
||||||
await updateDisplayValuesAndDelay();
|
|
||||||
|
|
||||||
await scene.endSection();
|
|
||||||
</script>
|
|
|
@ -1,33 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<head>
|
|
||||||
<title>Forum Graph</title>
|
|
||||||
<link type="text/css" rel="stylesheet" href="/index.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="graph-test"></div>
|
|
||||||
</body>
|
|
||||||
<script type="module">
|
|
||||||
import { Box } from '../classes/box.js';
|
|
||||||
import { Scene } from '../classes/scene.js';
|
|
||||||
import { Graph } from '../classes/graph.js';
|
|
||||||
|
|
||||||
const rootElement = document.getElementById('graph-test');
|
|
||||||
const rootBox = new Box('rootBox', rootElement).flex();
|
|
||||||
|
|
||||||
window.scene = new Scene('Graph test', rootBox);
|
|
||||||
|
|
||||||
window.graph = new Graph();
|
|
||||||
|
|
||||||
window.v = [];
|
|
||||||
function addVertex() {
|
|
||||||
const vertex = window.graph.addVertex({ seq: window.v.length });
|
|
||||||
window.v.push(vertex);
|
|
||||||
}
|
|
||||||
addVertex();
|
|
||||||
addVertex();
|
|
||||||
addVertex();
|
|
||||||
addVertex();
|
|
||||||
addVertex();
|
|
||||||
|
|
||||||
window.graph.addEdge('e1', 0, 1);
|
|
||||||
</script>
|
|
|
@ -1,32 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<head>
|
|
||||||
<title>Reputation test</title>
|
|
||||||
<link type="text/css" rel="stylesheet" href="/index.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="test"></div>
|
|
||||||
</body>
|
|
||||||
<script type="module">
|
|
||||||
import { Box } from '../classes/box.js';
|
|
||||||
import { Scene } from '../classes/scene.js';
|
|
||||||
import { ValidationPool } from '../classes/validation-pool.js';
|
|
||||||
import { TokenHolder } from '../classes/token-holder.js';
|
|
||||||
import { ReputationToken } from '../classes/reputation-token.js';
|
|
||||||
import { delay } from '../util.js';
|
|
||||||
|
|
||||||
const DEFAULT_DELAY_INTERVAL = 500;
|
|
||||||
|
|
||||||
const rootElement = document.getElementById('forum-test');
|
|
||||||
const rootBox = new Box('rootBox', rootElement).flex();
|
|
||||||
|
|
||||||
const scene = (window.scene = new Scene('Forum test', rootBox));
|
|
||||||
scene.withSequenceDiagram();
|
|
||||||
scene.withFlowchart();
|
|
||||||
|
|
||||||
const pool = new ValidationPool();
|
|
||||||
const repToken = new ReputationToken();
|
|
||||||
|
|
||||||
// const tokenMinter = new TokenHolder('TokenMinter', scene);
|
|
||||||
|
|
||||||
await delay(DEFAULT_DELAY_INTERVAL);
|
|
||||||
</script>
|
|
|
@ -1,125 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<head>
|
|
||||||
<title>Validation Pool test</title>
|
|
||||||
<link type="text/css" rel="stylesheet" href="/index.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="validation-pool"></div>
|
|
||||||
</body>
|
|
||||||
<script type="module">
|
|
||||||
import { Box } from '../classes/box.js';
|
|
||||||
import { Scene } from '../classes/scene.js';
|
|
||||||
import { Expert } from '../classes/expert.js';
|
|
||||||
import { Bench } from '../classes/bench.js';
|
|
||||||
import { Forum } from '../classes/forum.js';
|
|
||||||
import { PostContent } from '../classes/post-content.js';
|
|
||||||
import { delay } from '../util.js';
|
|
||||||
|
|
||||||
const rootElement = document.getElementById('validation-pool');
|
|
||||||
const rootBox = new Box('rootBox', rootElement).flex();
|
|
||||||
|
|
||||||
const scene = (window.scene = new Scene('Validation Pool test', rootBox));
|
|
||||||
await scene.withSequenceDiagram();
|
|
||||||
const expert1 = (window.expert1 = await new Expert(
|
|
||||||
'Expert1',
|
|
||||||
scene,
|
|
||||||
).initialize());
|
|
||||||
const expert2 = (window.expert2 = await new Expert(
|
|
||||||
'Expert2',
|
|
||||||
scene,
|
|
||||||
).initialize());
|
|
||||||
const forum = (window.forum = new Forum('Forum', scene));
|
|
||||||
const bench = (window.bench = new Bench(forum, 'Bench', scene));
|
|
||||||
|
|
||||||
const updateDisplayValues = async () => {
|
|
||||||
await expert1.setValue(
|
|
||||||
'rep',
|
|
||||||
bench.reputation.valueOwnedBy(expert1.reputationPublicKey),
|
|
||||||
);
|
|
||||||
await expert2.setValue(
|
|
||||||
'rep',
|
|
||||||
bench.reputation.valueOwnedBy(expert2.reputationPublicKey),
|
|
||||||
);
|
|
||||||
await bench.setValue('total rep', bench.reputation.getTotal());
|
|
||||||
// With params.lockingTimeExponent = 0 and params.activeVoterThreshold = null,
|
|
||||||
// these next 3 propetries are all equal to total rep
|
|
||||||
// await bench.setValue('available rep', bench.reputation.getTotalAvailable());
|
|
||||||
// await bench.setValue('active rep', bench.getActiveReputation());
|
|
||||||
// await bench.setValue('active available rep', bench.getActiveAvailableReputation());
|
|
||||||
await scene.sequence.render();
|
|
||||||
};
|
|
||||||
|
|
||||||
updateDisplayValues();
|
|
||||||
await delay(1000);
|
|
||||||
|
|
||||||
// First expert can self-approve
|
|
||||||
{
|
|
||||||
const { pool } = await expert1.submitPostWithFee(bench, forum, new PostContent(), {
|
|
||||||
fee: 7,
|
|
||||||
duration: 1000,
|
|
||||||
tokenLossRatio: 1,
|
|
||||||
});
|
|
||||||
// Attempting to evaluate winning conditions before the duration has expired
|
|
||||||
// should result in an exception
|
|
||||||
try {
|
|
||||||
await pool.evaluateWinningConditions();
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message.match(/Validation pool duration has not yet elapsed/)) {
|
|
||||||
console.log(
|
|
||||||
'Caught expected error: Validation pool duration has not yet elapsed',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error('Unexpected error');
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await delay(1000);
|
|
||||||
await pool.evaluateWinningConditions(); // Vote passes
|
|
||||||
await updateDisplayValues();
|
|
||||||
await delay(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Failure example: second expert can not self-approve
|
|
||||||
try {
|
|
||||||
const { pool } = await expert2.submitPostWithFee(bench, forum, new PostContent(), {
|
|
||||||
fee: 1,
|
|
||||||
duration: 1000,
|
|
||||||
tokenLossRatio: 1,
|
|
||||||
});
|
|
||||||
await delay(1000);
|
|
||||||
await pool.evaluateWinningConditions(); // Quorum not met!
|
|
||||||
await updateDisplayValues();
|
|
||||||
await delay(1000);
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message.match(/Quorum is not met/)) {
|
|
||||||
console.log('Caught expected error: Quorum not met');
|
|
||||||
} else {
|
|
||||||
console.error('Unexpected error');
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second expert must be approved by first expert
|
|
||||||
{
|
|
||||||
const { pool } = await expert2.submitPostWithFee(bench, forum, new PostContent(), {
|
|
||||||
fee: 1,
|
|
||||||
duration: 1000,
|
|
||||||
tokenLossRatio: 1,
|
|
||||||
});
|
|
||||||
await expert1.stake(pool, {
|
|
||||||
position: true,
|
|
||||||
amount: 4,
|
|
||||||
lockingTime: 0,
|
|
||||||
});
|
|
||||||
await delay(1000);
|
|
||||||
await pool.evaluateWinningConditions(); // Stake passes
|
|
||||||
await updateDisplayValues();
|
|
||||||
await delay(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateDisplayValues();
|
|
||||||
|
|
||||||
scene.deactivateAll();
|
|
||||||
|
|
||||||
await updateDisplayValues();
|
|
||||||
</script>
|
|
|
@ -1,30 +0,0 @@
|
||||||
const timers = new Map();
|
|
||||||
|
|
||||||
export const debounce = async (fn, delayMs) => {
|
|
||||||
const timer = timers.get(fn);
|
|
||||||
if (timer) {
|
|
||||||
return timer.result;
|
|
||||||
}
|
|
||||||
const result = await fn();
|
|
||||||
timers.set(fn, { result });
|
|
||||||
setTimeout(() => {
|
|
||||||
timers.delete(fn);
|
|
||||||
}, delayMs);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const delay = async (delayMs) => {
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
setTimeout(resolve, delayMs);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hexToRGB = (input) => {
|
|
||||||
if (input.startsWith('#')) {
|
|
||||||
input = input.slice(1);
|
|
||||||
}
|
|
||||||
const r = parseInt(`${input[0]}${input[1]}`, 16);
|
|
||||||
const g = parseInt(`${input[2]}${input[3]}`, 16);
|
|
||||||
const b = parseInt(`${input[4]}${input[5]}`, 16);
|
|
||||||
return { r, g, b };
|
|
||||||
};
|
|
|
@ -0,0 +1 @@
|
||||||
|
SEMANTIC_SCHOLAR_API_KEY=
|
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
.env
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "semantic-scholar-client"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
default-run = "import"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-recursion = "1.0.0"
|
||||||
|
clap = { version = "3.2.11", features = ["derive"] }
|
||||||
|
dotenv = "0.15.0"
|
||||||
|
mongodb = "2.2.2"
|
||||||
|
reqwest = { version = "0.11.11", features = ["json"] }
|
||||||
|
serde = { version = "1.0.139", features = ["derive"] }
|
||||||
|
serde_json = "1.0.82"
|
||||||
|
tokio = { version = "1.20.0", features = ["full"] }
|
|
@ -0,0 +1,25 @@
|
||||||
|
#`semantic-scholar-client`
|
||||||
|
|
||||||
|
This utility is able to fetch data from Semantic Scholar API.
|
||||||
|
|
||||||
|
Initial proof of concept here writes the result to stdout.
|
||||||
|
|
||||||
|
Work in progress to pipe this data into an operating database.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
* (Optional) Copy `.env.example` to `.env` and set the value of `SEMANTIC_SCHOLAR_API_KEY`
|
||||||
|
* Run the program
|
||||||
|
|
||||||
|
cargo run -- --paper-id <paper_id> --depth <depth>
|
||||||
|
|
||||||
|
* `paper_id` values are in accordance with [Semantic Scholar API](https://api.semanticscholar.org/api-docs/).
|
||||||
|
* `depth` is the number of citations to traverse, from the starting paper.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
Ideas for followup work:
|
||||||
|
- Consider strategies for deciding where to terminate a given traversal
|
||||||
|
- Provide an HTTP/WebSocket interface that can be used to talk to this process during its operation.
|
||||||
|
This can enable us to pipe the data to other tasks, to monitor, to start/stop, and even to make configuration changes.
|
||||||
|
- Rate limit requests
|
|
@ -0,0 +1,153 @@
|
||||||
|
// During development, allowing dead code
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use async_recursion::async_recursion;
|
||||||
|
use clap::Parser;
|
||||||
|
use dotenv::dotenv;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::cmp::min;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
type DataResult<T> = Result<T, Box<dyn Error>>;
|
||||||
|
|
||||||
|
const BASE_URL: &str = "https://api.semanticscholar.org/graph/v1";
|
||||||
|
const MAX_DEPTH: u32 = 3;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[clap(author, version, about, long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
/// How deep to traverse citation graph from the starting paper
|
||||||
|
#[clap(short, long, value_parser)]
|
||||||
|
depth: u32,
|
||||||
|
|
||||||
|
/// Starting paper. We will traverse papers that cite this one
|
||||||
|
#[clap(short, long, value_parser)]
|
||||||
|
paper_id: String,
|
||||||
|
// Write the results to MongoDB
|
||||||
|
// #[clap(short, long, value_parser)]
|
||||||
|
// write_to_mongo: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Author {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Authors = Vec<Author>;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Paper {
|
||||||
|
paper_id: String,
|
||||||
|
title: Option<String>,
|
||||||
|
citations: Vec<Citation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs within Citation struct
|
||||||
|
*/
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct CitingPaper {
|
||||||
|
paper_id: Option<String>,
|
||||||
|
title: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Citation {
|
||||||
|
citing_paper: CitingPaper,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
code: Option<String>,
|
||||||
|
* Generic struct to wrap the common API response pattern {data: [...]}
|
||||||
|
*/
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct ApiListResponse<T> {
|
||||||
|
data: Option<Vec<T>>,
|
||||||
|
message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Cache results in a (separate but local) database such as Redis
|
||||||
|
// TODO: Store results in a (separate but local) database such as Postgres
|
||||||
|
#[async_recursion]
|
||||||
|
async fn get_citations(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
paper_id: String,
|
||||||
|
depth: u32,
|
||||||
|
authors: &mut Vec<Author>,
|
||||||
|
) -> DataResult<Vec<Citation>> {
|
||||||
|
// Bound recursion to some depth
|
||||||
|
if depth > MAX_DEPTH {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the URL
|
||||||
|
let mut url = String::new();
|
||||||
|
write!(&mut url, "{}/paper/{}/citations", BASE_URL, paper_id)?;
|
||||||
|
|
||||||
|
let mut req = client.get(url);
|
||||||
|
let api_key = std::env::var("SEMANTIC_SCHOLAR_API_KEY");
|
||||||
|
if api_key.is_ok() {
|
||||||
|
req = req.header("x-api-key", api_key.unwrap());
|
||||||
|
}
|
||||||
|
let resp = req.send().await?.text().await?;
|
||||||
|
|
||||||
|
let resp_deserialized_attempt =
|
||||||
|
serde_json::from_str::<ApiListResponse<Citation>>(resp.as_str());
|
||||||
|
|
||||||
|
if let Err(err) = resp_deserialized_attempt {
|
||||||
|
println!("depth {} paper {} error {}", depth, paper_id, err);
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp_deserialized: ApiListResponse<Citation> = resp_deserialized_attempt.unwrap();
|
||||||
|
|
||||||
|
if resp_deserialized.message.is_some() {
|
||||||
|
println!(
|
||||||
|
"depth {} paper {} error {}",
|
||||||
|
depth,
|
||||||
|
paper_id,
|
||||||
|
resp_deserialized.message.unwrap()
|
||||||
|
);
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for Citation {
|
||||||
|
citing_paper:
|
||||||
|
CitingPaper {
|
||||||
|
paper_id: citing_paper_id,
|
||||||
|
title,
|
||||||
|
},
|
||||||
|
} in resp_deserialized.data.unwrap()
|
||||||
|
{
|
||||||
|
if let (Some(citing_paper_id), Some(title)) = (citing_paper_id, title) {
|
||||||
|
let short_len = min(50, title.len());
|
||||||
|
let (short_title, _) = title.split_at(short_len);
|
||||||
|
println!(
|
||||||
|
"depth {} paper {} cites {} title {}",
|
||||||
|
depth, citing_paper_id, paper_id, short_title
|
||||||
|
);
|
||||||
|
|
||||||
|
get_citations(&client, citing_paper_id, depth + 1, authors).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let Args { depth, paper_id } = Args::parse();
|
||||||
|
|
||||||
|
dotenv().ok();
|
||||||
|
|
||||||
|
let mut authors = Authors::new();
|
||||||
|
|
||||||
|
let client: reqwest::Client = reqwest::Client::new();
|
||||||
|
|
||||||
|
get_citations(&client, paper_id, depth, &mut authors).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
|
||||||
|
use mongodb::{Client, options::ClientOptions};
|
||||||
|
|
||||||
|
const MONGO_DB_ADDRESS: &str = "mongodb://docker:mongopw@localhost:55000";
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Parse a connection string into an options struct.
|
||||||
|
let client_options = ClientOptions::parse(MONGO_DB_ADDRESS).await?;
|
||||||
|
|
||||||
|
// Get a handle to the deployment.
|
||||||
|
let client = Client::with_options(client_options)?;
|
||||||
|
|
||||||
|
// Try creating a collection
|
||||||
|
{
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct Book {
|
||||||
|
title: String,
|
||||||
|
author: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference a (new) database
|
||||||
|
let db = client.database("db2");
|
||||||
|
|
||||||
|
// Get a handle to a collection of `Book`.
|
||||||
|
let typed_collection = db.collection::<Book>("books");
|
||||||
|
|
||||||
|
let books = vec![
|
||||||
|
Book {
|
||||||
|
title: "The Grapes of Wrath".to_string(),
|
||||||
|
author: "John Steinbeck".to_string(),
|
||||||
|
},
|
||||||
|
Book {
|
||||||
|
title: "To Kill a Mockingbird".to_string(),
|
||||||
|
author: "Harper Lee".to_string(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Insert the books into "mydb.books" collection, no manual conversion to BSON necessary.
|
||||||
|
typed_collection.insert_many(books, None).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// List the names of the databases in that deployment.
|
||||||
|
for db_name in client.list_database_names(None, None).await? {
|
||||||
|
println!("{}", db_name);
|
||||||
|
// Get a handle to a database.
|
||||||
|
let db = client.database(db_name.as_str());
|
||||||
|
|
||||||
|
// List the names of the collections in that database.
|
||||||
|
for collection_name in db.list_collection_names(None).await? {
|
||||||
|
println!("- {}", collection_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue