Compare commits
No commits in common. "main" and "forum-network" have entirely different histories.
main
...
forum-netw
|
@ -0,0 +1,7 @@
|
|||
pages:
|
||||
script:
|
||||
- mkdir public
|
||||
- cp -r forum-network/src/* public/
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
|
@ -1,7 +1,7 @@
|
|||
# DAO Governance Framework
|
||||
# Science Publishing DAO
|
||||
|
||||
## Subprojects
|
||||
|
||||
| Name | Description |
|
||||
| --- | --- |
|
||||
| [semantic-scholar-client](./semantic-scholar-client) | Rust utility for reading data from the [Semantic Scholar API](https://api.semanticscholar.org/api-docs) |
|
||||
| [forum-network](./forum-network) | Javascript prototyping forum architecture |
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
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'],
|
||||
},
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
ssl/
|
||||
node_modules/
|
||||
git/
|
|
@ -0,0 +1,30 @@
|
|||
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?
|
|
@ -0,0 +1,99 @@
|
|||
# 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.
|
|
@ -0,0 +1,50 @@
|
|||
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
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
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) : ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* 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) {}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
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 });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
export class Token {
|
||||
constructor(ownerPublicKey) {
|
||||
this.ownerPublicKey = ownerPublicKey;
|
||||
}
|
||||
|
||||
transfer(newOwnerPublicKey) {
|
||||
// TODO: Current owner must sign this request
|
||||
this.ownerPublicKey = newOwnerPublicKey;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
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,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
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);
|
||||
};
|
|
@ -0,0 +1,54 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { Actor } from './actor.js';
|
||||
|
||||
export class ReputationHolder extends Actor {
|
||||
constructor(reputationPublicKey, name, scene) {
|
||||
super(name, scene);
|
||||
this.reputationPublicKey = reputationPublicKey;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
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');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,270 @@
|
|||
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');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
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.
After Width: | Height: | Size: 3.5 KiB |
|
@ -0,0 +1,38 @@
|
|||
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; */
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<!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>
|
|
@ -0,0 +1,28 @@
|
|||
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;
|
|
@ -0,0 +1,189 @@
|
|||
<!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>
|
|
@ -0,0 +1,203 @@
|
|||
<!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>
|
|
@ -0,0 +1,33 @@
|
|||
<!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>
|
|
@ -0,0 +1,38 @@
|
|||
<!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>
|
|
@ -0,0 +1,73 @@
|
|||
<!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>
|
|
@ -0,0 +1,134 @@
|
|||
<!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>
|
|
@ -0,0 +1,33 @@
|
|||
<!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>
|
|
@ -0,0 +1,32 @@
|
|||
<!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>
|
|
@ -0,0 +1,125 @@
|
|||
<!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>
|
|
@ -0,0 +1,30 @@
|
|||
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 };
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
SEMANTIC_SCHOLAR_API_KEY=
|
|
@ -1,2 +0,0 @@
|
|||
/target
|
||||
.env
|
File diff suppressed because it is too large
Load Diff
|
@ -1,17 +0,0 @@
|
|||
[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"] }
|
|
@ -1,25 +0,0 @@
|
|||
#`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
|
|
@ -1,153 +0,0 @@
|
|||
// 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(())
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
|
||||
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