From 3541802d93dbaa29047d247f5fa1874c95db7518 Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Wed, 1 Feb 2023 21:43:47 -0600 Subject: [PATCH] Refactor to simplify - Add DAO class to contain the various contracts and data - Use window?.scene? instead of passing as arg to every constructor --- forum-network/.eslintrc.js | 1 + forum-network/notes/sequences.md | 5 + forum-network/notes/work.md | 8 +- forum-network/src/classes/action.js | 15 +- forum-network/src/classes/actor.js | 21 +- forum-network/src/classes/availability.js | 10 +- forum-network/src/classes/box.js | 4 +- forum-network/src/classes/business.js | 40 ++-- .../src/classes/{bench.js => dao.js} | 39 ++-- forum-network/src/classes/expert.js | 61 +++--- forum-network/src/classes/forum-node.js | 10 +- forum-network/src/classes/forum.js | 81 ++------ forum-network/src/classes/network-node.js | 6 +- forum-network/src/classes/post.js | 46 +++++ forum-network/src/classes/public.js | 6 +- .../src/classes/reputation-holder.js | 7 +- forum-network/src/classes/reputation-token.js | 5 +- forum-network/src/classes/scene.js | 2 +- forum-network/src/classes/validation-pool.js | 70 +++---- forum-network/src/classes/wdag.js | 13 +- .../src/tests/availability.test.html | 184 +----------------- forum-network/src/tests/basic.test.html | 2 +- forum-network/src/tests/business.test.html | 27 +++ forum-network/src/tests/forum1.test.html | 2 +- forum-network/src/tests/forum2.test.html | 2 +- forum-network/src/tests/forum3.test.html | 2 +- forum-network/src/tests/forum4.test.html | 2 +- .../src/tests/scripts/availability.test.js | 165 ++++++++++++++++ .../src/tests/scripts/business.test.js | 16 ++ .../src/tests/scripts/forum-network.test.js | 5 +- .../tests/scripts/forum/forum.test-util.js | 45 ++--- .../src/tests/scripts/validation-pool.test.js | 24 +-- forum-network/src/tests/scripts/wdag.test.js | 7 +- .../src/tests/validation-pool.test.html | 2 +- forum-network/src/tests/wdag.test.html | 2 +- forum-network/src/util.js | 4 + 36 files changed, 493 insertions(+), 448 deletions(-) create mode 100644 forum-network/notes/sequences.md rename forum-network/src/classes/{bench.js => dao.js} (64%) create mode 100644 forum-network/src/classes/post.js create mode 100644 forum-network/src/tests/business.test.html create mode 100644 forum-network/src/tests/scripts/availability.test.js create mode 100644 forum-network/src/tests/scripts/business.test.js diff --git a/forum-network/.eslintrc.js b/forum-network/.eslintrc.js index 1d9fdd3..4601ff3 100644 --- a/forum-network/.eslintrc.js +++ b/forum-network/.eslintrc.js @@ -39,6 +39,7 @@ module.exports = { context: 'readonly', it: 'readonly', specify: 'readonly', + should: 'readonly', before: 'readonly', after: 'readonly', beforeEach: 'readonly', diff --git a/forum-network/notes/sequences.md b/forum-network/notes/sequences.md new file mode 100644 index 0000000..d873d30 --- /dev/null +++ b/forum-network/notes/sequences.md @@ -0,0 +1,5 @@ +expert Expert1 +expert Expert2 +forum Forum + +source -- action --> destination diff --git a/forum-network/notes/work.md b/forum-network/notes/work.md index 46a0fb5..6530075 100644 --- a/forum-network/notes/work.md +++ b/forum-network/notes/work.md @@ -59,14 +59,8 @@ In a messaging system, the work is - Receiving messages - Processing messages - Sending messages - -and may include - - Maintaining context related to incoming messages for the duration of some operation - Performing computations -- Storing data -- Retrieving data -- Performing other actions The work of verifying peers in a messaging system is @@ -76,6 +70,8 @@ The work of verifying peers in a messaging system is - Voting in validation pools? The work of providing a storage service extends that of participating in a messaging system. +- Storing data +- Retrieving data The work of verifying peers work products in a storage network is diff --git a/forum-network/src/classes/action.js b/forum-network/src/classes/action.js index 0d9c5c4..69df2a3 100644 --- a/forum-network/src/classes/action.js +++ b/forum-network/src/classes/action.js @@ -1,16 +1,13 @@ export class Action { - constructor(name, scene) { + constructor(name) { this.name = name; - this.scene = scene; } async log(src, dest, msg, obj, symbol = '->>') { - if (this.scene.sequence) { - await this.scene.sequence.log( - `${src.name} ${symbol} ${dest.name} : ${this.name} ${msg ?? ''} ${ - JSON.stringify(obj) ?? '' - }`, - ); - } + await window?.scene?.sequence?.log( + `${src.name} ${symbol} ${dest.name} : ${this.name} ${msg ?? ''} ${ + JSON.stringify(obj) ?? '' + }`, + ); } } diff --git a/forum-network/src/classes/actor.js b/forum-network/src/classes/actor.js index 609cec4..05b2302 100644 --- a/forum-network/src/classes/actor.js +++ b/forum-network/src/classes/actor.js @@ -1,21 +1,20 @@ import { displayNumber } from '../util.js'; export class Actor { - constructor(name, scene) { + constructor(name) { this.name = name; - this.scene = scene; this.callbacks = new Map(); - this.status = this.scene.addDisplayValue(`${this.name} status`); + this.status = window?.scene?.addDisplayValue(`${this.name} status`); this.status.set('Created'); this.values = new Map(); this.valueFunctions = new Map(); this.active = 0; - this.scene.registerActor(this); + window?.scene?.registerActor(this); } activate() { this.active += 1; - this.scene.sequence.log(`activate ${this.name}`, false); + window?.scene?.sequence.log(`activate ${this.name}`, false); } async deactivate() { @@ -23,7 +22,7 @@ export class Actor { throw new Error(`${this.name} is not active, can not deactivate`); } this.active -= 1; - await this.scene.sequence.log(`deactivate ${this.name}`); + await window?.scene?.sequence.log(`deactivate ${this.name}`); } async send(dest, action, detail) { @@ -36,7 +35,7 @@ export class Actor { 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}`, + `[${window?.scene?.name} actor ${this.name} does not have a callback registered for ${action.name}`, ); } await cb(src, detail); @@ -54,7 +53,7 @@ export class Actor { } addValue(label, fn) { - this.values.set(label, this.scene.addDisplayValue(`${this.name} ${label}`)); + this.values.set(label, window?.scene?.addDisplayValue(`${this.name} ${label}`)); if (fn) { this.valueFunctions.set(label, fn); } @@ -67,11 +66,11 @@ export class Actor { } let displayValue = this.values.get(label); if (!displayValue) { - displayValue = this.scene.addDisplayValue(`${this.name} ${label}`); + displayValue = window?.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}`); + await window?.scene?.sequence.log(`note over ${this.name} : ${label} = ${value}`); } displayValue.set(value); return this; @@ -85,7 +84,7 @@ export class Actor { value = displayNumber(value); } if (value !== displayValue.get()) { - await this.scene.sequence.log(`note over ${this.name} : ${label} = ${value}`); + await window?.scene?.sequence.log(`note over ${this.name} : ${label} = ${value}`); } displayValue.set(value); } diff --git a/forum-network/src/classes/availability.js b/forum-network/src/classes/availability.js index cf34d3d..c2c1b00 100644 --- a/forum-network/src/classes/availability.js +++ b/forum-network/src/classes/availability.js @@ -17,12 +17,12 @@ class Worker { * 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; + constructor(dao, name) { + super(name); + this.dao = dao; this.actions = { - assignWork: new Action('assign work', scene), + assignWork: new Action('assign work'), }; this.workers = new Map(); @@ -30,7 +30,7 @@ export class Availability extends Actor { register(reputationPublicKey, { stakeAmount, tokenId, duration }) { // TODO: Should be signed by token owner - this.bench.reputation.lock(tokenId, stakeAmount, duration); + this.dao.reputation.lock(tokenId, stakeAmount, duration); const workerId = CryptoUtil.randomUUID(); this.workers.set(workerId, new Worker(reputationPublicKey, tokenId, stakeAmount, duration)); return workerId; diff --git a/forum-network/src/classes/box.js b/forum-network/src/classes/box.js index 0588f30..ecc4f77 100644 --- a/forum-network/src/classes/box.js +++ b/forum-network/src/classes/box.js @@ -1,11 +1,11 @@ import { DisplayValue } from './display-value.js'; -import { CryptoUtil } from './crypto.js'; +import { randomID } from '../util.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.id = `box_${randomID()}`; this.el.classList.add('box'); if (name) { this.el.setAttribute('box-name', name); diff --git a/forum-network/src/classes/business.js b/forum-network/src/classes/business.js index 4dd5f42..9ddd799 100644 --- a/forum-network/src/classes/business.js +++ b/forum-network/src/classes/business.js @@ -1,11 +1,15 @@ +import { randomID } from '../util.js'; import { Action } from './action.js'; import { Actor } from './actor.js'; -import { CryptoUtil } from './crypto.js'; import { PostContent } from './post-content.js'; class Request { + static nextSeq = 0; + constructor(fee, content) { - this.id = CryptoUtil.randomUUID(); + this.seq = this.nextSeq; + this.nextSeq += 1; + this.id = `req_${randomID()}`; this.fee = fee; this.content = content; this.worker = null; @@ -16,16 +20,14 @@ class Request { * 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; + constructor(dao, name) { + super(name); + this.dao = dao; this.actions = { - assignWork: new Action('assign work', scene), - submitPost: new Action('submit post', scene), - initiateValidationPool: new Action('initiate validation pool', scene), + assignWork: new Action('assign work'), + submitPost: new Action('submit post'), + initiateValidationPool: new Action('initiate validation pool'), }; this.requests = new Map(); @@ -34,8 +36,8 @@ export class Business extends Actor { 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); + await this.actions.assignWork.log(this, this.dao.availability); + const worker = await this.dao.availability.assignWork(request.id); request.worker = worker; return request.id; } @@ -45,6 +47,10 @@ export class Business extends Actor { return request; } + async getRequests() { + return Array.from(this.requests.values()); + } + async submitWork(reputationPublicKey, requestId, workEvidence, { tokenLossRatio, duration }) { const request = this.requests.get(requestId); if (!request) { @@ -60,16 +66,18 @@ export class Business extends Actor { 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); + await this.actions.submitPost.log(this, this.dao); + const postId = await this.dao.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({ + await this.actions.initiateValidationPool.log(this, this.dao); + const pool = await this.dao.initiateValidationPool({ postId, fee: request.fee, duration, diff --git a/forum-network/src/classes/bench.js b/forum-network/src/classes/dao.js similarity index 64% rename from forum-network/src/classes/bench.js rename to forum-network/src/classes/dao.js index 7354e50..4a8df80 100644 --- a/forum-network/src/classes/bench.js +++ b/forum-network/src/classes/dao.js @@ -1,22 +1,35 @@ -import { Actor } from './actor.js'; -import { ValidationPool } from './validation-pool.js'; -import params from '../params.js'; import { Action } from './action.js'; +import params from '../params.js'; +import { Forum } from './forum.js'; import { ReputationTokenContract } from './reputation-token.js'; +import { ValidationPool } from './validation-pool.js'; +import { Availability } from './availability.js'; +import { Business } from './business.js'; +import { Actor } from './actor.js'; /** - * Purpose: Keep track of reputation holders + * Purpose: + * - Forum: Maintain a directed, acyclic, graph of positively and negatively weighted citations. + * and the value accrued via each post and citation. + * - Reputation: Keep track of reputation accrued to each expert */ -export class Bench extends Actor { - constructor(forum, name, scene) { - super(name, scene); - this.forum = forum; - this.validationPools = new Map(); - this.voters = new Map(); +export class DAO extends Actor { + constructor(name) { + super(name); + + /* Contracts */ + this.forum = new Forum(this, `${name} Forum`); + this.availability = new Availability(this, `${name} Availability`); + this.business = new Business(this, `${name} Business`); this.reputation = new ReputationTokenContract(); + /* Data */ + this.validationPools = new Map(); + this.experts = new Map(); + this.actions = { - createValidationPool: new Action('create validation pool', scene), + addPost: new Action('add post'), + createValidationPool: new Action('create validation pool'), }; } @@ -25,7 +38,7 @@ export class Bench extends Actor { } listActiveVoters() { - return Array.from(this.voters.values()).filter((voter) => { + return Array.from(this.experts.values()).filter((voter) => { const hasVoted = !!voter.dateLastVote; const withinThreshold = !params.activeVoterThreshold || new Date() - voter.dateLastVote >= params.activeVoterThreshold; @@ -48,7 +61,7 @@ export class Bench extends Actor { 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); + const pool = new ValidationPool(this, poolOptions, name); this.validationPools.set(pool.id, pool); await this.actions.createValidationPool.log(this, pool); pool.activate(); diff --git a/forum-network/src/classes/expert.js b/forum-network/src/classes/expert.js index a30f692..04f80a9 100644 --- a/forum-network/src/classes/expert.js +++ b/forum-network/src/classes/expert.js @@ -4,16 +4,17 @@ import { CryptoUtil } from './crypto.js'; import { ReputationHolder } from './reputation-holder.js'; export class Expert extends ReputationHolder { - constructor(name, scene) { - super(undefined, name, scene); + constructor(dao, name) { + super(name); + this.dao = dao; 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), + submitPostViaNetwork: new Action('submit post via network'), + submitPost: new Action('submit post'), + initiateValidationPool: new Action('initiate validation pool'), + stake: new Action('stake on post'), + registerAvailability: new Action('register availability'), + getAssignedWork: new Action('get assigned work'), + submitWork: new Action('submit work evidence'), }; this.validationPools = new Map(); this.tokens = []; @@ -36,29 +37,29 @@ export class Expert extends ReputationHolder { await forumNode.receiveMessage(JSON.stringify(postMessage.toJSON())); } - async submitPost(forum, postContent) { + async submitPost(postContent) { // TODO: Include fee - await this.actions.submitPost.log(this, forum); - return forum.addPost(this.reputationPublicKey, postContent); + await this.actions.submitPost.log(this, this.dao.forum); + return this.dao.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 }); + async submitPostWithFee(postContent, poolOptions) { + await this.actions.submitPost.log(this, this.dao.forum); + const postId = await this.dao.forum.addPost(this.reputationPublicKey, postContent); + const pool = await this.initiateValidationPool({ ...poolOptions, postId }); this.tokens.push(pool.tokenId); return { postId, pool }; } - async initiateValidationPool(bench, poolOptions) { + async initiateValidationPool(poolOptions) { // For now, directly call bench.initiateValidationPool(); poolOptions.reputationPublicKey = this.reputationPublicKey; await this.actions.initiateValidationPool.log( this, - bench, + this.dao, `(fee: ${poolOptions.fee}, stake: ${poolOptions.authorStakeAmount ?? 0})`, ); - const pool = await bench.initiateValidationPool(poolOptions); + const pool = await this.dao.initiateValidationPool(poolOptions); this.tokens.push(pool.tokenId); this.validationPools.set(pool.id, poolOptions); return pool; @@ -79,23 +80,27 @@ export class Expert extends ReputationHolder { }); } - async registerAvailability(availability, stakeAmount, duration) { - await this.actions.registerAvailability.log(this, availability, `(stake: ${stakeAmount}, duration: ${duration})`); - this.workerId = await availability.register(this.reputationPublicKey, { + async registerAvailability(stakeAmount, duration) { + await this.actions.registerAvailability.log( + this, + this.dao.availability, + `(stake: ${stakeAmount}, duration: ${duration})`, + ); + this.workerId = await this.dao.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); + async getAssignedWork() { + const requestId = await this.dao.availability.getAssignedWork(this.workerId); + const request = await this.dao.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 }); + async submitWork(requestId, evidence, { tokenLossRatio, duration }) { + await this.actions.submitWork.log(this, this.dao.business); + return this.dao.business.submitWork(this.reputationPublicKey, requestId, evidence, { tokenLossRatio, duration }); } } diff --git a/forum-network/src/classes/forum-node.js b/forum-network/src/classes/forum-node.js index 992ce33..832ad7d 100644 --- a/forum-network/src/classes/forum-node.js +++ b/forum-network/src/classes/forum-node.js @@ -2,17 +2,17 @@ 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 { NetworkNode } from './network-node.js'; +import { randomID } from '../util.js'; export class ForumNode extends NetworkNode { - constructor(name, scene) { - super(name, scene); + constructor(name) { + super(name); this.forumView = new ForumView(); this.actions = { ...this.actions, - storePost: new Action('store post', scene), + storePost: new Action('store post'), }; } @@ -42,7 +42,7 @@ export class ForumNode extends NetworkNode { // Process an incoming post, received by whatever means async processPost(authorId, post) { if (!post.id) { - post.id = CryptoUtil.randomUUID(); + post.id = randomID(); } await this.actions.storePost.log(this, this); // this.forumView.addPost(authorId, post.id, post, stake); diff --git a/forum-network/src/classes/forum.js b/forum-network/src/classes/forum.js index 7ea03b1..ef0b477 100644 --- a/forum-network/src/classes/forum.js +++ b/forum-network/src/classes/forum.js @@ -1,69 +1,28 @@ -import { Actor } from './actor.js'; import { WDAG } from './wdag.js'; import { Action } from './action.js'; -import { CryptoUtil } from './crypto.js'; import params from '../params.js'; import { ReputationHolder } from './reputation-holder.js'; -import { displayNumber, EPSILON } from '../util.js'; +import { EPSILON } from '../util.js'; +import { Post } from './post.js'; const CITATION = 'citation'; const BALANCE = 'balance'; -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 ?? name; - this.authorPublicKey = authorPublicKey; - this.value = 0; - this.initialValue = 0; - this.citations = postContent.citations; - this.title = postContent.title; - const leachingTotal = this.citations - .filter(({ weight }) => weight < 0) - .reduce((total, { weight }) => total += -weight, 0); - const donationTotal = this.citations - .filter(({ weight }) => weight > 0) - .reduce((total, { weight }) => total += weight, 0); - if (leachingTotal > params.revaluationLimit) { - throw new Error('Post leaching total exceeds revaluation limit ' - + `(${leachingTotal} > ${params.revaluationLimit})`); - } - if (donationTotal > params.revaluationLimit) { - throw new Error('Post donation total exceeds revaluation limit ' - + `(${donationTotal} > ${params.revaluationLimit})`); - } - if (this.citations.some(({ weight }) => Math.abs(weight) > params.revaluationLimit)) { - throw new Error(`Each citation magnitude must not exceed revaluation limit ${params.revaluationLimit}`); - } - } - - getLabel() { - return `${this.name} - - - - - - -
initial${displayNumber(this.initialValue)}
value${displayNumber(this.value)}
` - .replaceAll(/\n\s*/g, ''); - } -} - /** - * Purpose: Maintain a directed, acyclic, weighted graph of posts referencing other posts + * Purpose: + * - Forum: Maintain a directed, acyclic, graph of positively and negatively weighted citations. + * and the value accrued via each post and citation. */ export class Forum extends ReputationHolder { - constructor(name, scene) { - super(`forum_${CryptoUtil.randomUUID()}`, name, scene); + constructor(dao, name) { + super(name); + this.dao = dao; this.id = this.reputationPublicKey; - this.posts = new WDAG(scene); + this.posts = new WDAG(); this.actions = { - addPost: new Action('add post', scene), - propagateValue: new Action('propagate', this.scene), - transfer: new Action('transfer', this.scene), + addPost: new Action('add post'), + propagateValue: new Action('propagate'), + transfer: new Action('transfer'), }; } @@ -96,10 +55,10 @@ export class Forum extends ReputationHolder { } async onValidate({ - bench, postId, tokenId, + postId, tokenId, }) { this.activate(); - const initialValue = bench.reputation.valueOf(tokenId); + const initialValue = this.dao.reputation.valueOf(tokenId); const postVertex = this.posts.getVertex(postId); const post = postVertex.data; post.setStatus('Validated'); @@ -121,16 +80,16 @@ export class Forum extends ReputationHolder { // Apply computed rewards to update values of tokens for (const [id, value] of rewardsAccumulator) { if (value < 0) { - bench.reputation.transferValueFrom(id, post.tokenId, -value); + this.dao.reputation.transferValueFrom(id, post.tokenId, -value); } else { - bench.reputation.transferValueFrom(post.tokenId, id, value); + this.dao.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); + // Transfer ownership of the minted/staked token, from the posts to the post author + this.dao.reputation.transferFrom(this.id, post.authorPublicKey, post.tokenId); + const toActor = window?.scene?.findActor((actor) => actor.reputationPublicKey === post.authorPublicKey); + const value = this.dao.reputation.valueOf(post.tokenId); this.actions.transfer.log(this, toActor, `(${value})`); this.deactivate(); } diff --git a/forum-network/src/classes/network-node.js b/forum-network/src/classes/network-node.js index d365c9d..8e96b5f 100644 --- a/forum-network/src/classes/network-node.js +++ b/forum-network/src/classes/network-node.js @@ -4,11 +4,11 @@ import { CryptoUtil } from './crypto.js'; import { PrioritizedQueue } from './prioritized-queue.js'; export class NetworkNode extends Actor { - constructor(name, scene) { - super(name, scene); + constructor(name) { + super(name); this.queue = new PrioritizedQueue(); this.actions = { - peerMessage: new Action('peer message', scene), + peerMessage: new Action('peer message'), }; } diff --git a/forum-network/src/classes/post.js b/forum-network/src/classes/post.js new file mode 100644 index 0000000..ffdfef3 --- /dev/null +++ b/forum-network/src/classes/post.js @@ -0,0 +1,46 @@ +import { Actor } from './actor.js'; +import { displayNumber } from '../util.js'; +import params from '../params.js'; + +export 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 ?? name; + this.authorPublicKey = authorPublicKey; + this.value = 0; + this.initialValue = 0; + this.citations = postContent.citations; + this.title = postContent.title; + const leachingTotal = this.citations + .filter(({ weight }) => weight < 0) + .reduce((total, { weight }) => total += -weight, 0); + const donationTotal = this.citations + .filter(({ weight }) => weight > 0) + .reduce((total, { weight }) => total += weight, 0); + if (leachingTotal > params.revaluationLimit) { + throw new Error('Post leaching total exceeds revaluation limit ' + + `(${leachingTotal} > ${params.revaluationLimit})`); + } + if (donationTotal > params.revaluationLimit) { + throw new Error('Post donation total exceeds revaluation limit ' + + `(${donationTotal} > ${params.revaluationLimit})`); + } + if (this.citations.some(({ weight }) => Math.abs(weight) > params.revaluationLimit)) { + throw new Error(`Each citation magnitude must not exceed revaluation limit ${params.revaluationLimit}`); + } + } + + getLabel() { + return `${this.name} + + + + + + +
initial${displayNumber(this.initialValue)}
value${displayNumber(this.value)}
` + .replaceAll(/\n\s*/g, ''); + } +} diff --git a/forum-network/src/classes/public.js b/forum-network/src/classes/public.js index ad25f22..1eb142a 100644 --- a/forum-network/src/classes/public.js +++ b/forum-network/src/classes/public.js @@ -2,10 +2,10 @@ import { Action } from './action.js'; import { Actor } from './actor.js'; export class Public extends Actor { - constructor(name, scene) { - super(name, scene); + constructor(name) { + super(name); this.actions = { - submitRequest: new Action('submit work request', scene), + submitRequest: new Action('submit work request'), }; } diff --git a/forum-network/src/classes/reputation-holder.js b/forum-network/src/classes/reputation-holder.js index 5bfcc42..7ad195e 100644 --- a/forum-network/src/classes/reputation-holder.js +++ b/forum-network/src/classes/reputation-holder.js @@ -1,8 +1,9 @@ +import { randomID } from '../util.js'; import { Actor } from './actor.js'; export class ReputationHolder extends Actor { - constructor(reputationPublicKey, name, scene) { - super(name, scene); - this.reputationPublicKey = reputationPublicKey; + constructor(name) { + super(name); + this.reputationPublicKey = `${name}_${randomID()}`; } } diff --git a/forum-network/src/classes/reputation-token.js b/forum-network/src/classes/reputation-token.js index 76ab770..a12b4e8 100644 --- a/forum-network/src/classes/reputation-token.js +++ b/forum-network/src/classes/reputation-token.js @@ -1,7 +1,6 @@ import { ERC721 } from './erc721.js'; -import { CryptoUtil } from './crypto.js'; -import { EPSILON } from '../util.js'; +import { EPSILON, randomID } from '../util.js'; class Lock { constructor(tokenId, amount, duration) { @@ -21,7 +20,7 @@ export class ReputationTokenContract extends ERC721 { } mint(to, value, context) { - const tokenId = `token_${CryptoUtil.randomUUID()}`; + const tokenId = `token_${randomID()}`; super.mint(to, tokenId); this.values.set(tokenId, value); this.histories.set(tokenId, [{ increment: value, context }]); diff --git a/forum-network/src/classes/scene.js b/forum-network/src/classes/scene.js index 0b0f082..e487ba1 100644 --- a/forum-network/src/classes/scene.js +++ b/forum-network/src/classes/scene.js @@ -173,7 +173,7 @@ export class Scene { } async addActor(name) { - const actor = new Actor(name, this); + const actor = new Actor(name); if (this.sequence) { await this.sequence.log(`participant ${name}`); } diff --git a/forum-network/src/classes/validation-pool.js b/forum-network/src/classes/validation-pool.js index ac2583d..e344cc9 100644 --- a/forum-network/src/classes/validation-pool.js +++ b/forum-network/src/classes/validation-pool.js @@ -1,4 +1,3 @@ -import { CryptoUtil } from './crypto.js'; import { ReputationHolder } from './reputation-holder.js'; import { Stake } from './stake.js'; import { Voter } from './voter.js'; @@ -16,8 +15,7 @@ const ValidationPoolStates = Object.freeze({ */ export class ValidationPool extends ReputationHolder { constructor( - bench, - forum, + dao, { postId, reputationPublicKey, @@ -27,14 +25,13 @@ export class ValidationPool extends ReputationHolder { contentiousDebate = false, }, name, - scene, ) { - super(`pool_${CryptoUtil.randomUUID()}`, name, scene); + super(name); this.id = this.reputationPublicKey; this.actions = { - reward: new Action('reward', scene), - transfer: new Action('transfer', scene), + reward: new Action('reward'), + transfer: new Action('transfer'), }; // If contentiousDebate = true, we will follow the progression defined by getTokenLossRatio() @@ -59,8 +56,7 @@ export class ValidationPool extends ReputationHolder { }]; got ${duration}`, ); } - this.bench = bench; - this.forum = forum; + this.dao = dao; this.postId = postId; this.state = ValidationPoolStates.OPEN; this.setStatus('Open'); @@ -72,7 +68,7 @@ export class ValidationPool extends ReputationHolder { this.tokenLossRatio = tokenLossRatio; this.contentiousDebate = contentiousDebate; this.mintedValue = fee * params.mintingRatio(); - this.tokenId = this.bench.reputation.mint(this.id, this.mintedValue); + this.tokenId = this.dao.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, { @@ -87,9 +83,9 @@ export class ValidationPool extends ReputationHolder { }); // Keep a record of voters and their votes - const voter = this.bench.voters.get(reputationPublicKey) ?? new Voter(reputationPublicKey); + const voter = this.dao.experts.get(reputationPublicKey) ?? new Voter(reputationPublicKey); voter.addVoteRecord(this); - this.bench.voters.set(reputationPublicKey, voter); + this.dao.experts.set(reputationPublicKey, voter); } getTokenLossRatio() { @@ -158,7 +154,7 @@ export class ValidationPool extends ReputationHolder { ); } - if (reputationPublicKey !== this.bench.reputation.ownerOf(tokenId)) { + if (reputationPublicKey !== this.dao.reputation.ownerOf(tokenId)) { throw new Error('Reputation may only be staked by its owner!'); } @@ -168,18 +164,18 @@ export class ValidationPool extends ReputationHolder { this.stakes.add(stake); // Transfer staked amount from the sender to the validation pool - this.bench.reputation.transferValueFrom(tokenId, this.tokenId, amount); + this.dao.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); + const voter = this.dao.experts.get(reputationPublicKey) ?? new Voter(reputationPublicKey); voter.addVoteRecord(this); - this.bench.voters.set(reputationPublicKey, voter); + this.dao.experts.set(reputationPublicKey, voter); } // Update computed display values - for (const voter of this.bench.voters.values()) { - const actor = this.scene.findActor((a) => a.reputationPublicKey === voter.reputationPublicKey); + for (const voter of this.dao.experts.values()) { + const actor = window?.scene?.findActor((a) => a.reputationPublicKey === voter.reputationPublicKey); await actor.computeValues(); } } @@ -189,7 +185,7 @@ export class ValidationPool extends ReputationHolder { // 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); + this.dao.reputation.lock(tokenId, amount, lockingTime); // TODO: If there is an exception here, the voter may have voted incorrectly. Consider penalties. } } @@ -208,7 +204,7 @@ export class ValidationPool extends ReputationHolder { const upvoteValue = this.getTotalValueOfStakesForOutcome(true); const downvoteValue = this.getTotalValueOfStakesForOutcome(false); - const activeAvailableReputation = this.bench.getActiveAvailableReputation(); + const activeAvailableReputation = this.dao.getActiveAvailableReputation(); const votePasses = upvoteValue >= params.winningRatio * downvoteValue; const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation; @@ -220,13 +216,13 @@ export class ValidationPool extends ReputationHolder { if (quorumMet) { this.setStatus(`Resolved - ${votePasses ? 'Won' : 'Lost'}`); - this.scene.sequence.log(`note over ${this.name} : ${votePasses ? 'Win' : 'Lose'}`); + window?.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`); + window?.scene?.sequence.log(`note over ${this.name} : Quorum not met`); } this.deactivate(); @@ -250,26 +246,25 @@ export class ValidationPool extends ReputationHolder { const value = stake.getStakeValue(); const reward = tokensForWinners * (value / totalValueOfStakesForWin); // Also return each winning voter their staked amount - const reputationPublicKey = this.bench.reputation.ownerOf(tokenId); + const reputationPublicKey = this.dao.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.dao.reputation.transferValueFrom(this.tokenId, tokenId, reward + amount); + const toActor = window?.scene?.findActor((actor) => actor.reputationPublicKey === reputationPublicKey); this.actions.reward.log(this, toActor, `(${reward})`); } - if (votePasses && !!this.forum) { + if (votePasses) { // 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)}`); + console.log(`sending reward for author stake to forum: ${this.dao.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})`); + this.dao.reputation.transferFrom(this.id, this.dao.forum.id, this.tokenId); + const value = this.dao.reputation.valueOf(this.tokenId); + this.actions.transfer.log(this, this.dao.forum, `(${value})`); // Recurse through forum to determine reputation effects - await this.forum.onValidate({ - bench: this.bench, + await this.dao.forum.onValidate({ pool: this, postId: this.postId, tokenId: this.tokenId, @@ -279,15 +274,12 @@ export class ValidationPool extends ReputationHolder { console.log('pool complete'); // Update computed display values - for (const voter of this.bench.voters.values()) { - const actor = this.scene.findActor((a) => a.reputationPublicKey === voter.reputationPublicKey); + for (const voter of this.dao.experts.values()) { + const actor = window?.scene?.findActor((a) => a.reputationPublicKey === voter.reputationPublicKey); await actor.computeValues(); } - await this.bench.computeValues(); - if (this.forum) { - await this.forum.computeValues(); - } + await this.dao.forum.computeValues(); - this.scene.stateToTable(`validation pool ${this.name} complete`); + window?.scene?.stateToTable(`validation pool ${this.name} complete`); } } diff --git a/forum-network/src/classes/wdag.js b/forum-network/src/classes/wdag.js index 06e3dc0..28871b8 100644 --- a/forum-network/src/classes/wdag.js +++ b/forum-network/src/classes/wdag.js @@ -25,17 +25,16 @@ export class Edge { } export class WDAG { - constructor(scene) { - this.scene = scene; + constructor() { this.vertices = new Map(); this.edgeLabels = new Map(); this.nextVertexId = 0; - this.flowchart = scene?.flowchart ?? null; + this.flowchart = window?.scene?.flowchart ?? null; } withFlowchart() { - this.scene.withAdditionalFlowchart(); - this.flowchart = this.scene.lastFlowchart(); + window?.scene?.withAdditionalFlowchart(); + this.flowchart = window?.scene?.lastFlowchart(); return this; } @@ -71,7 +70,7 @@ export class WDAG { } static getEdgeKey({ from, to }) { - return btoa([from.id, to.id]).replaceAll(/[^A-Z]+/g, ''); + return btoa([from.id, to.id]).replaceAll(/[^A-Za-z0-9]+/g, ''); } getEdge(label, from, to) { @@ -122,7 +121,7 @@ export class WDAG { from = from instanceof Vertex ? from : this.getVertex(from); to = to instanceof Vertex ? to : this.getVertex(to); if (this.getEdge(label, from, to)) { - throw new Error(`Edge ${label} from ${from} to ${to} already exists`); + throw new Error(`Edge ${label} from ${from.id} to ${to.id} already exists`); } const edge = this.setEdgeWeight(label, from, to, weight); from.edges.from.push(edge); diff --git a/forum-network/src/tests/availability.test.html b/forum-network/src/tests/availability.test.html index ac9ffcb..8c4f609 100644 --- a/forum-network/src/tests/availability.test.html +++ b/forum-network/src/tests/availability.test.html @@ -4,187 +4,7 @@ -
+
- diff --git a/forum-network/src/tests/basic.test.html b/forum-network/src/tests/basic.test.html index 8bd95c1..47380f4 100644 --- a/forum-network/src/tests/basic.test.html +++ b/forum-network/src/tests/basic.test.html @@ -26,7 +26,7 @@ } if (true) { - const scene = new Scene('Scene 1', rootBox).withSequenceDiagram(); + const scene = (window.scene = new Scene('Scene 1', rootBox).withSequenceDiagram()); const webClientStatus = scene.addDisplayValue('WebClient Status'); const node1Status = scene.addDisplayValue('Node 1 Status'); const blockchainStatus = scene.addDisplayValue('Blockchain Status'); diff --git a/forum-network/src/tests/business.test.html b/forum-network/src/tests/business.test.html new file mode 100644 index 0000000..c46ff0f --- /dev/null +++ b/forum-network/src/tests/business.test.html @@ -0,0 +1,27 @@ + + + Business + + + + + +
+
+ + + + + + + diff --git a/forum-network/src/tests/forum1.test.html b/forum-network/src/tests/forum1.test.html index 1de1bba..5ddcaf8 100644 --- a/forum-network/src/tests/forum1.test.html +++ b/forum-network/src/tests/forum1.test.html @@ -16,7 +16,7 @@