From fc27cda81d87c542509cf2f5dd19b1713f8b89c6 Mon Sep 17 00:00:00 2001
From: Ladd Hoffman
Date: Sat, 4 Feb 2023 22:32:47 -0600
Subject: [PATCH] Reorganization
---
forum-network/.eslintrc.js | 22 +-
forum-network/notes/dao.md | 1 +
.../src/classes/{ => actors}/availability.js | 12 +-
.../src/classes/{ => actors}/business.js | 18 +-
forum-network/src/classes/{ => actors}/dao.js | 25 +-
.../src/classes/{ => actors}/expert.js | 24 +-
.../src/classes/{ => actors}/forum.js | 33 ++-
.../src/classes/{ => actors}/post.js | 6 +-
.../src/classes/{ => actors}/public.js | 10 +-
.../src/classes/actors/reputation-holder.js | 9 +
.../classes/{ => actors}/validation-pool.js | 60 +++--
.../src/classes/{ => contracts}/erc20.js | 0
.../src/classes/{ => contracts}/erc721.js | 0
.../{ => contracts}/reputation-token.js | 2 +-
.../src/classes/{ => display}/action.js | 5 +-
.../src/classes/{ => display}/actor.js | 45 ++--
.../src/classes/{ => display}/box.js | 2 +-
.../classes/{ => display}/display-value.js | 4 +-
forum-network/src/classes/display/mermaid.js | 65 +++++
forum-network/src/classes/display/scene.js | 118 +++++++++
forum-network/src/classes/display/sequence.js | 74 ++++++
forum-network/src/classes/display/table.js | 45 ++++
.../classes/{ => forum-network}/forum-node.js | 10 +-
.../classes/{ => forum-network}/forum-view.js | 2 +-
.../classes/{ => forum-network}/message.js | 4 +-
.../{ => forum-network}/network-node.js | 14 +-
.../classes/{ => forum-network}/network.js | 0
.../classes/{ => ideas}/block-consensus.js | 0
.../src/classes/{ => ideas}/exchange.js | 0
.../src/classes/{ => ideas}/finance.js | 0
.../src/classes/{ => ideas}/question.js | 0
.../src/classes/{ => ideas}/storage.js | 0
forum-network/src/classes/list.js | 9 -
.../src/classes/reputation-holder.js | 9 -
forum-network/src/classes/reputation.js | 88 -------
forum-network/src/classes/scene.js | 239 ------------------
.../src/classes/{ => supporting}/stake.js | 2 +-
.../src/classes/{ => supporting}/vm.js | 16 +-
.../src/classes/{ => supporting}/voter.js | 0
.../src/classes/{ => supporting}/wdag.js | 9 +-
.../src/classes/{ => util}/crypto.js | 0
.../src/classes/{ => util}/post-content.js | 0
.../classes/{ => util}/prioritized-queue.js | 0
forum-network/src/index.html | 5 +-
forum-network/src/tests/all.test.html | 2 +-
.../src/tests/availability.test.html | 26 +-
forum-network/src/tests/basic.test.html | 195 +-------------
forum-network/src/tests/basic2.test.html | 13 +
forum-network/src/tests/debounce.test.html | 50 ++--
.../src/tests/forum-network.test.html | 22 +-
.../src/tests/scripts/availability.test.js | 222 ++++++++--------
forum-network/src/tests/scripts/basic.test.js | 67 +++++
.../src/tests/scripts/basic2.test.js | 139 ++++++++++
.../src/tests/scripts/business.test.js | 6 +-
.../src/tests/scripts/debounce.test.js | 72 ++++++
.../src/tests/scripts/forum-network.test.js | 112 ++++----
.../tests/scripts/forum/forum.test-util.js | 45 ++--
.../src/tests/scripts/forum/forum4.test.js | 1 -
.../src/tests/scripts/validation-pool.test.js | 34 +--
forum-network/src/tests/scripts/vm.test.js | 64 +++--
forum-network/src/tests/scripts/wdag.test.js | 6 +-
.../src/tests/validation-pool.test.html | 8 +-
forum-network/src/util.js | 6 +-
63 files changed, 1135 insertions(+), 942 deletions(-)
create mode 100644 forum-network/notes/dao.md
rename forum-network/src/classes/{ => actors}/availability.js (88%)
rename forum-network/src/classes/{ => actors}/business.js (86%)
rename forum-network/src/classes/{ => actors}/dao.js (75%)
rename forum-network/src/classes/{ => actors}/expert.js (85%)
rename forum-network/src/classes/{ => actors}/forum.js (86%)
rename forum-network/src/classes/{ => actors}/post.js (92%)
rename forum-network/src/classes/{ => actors}/public.js (54%)
create mode 100644 forum-network/src/classes/actors/reputation-holder.js
rename forum-network/src/classes/{ => actors}/validation-pool.js (87%)
rename forum-network/src/classes/{ => contracts}/erc20.js (100%)
rename forum-network/src/classes/{ => contracts}/erc721.js (100%)
rename forum-network/src/classes/{ => contracts}/reputation-token.js (98%)
rename forum-network/src/classes/{ => display}/action.js (71%)
rename forum-network/src/classes/{ => display}/actor.js (54%)
rename forum-network/src/classes/{ => display}/box.js (96%)
rename forum-network/src/classes/{ => display}/display-value.js (75%)
create mode 100644 forum-network/src/classes/display/mermaid.js
create mode 100644 forum-network/src/classes/display/scene.js
create mode 100644 forum-network/src/classes/display/sequence.js
create mode 100644 forum-network/src/classes/display/table.js
rename forum-network/src/classes/{ => forum-network}/forum-node.js (91%)
rename forum-network/src/classes/{ => forum-network}/forum-view.js (97%)
rename forum-network/src/classes/{ => forum-network}/message.js (92%)
rename forum-network/src/classes/{ => forum-network}/network-node.js (83%)
rename forum-network/src/classes/{ => forum-network}/network.js (100%)
rename forum-network/src/classes/{ => ideas}/block-consensus.js (100%)
rename forum-network/src/classes/{ => ideas}/exchange.js (100%)
rename forum-network/src/classes/{ => ideas}/finance.js (100%)
rename forum-network/src/classes/{ => ideas}/question.js (100%)
rename forum-network/src/classes/{ => ideas}/storage.js (100%)
delete mode 100644 forum-network/src/classes/list.js
delete mode 100644 forum-network/src/classes/reputation-holder.js
delete mode 100644 forum-network/src/classes/reputation.js
delete mode 100644 forum-network/src/classes/scene.js
rename forum-network/src/classes/{ => supporting}/stake.js (89%)
rename forum-network/src/classes/{ => supporting}/vm.js (71%)
rename forum-network/src/classes/{ => supporting}/voter.js (100%)
rename forum-network/src/classes/{ => supporting}/wdag.js (95%)
rename forum-network/src/classes/{ => util}/crypto.js (100%)
rename forum-network/src/classes/{ => util}/post-content.js (100%)
rename forum-network/src/classes/{ => util}/prioritized-queue.js (100%)
create mode 100644 forum-network/src/tests/basic2.test.html
create mode 100644 forum-network/src/tests/scripts/basic.test.js
create mode 100644 forum-network/src/tests/scripts/basic2.test.js
create mode 100644 forum-network/src/tests/scripts/debounce.test.js
diff --git a/forum-network/.eslintrc.js b/forum-network/.eslintrc.js
index 4601ff3..3a4f0e3 100644
--- a/forum-network/.eslintrc.js
+++ b/forum-network/.eslintrc.js
@@ -2,9 +2,17 @@ module.exports = {
env: {
browser: true,
es2021: true,
+ mocha: true,
},
extends: ['airbnb-base'],
- overrides: [],
+ overrides: [
+ {
+ files: ['*.test.js'],
+ rules: {
+ 'no-unused-expressions': 'off',
+ },
+ },
+ ],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
@@ -33,16 +41,8 @@ module.exports = {
globals: {
_: 'readonly',
chai: 'readonly',
- expect: 'readonly',
- mocha: 'readonly',
- describe: 'readonly',
- context: 'readonly',
- it: 'readonly',
- specify: 'readonly',
+ sinon: 'readonly',
+ sinonChai: 'readonly',
should: 'readonly',
- before: 'readonly',
- after: 'readonly',
- beforeEach: 'readonly',
- afterEach: 'readonly',
},
};
diff --git a/forum-network/notes/dao.md b/forum-network/notes/dao.md
new file mode 100644
index 0000000..48a026e
--- /dev/null
+++ b/forum-network/notes/dao.md
@@ -0,0 +1 @@
+A DAO is a group of cooperating entities
diff --git a/forum-network/src/classes/availability.js b/forum-network/src/classes/actors/availability.js
similarity index 88%
rename from forum-network/src/classes/availability.js
rename to forum-network/src/classes/actors/availability.js
index c2c1b00..47d936d 100644
--- a/forum-network/src/classes/availability.js
+++ b/forum-network/src/classes/actors/availability.js
@@ -1,6 +1,6 @@
-import { Action } from './action.js';
-import { Actor } from './actor.js';
-import { CryptoUtil } from './crypto.js';
+import { Action } from '../display/action.js';
+import { Actor } from '../display/actor.js';
+import { CryptoUtil } from '../util/crypto.js';
class Worker {
constructor(reputationPublicKey, tokenId, stakeAmount, duration) {
@@ -17,12 +17,12 @@ class Worker {
* Purpose: Enable staking reputation to enter the pool of workers
*/
export class Availability extends Actor {
- constructor(dao, name) {
- super(name);
+ constructor(dao, name, scene) {
+ super(name, scene);
this.dao = dao;
this.actions = {
- assignWork: new Action('assign work'),
+ assignWork: new Action('assign work', scene),
};
this.workers = new Map();
diff --git a/forum-network/src/classes/business.js b/forum-network/src/classes/actors/business.js
similarity index 86%
rename from forum-network/src/classes/business.js
rename to forum-network/src/classes/actors/business.js
index 23ff6e3..67a2705 100644
--- a/forum-network/src/classes/business.js
+++ b/forum-network/src/classes/actors/business.js
@@ -1,7 +1,7 @@
-import { randomID } from '../util.js';
-import { Action } from './action.js';
-import { Actor } from './actor.js';
-import { PostContent } from './post-content.js';
+import { randomID } from '../../util.js';
+import { Action } from '../display/action.js';
+import { Actor } from '../display/actor.js';
+import { PostContent } from '../util/post-content.js';
class Request {
static nextSeq = 0;
@@ -20,14 +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(dao, name) {
- super(name);
+ constructor(dao, name, scene) {
+ super(name, scene);
this.dao = dao;
this.actions = {
- assignWork: new Action('assign work'),
- submitPost: new Action('submit post'),
- initiateValidationPool: new Action('initiate validation pool'),
+ assignWork: new Action('assign work', scene),
+ submitPost: new Action('submit post', scene),
+ initiateValidationPool: new Action('initiate validation pool', scene),
};
this.requests = new Map();
diff --git a/forum-network/src/classes/dao.js b/forum-network/src/classes/actors/dao.js
similarity index 75%
rename from forum-network/src/classes/dao.js
rename to forum-network/src/classes/actors/dao.js
index 59c9535..ee6f6a3 100644
--- a/forum-network/src/classes/dao.js
+++ b/forum-network/src/classes/actors/dao.js
@@ -1,11 +1,10 @@
-import { Action } from './action.js';
-import params from '../params.js';
+import params from '../../params.js';
import { Forum } from './forum.js';
-import { ReputationTokenContract } from './reputation-token.js';
+import { ReputationTokenContract } from '../contracts/reputation-token.js';
import { ValidationPool } from './validation-pool.js';
import { Availability } from './availability.js';
import { Business } from './business.js';
-import { Actor } from './actor.js';
+import { Actor } from '../display/actor.js';
/**
* Purpose:
@@ -14,13 +13,13 @@ import { Actor } from './actor.js';
* - Reputation: Keep track of reputation accrued to each expert
*/
export class DAO extends Actor {
- constructor(name) {
- super(name);
+ constructor(name, scene) {
+ super(name, scene);
/* Contracts */
- this.forum = new Forum(this, 'Forum');
- this.availability = new Availability(this, 'Availability');
- this.business = new Business(this, 'Business');
+ this.forum = new Forum(this, 'Forum', scene);
+ this.availability = new Availability(this, 'Availability', scene);
+ this.business = new Business(this, 'Business', scene);
this.reputation = new ReputationTokenContract();
/* Data */
@@ -59,9 +58,8 @@ export class DAO extends Actor {
async initiateValidationPool(poolOptions, stakeOptions) {
const validationPoolNumber = this.validationPools.size + 1;
const name = `Pool${validationPoolNumber}`;
- const pool = new ValidationPool(this, poolOptions, name);
+ const pool = new ValidationPool(this, poolOptions, name, this.scene);
this.validationPools.set(pool.id, pool);
- pool.activate();
if (stakeOptions) {
const { reputationPublicKey, tokenId, authorStakeAmount } = stakeOptions;
@@ -74,4 +72,9 @@ export class DAO extends Actor {
return pool;
}
+
+ async submitPost(reputationPublicKey, postContent) {
+ const post = await this.forum.addPost(reputationPublicKey, postContent);
+ return post.id;
+ }
}
diff --git a/forum-network/src/classes/expert.js b/forum-network/src/classes/actors/expert.js
similarity index 85%
rename from forum-network/src/classes/expert.js
rename to forum-network/src/classes/actors/expert.js
index 2fff24d..b8ecda0 100644
--- a/forum-network/src/classes/expert.js
+++ b/forum-network/src/classes/actors/expert.js
@@ -1,20 +1,20 @@
-import { Action } from './action.js';
-import { PostMessage } from './message.js';
-import { CryptoUtil } from './crypto.js';
+import { Action } from '../display/action.js';
+import { PostMessage } from '../forum-network/message.js';
+import { CryptoUtil } from '../util/crypto.js';
import { ReputationHolder } from './reputation-holder.js';
export class Expert extends ReputationHolder {
- constructor(dao, name) {
- super(name);
+ constructor(dao, name, scene) {
+ super(name, scene);
this.dao = dao;
this.actions = {
- 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'),
+ 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 = [];
diff --git a/forum-network/src/classes/forum.js b/forum-network/src/classes/actors/forum.js
similarity index 86%
rename from forum-network/src/classes/forum.js
rename to forum-network/src/classes/actors/forum.js
index 9a1c671..a034f84 100644
--- a/forum-network/src/classes/forum.js
+++ b/forum-network/src/classes/actors/forum.js
@@ -1,8 +1,8 @@
-import { WDAG } from './wdag.js';
-import { Action } from './action.js';
-import params from '../params.js';
+import { WDAG } from '../supporting/wdag.js';
+import { Action } from '../display/action.js';
+import params from '../../params.js';
import { ReputationHolder } from './reputation-holder.js';
-import { EPSILON } from '../util.js';
+import { displayNumber, EPSILON } from '../../util.js';
import { Post } from './post.js';
const CITATION = 'citation';
@@ -14,14 +14,16 @@ const BALANCE = 'balance';
* and the value accrued via each post and citation.
*/
export class Forum extends ReputationHolder {
- constructor(dao, name) {
- super(name);
+ constructor(dao, name, scene) {
+
+ super(name, scene);
this.dao = dao;
this.id = this.reputationPublicKey;
this.posts = new WDAG();
this.actions = {
- propagateValue: new Action('propagate'),
- transfer: new Action('transfer'),
+ propagate: new Action('propagate', scene),
+ confirm: new Action('confirm', scene),
+ transfer: new Action('transfer', scene),
};
}
@@ -85,7 +87,7 @@ export class Forum extends ReputationHolder {
// Transfer ownership of the minted/staked token, from the posts to the post author
this.dao.reputation.transfer(this.id, post.authorPublicKey, post.tokenId);
- // const toActor = window?.scene?.findActor((actor) => actor.reputationPublicKey === post.authorPublicKey);
+ // const toActor = this.scene?.findActor((actor) => actor.reputationPublicKey === post.authorPublicKey);
// const value = this.dao.reputation.valueOf(post.tokenId);
}
@@ -98,10 +100,10 @@ export class Forum extends ReputationHolder {
}) {
const postVertex = edge.to;
const post = postVertex?.data;
- this.actions.propagateValue.log(edge.from.data, post, `(${increment})`);
+ this.actions.propagate.log(edge.from.data, post, `(${increment})`);
if (!!params.referenceChainLimit && depth > params.referenceChainLimit) {
- this.actions.propagateValue.log(
+ this.actions.propagate.log(
edge.from.data,
post,
`referenceChainLimit (${params.referenceChainLimit}) reached`,
@@ -141,9 +143,18 @@ export class Forum extends ReputationHolder {
depth: depth + 1,
initialNegative: initialNegative || (depth === 0 && outboundAmount < 0),
});
+
outboundAmount -= refundFromOutbound;
this.posts.setEdgeWeight(BALANCE, citationEdge.from, citationEdge.to, balanceToOutbound + outboundAmount);
totalOutboundAmount += outboundAmount;
+
+ this.actions.confirm.log(
+ citationEdge.to.data,
+ citationEdge.from.data,
+ `(refund: ${displayNumber(refundFromOutbound)}, leach: ${outboundAmount * params.leachingValue})`,
+ undefined,
+ '-->>',
+ );
}
}
return totalOutboundAmount;
diff --git a/forum-network/src/classes/post.js b/forum-network/src/classes/actors/post.js
similarity index 92%
rename from forum-network/src/classes/post.js
rename to forum-network/src/classes/actors/post.js
index ffdfef3..a214c6d 100644
--- a/forum-network/src/classes/post.js
+++ b/forum-network/src/classes/actors/post.js
@@ -1,6 +1,6 @@
-import { Actor } from './actor.js';
-import { displayNumber } from '../util.js';
-import params from '../params.js';
+import { Actor } from '../display/actor.js';
+import { displayNumber } from '../../util.js';
+import params from '../../params.js';
export class Post extends Actor {
constructor(forum, authorPublicKey, postContent) {
diff --git a/forum-network/src/classes/public.js b/forum-network/src/classes/actors/public.js
similarity index 54%
rename from forum-network/src/classes/public.js
rename to forum-network/src/classes/actors/public.js
index 1eb142a..5c49b92 100644
--- a/forum-network/src/classes/public.js
+++ b/forum-network/src/classes/actors/public.js
@@ -1,11 +1,11 @@
-import { Action } from './action.js';
-import { Actor } from './actor.js';
+import { Action } from '../display/action.js';
+import { Actor } from '../display/actor.js';
export class Public extends Actor {
- constructor(name) {
- super(name);
+ constructor(name, scene) {
+ super(name, scene);
this.actions = {
- submitRequest: new Action('submit work request'),
+ submitRequest: new Action('submit work request', scene),
};
}
diff --git a/forum-network/src/classes/actors/reputation-holder.js b/forum-network/src/classes/actors/reputation-holder.js
new file mode 100644
index 0000000..b2aaf0e
--- /dev/null
+++ b/forum-network/src/classes/actors/reputation-holder.js
@@ -0,0 +1,9 @@
+import { randomID } from '../../util.js';
+import { Actor } from '../display/actor.js';
+
+export class ReputationHolder extends Actor {
+ constructor(name, scene) {
+ super(name, scene);
+ this.reputationPublicKey = `${name}_${randomID()}`;
+ }
+}
diff --git a/forum-network/src/classes/validation-pool.js b/forum-network/src/classes/actors/validation-pool.js
similarity index 87%
rename from forum-network/src/classes/validation-pool.js
rename to forum-network/src/classes/actors/validation-pool.js
index 39b1638..5accbf6 100644
--- a/forum-network/src/classes/validation-pool.js
+++ b/forum-network/src/classes/actors/validation-pool.js
@@ -1,8 +1,9 @@
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';
+import { Stake } from '../supporting/stake.js';
+import { Voter } from '../supporting/voter.js';
+import params from '../../params.js';
+import { Action } from '../display/action.js';
+import { displayNumber } from '../../util.js';
const ValidationPoolStates = Object.freeze({
OPEN: 'OPEN',
@@ -25,13 +26,15 @@ export class ValidationPool extends ReputationHolder {
contentiousDebate = false,
},
name,
+ scene,
) {
- super(name);
+ super(name, scene);
this.id = this.reputationPublicKey;
this.actions = {
- reward: new Action('reward'),
- transfer: new Action('transfer'),
+ reward: new Action('reward', scene),
+ transfer: new Action('transfer', scene),
+ mint: new Action('mint', scene),
};
// If contentiousDebate = true, we will follow the progression defined by getTokenLossRatio()
@@ -82,10 +85,14 @@ export class ValidationPool extends ReputationHolder {
tokenId: this.tokenId,
});
+ this.actions.mint.log(this, this, `(${this.mintedValue})`);
+
// Keep a record of voters and their votes
const voter = this.dao.experts.get(reputationPublicKey) ?? new Voter(reputationPublicKey);
voter.addVoteRecord(this);
this.dao.experts.set(reputationPublicKey, voter);
+
+ this.activate();
}
getTokenLossRatio() {
@@ -167,15 +174,13 @@ export class ValidationPool extends ReputationHolder {
this.dao.reputation.transferValueFrom(tokenId, this.tokenId, amount);
// Keep a record of voters and their votes
- if (tokenId !== this.tokenId) {
+ if (reputationPublicKey !== this.id) {
const voter = this.dao.experts.get(reputationPublicKey) ?? new Voter(reputationPublicKey);
voter.addVoteRecord(this);
this.dao.experts.set(reputationPublicKey, voter);
- }
- // Update computed display values
- for (const voter of this.dao.experts.values()) {
- const actor = window?.scene?.findActor((a) => a.reputationPublicKey === voter.reputationPublicKey);
+ // Update computed display values
+ const actor = this.scene?.findActor((a) => a.reputationPublicKey === voter.reputationPublicKey);
await actor.computeValues();
}
}
@@ -216,16 +221,28 @@ export class ValidationPool extends ReputationHolder {
if (quorumMet) {
this.setStatus(`Resolved - ${votePasses ? 'Won' : 'Lost'}`);
- window?.scene?.sequence.log(`note over ${this.name} : ${votePasses ? 'Win' : 'Lose'}`);
+ 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');
- window?.scene?.sequence.log(`note over ${this.name} : Quorum not met`);
+ this.scene?.sequence.log(`note over ${this.name} : Quorum not met`);
}
- this.deactivate();
+ // Update computed display values
+ for (const voter of this.dao.experts.values()) {
+ const actor = this.scene?.findActor((a) => a.reputationPublicKey === voter.reputationPublicKey);
+ if (!actor) {
+ throw new Error('Actor not found!');
+ }
+ await actor.computeValues();
+ }
+ await this.dao.computeValues();
+
+ this.scene?.stateToTable(`validation pool ${this.name} complete`);
+
+ await this.deactivate();
this.state = ValidationPoolStates.RESOLVED;
return result;
}
@@ -249,8 +266,8 @@ export class ValidationPool extends ReputationHolder {
const reputationPublicKey = this.dao.reputation.ownerOf(tokenId);
console.log(`reward for winning stake by ${reputationPublicKey}: ${reward}`);
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})`);
+ const toActor = this.scene?.findActor((actor) => actor.reputationPublicKey === reputationPublicKey);
+ this.actions.reward.log(this, toActor, `(${displayNumber(reward)})`);
}
if (votePasses) {
@@ -272,14 +289,5 @@ export class ValidationPool extends ReputationHolder {
}
console.log('pool complete');
-
- // Update computed display values
- for (const voter of this.dao.experts.values()) {
- const actor = window?.scene?.findActor((a) => a.reputationPublicKey === voter.reputationPublicKey);
- await actor.computeValues();
- }
- await this.dao.computeValues();
-
- window?.scene?.stateToTable(`validation pool ${this.name} complete`);
}
}
diff --git a/forum-network/src/classes/erc20.js b/forum-network/src/classes/contracts/erc20.js
similarity index 100%
rename from forum-network/src/classes/erc20.js
rename to forum-network/src/classes/contracts/erc20.js
diff --git a/forum-network/src/classes/erc721.js b/forum-network/src/classes/contracts/erc721.js
similarity index 100%
rename from forum-network/src/classes/erc721.js
rename to forum-network/src/classes/contracts/erc721.js
diff --git a/forum-network/src/classes/reputation-token.js b/forum-network/src/classes/contracts/reputation-token.js
similarity index 98%
rename from forum-network/src/classes/reputation-token.js
rename to forum-network/src/classes/contracts/reputation-token.js
index a12b4e8..c39af4d 100644
--- a/forum-network/src/classes/reputation-token.js
+++ b/forum-network/src/classes/contracts/reputation-token.js
@@ -1,6 +1,6 @@
import { ERC721 } from './erc721.js';
-import { EPSILON, randomID } from '../util.js';
+import { EPSILON, randomID } from '../../util.js';
class Lock {
constructor(tokenId, amount, duration) {
diff --git a/forum-network/src/classes/action.js b/forum-network/src/classes/display/action.js
similarity index 71%
rename from forum-network/src/classes/action.js
rename to forum-network/src/classes/display/action.js
index 69df2a3..9c1329e 100644
--- a/forum-network/src/classes/action.js
+++ b/forum-network/src/classes/display/action.js
@@ -1,10 +1,11 @@
export class Action {
- constructor(name) {
+ constructor(name, scene) {
this.name = name;
+ this.scene = scene;
}
async log(src, dest, msg, obj, symbol = '->>') {
- await window?.scene?.sequence?.log(
+ await this.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/display/actor.js
similarity index 54%
rename from forum-network/src/classes/actor.js
rename to forum-network/src/classes/display/actor.js
index 05b2302..efdd08c 100644
--- a/forum-network/src/classes/actor.js
+++ b/forum-network/src/classes/display/actor.js
@@ -1,20 +1,22 @@
-import { displayNumber } from '../util.js';
+import { displayNumber } from '../../util.js';
export class Actor {
- constructor(name) {
+ constructor(name, scene) {
+ if (!scene) throw new Error('An actor without a scene!');
this.name = name;
+ this.scene = scene;
this.callbacks = new Map();
- this.status = window?.scene?.addDisplayValue(`${this.name} status`);
+ this.status = scene.addDisplayValue(`${this.name} status`);
this.status.set('Created');
this.values = new Map();
this.valueFunctions = new Map();
this.active = 0;
- window?.scene?.registerActor(this);
+ scene?.registerActor(this);
}
activate() {
this.active += 1;
- window?.scene?.sequence.log(`activate ${this.name}`, false);
+ this.scene?.sequence?.activate(this.name);
}
async deactivate() {
@@ -22,7 +24,7 @@ export class Actor {
throw new Error(`${this.name} is not active, can not deactivate`);
}
this.active -= 1;
- await window?.scene?.sequence.log(`deactivate ${this.name}`);
+ await this.scene?.sequence?.deactivate(this.name);
}
async send(dest, action, detail) {
@@ -35,7 +37,7 @@ export class Actor {
const cb = this.callbacks.get(action.name);
if (!cb) {
throw new Error(
- `[${window?.scene?.name} actor ${this.name} does not have a callback registered for ${action.name}`,
+ `[${this.scene?.name} actor ${this.name} does not have a callback registered for ${action.name}`,
);
}
await cb(src, detail);
@@ -52,41 +54,32 @@ export class Actor {
return this;
}
- addValue(label, fn) {
- this.values.set(label, window?.scene?.addDisplayValue(`${this.name} ${label}`));
+ async addComputedValue(label, fn) {
+ this.values.set(label, this.scene?.addDisplayValue(`${this.name} ${label}`));
if (fn) {
this.valueFunctions.set(label, fn);
+ await this.computeValues();
}
return this;
}
async setValue(label, value) {
- if (typeof value === 'number') {
- value = displayNumber(value);
- }
- let displayValue = this.values.get(label);
- if (!displayValue) {
- displayValue = window?.scene?.addDisplayValue(`${this.name} ${label}`);
- this.values.set(label, displayValue);
+ if (typeof value === 'function') {
+ return this.addComputedValue(label, value);
}
+ const displayValue = this.values.get(label) ?? this.scene?.addDisplayValue(`${this.name} ${label}`);
if (value !== displayValue.get()) {
- await window?.scene?.sequence.log(`note over ${this.name} : ${label} = ${value}`);
+ await this.scene?.sequence?.log(`note over ${this.name} : ${label} = ${displayNumber(value)}`);
}
displayValue.set(value);
+ this.values.set(label, displayValue);
return this;
}
async computeValues() {
for (const [label, fn] of this.valueFunctions.entries()) {
- const displayValue = this.values.get(label);
- let value = fn();
- if (typeof value === 'number') {
- value = displayNumber(value);
- }
- if (value !== displayValue.get()) {
- await window?.scene?.sequence.log(`note over ${this.name} : ${label} = ${value}`);
- }
- displayValue.set(value);
+ const value = fn();
+ await this.setValue(label, value);
}
}
diff --git a/forum-network/src/classes/box.js b/forum-network/src/classes/display/box.js
similarity index 96%
rename from forum-network/src/classes/box.js
rename to forum-network/src/classes/display/box.js
index ecc4f77..faacf17 100644
--- a/forum-network/src/classes/box.js
+++ b/forum-network/src/classes/display/box.js
@@ -1,5 +1,5 @@
import { DisplayValue } from './display-value.js';
-import { randomID } from '../util.js';
+import { randomID } from '../../util.js';
export class Box {
constructor(name, parentEl, elementType = 'div') {
diff --git a/forum-network/src/classes/display-value.js b/forum-network/src/classes/display/display-value.js
similarity index 75%
rename from forum-network/src/classes/display-value.js
rename to forum-network/src/classes/display/display-value.js
index 7f5b48d..e92de70 100644
--- a/forum-network/src/classes/display-value.js
+++ b/forum-network/src/classes/display/display-value.js
@@ -1,3 +1,5 @@
+import { displayNumber } from '../../util.js';
+
export class DisplayValue {
constructor(name, box) {
this.value = undefined;
@@ -9,7 +11,7 @@ export class DisplayValue {
}
render() {
- this.valueBox.setInnerHTML(this.value);
+ this.valueBox.setInnerHTML(typeof this.value === 'number' ? displayNumber(this.value, 6) : this.value);
}
set(value) {
diff --git a/forum-network/src/classes/display/mermaid.js b/forum-network/src/classes/display/mermaid.js
new file mode 100644
index 0000000..9d42760
--- /dev/null
+++ b/forum-network/src/classes/display/mermaid.js
@@ -0,0 +1,65 @@
+import mermaid from 'https://unpkg.com/mermaid@9.2.2/dist/mermaid.esm.min.mjs';
+import { debounce } from '../../util.js';
+
+export 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.logBoxPre = logBox.el.appendChild(document.createElement('pre'));
+ this.inSection = 0;
+ }
+
+ static initializeAPI() {
+ 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',
+ },
+ });
+ }
+
+ async log(msg, render = true) {
+ if (this.logBoxPre.textContent && !this.logBoxPre.textContent.endsWith('\n')) {
+ this.logBoxPre.textContent = `${this.logBoxPre.textContent}\n`;
+ }
+ this.logBoxPre.textContent = `${this.logBoxPre.textContent}${msg}\n`;
+ if (render) {
+ await this.render();
+ }
+ return this;
+ }
+
+ getText() {
+ return this.logBoxPre.textContent;
+ }
+
+ async render() {
+ return debounce(async () => {
+ const text = this.getText();
+ try {
+ const graph = await mermaid.mermaidAPI.render(
+ this.element.getId(),
+ text,
+ );
+ this.renderBox.setInnerHTML(graph);
+ } catch (e) {
+ console.error(`render text:\n${text}`);
+ throw e;
+ }
+ }, 100);
+ }
+}
diff --git a/forum-network/src/classes/display/scene.js b/forum-network/src/classes/display/scene.js
new file mode 100644
index 0000000..b8fa561
--- /dev/null
+++ b/forum-network/src/classes/display/scene.js
@@ -0,0 +1,118 @@
+import { Action } from './action.js';
+import { CryptoUtil } from '../util/crypto.js';
+import { MermaidDiagram } from './mermaid.js';
+import { SequenceDiagram } from './sequence.js';
+import { Table } from './table.js';
+
+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.middleSection = this.box.addBox('Middle section');
+ this.box.addBox('Spacer').setInnerHTML(' ');
+ this.actors = new Set();
+ this.dateStart = new Date();
+ this.flowcharts = new Map();
+
+ MermaidDiagram.initializeAPI();
+
+ this.options = {
+ edgeNodeColor: '#4d585c',
+ };
+ }
+
+ withSequenceDiagram() {
+ const box = this.box.addBox('Sequence diagram');
+ this.box.addBox('Spacer').setInnerHTML(' ');
+ const logBox = this.box.addBox('Sequence diagram text').addClass('dim');
+ this.sequence = new SequenceDiagram(box, logBox);
+ this.sequence.log('sequenceDiagram', false);
+ return this;
+ }
+
+ withFlowchart({ direction = 'BT' } = {}) {
+ const box = this.topSection.addBox('Flowchart').addClass('padded');
+ this.box.addBox('Spacer').setInnerHTML(' ');
+ const logBox = this.box.addBox('Flowchart text').addClass('dim');
+ this.flowchart = new MermaidDiagram(box, logBox);
+ this.flowchart.log(`graph ${direction}`, false);
+ return this;
+ }
+
+ withAdditionalFlowchart({ id, name, direction = 'BT' } = {}) {
+ const index = this.flowcharts.size;
+ name = name ?? `Flowchart ${index}`;
+ id = id ?? `flowchart_${CryptoUtil.randomUUID().slice(0, 4)}`;
+ const container = this.middleSection.addBox(name).flex();
+ const box = container.addBox('Flowchart').addClass('padded');
+ const logBox = container.addBox('Flowchart text').addClass('dim');
+ const flowchart = new MermaidDiagram(box, logBox);
+ flowchart.log(`graph ${direction}`, false);
+ this.flowcharts.set(id, flowchart);
+ return this;
+ }
+
+ lastFlowchart() {
+ if (!this.flowcharts.size) {
+ if (this.flowchart) {
+ return this.flowchart;
+ }
+ throw new Error('lastFlowchart: No additional flowcharts have been added.');
+ }
+ const flowcharts = Array.from(this.flowcharts.values());
+ return flowcharts[flowcharts.length - 1];
+ }
+
+ withTable() {
+ if (this.table) {
+ return this;
+ }
+ const box = this.middleSection.addBox('Table').addClass('padded');
+ this.box.addBox('Spacer').setInnerHTML(' ');
+ this.table = new Table(box);
+ return this;
+ }
+
+ registerActor(actor) {
+ this.actors.add(actor);
+ // this.sequence?.log(`participant ${actor.name}`);
+ }
+
+ 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;
+ }
+
+ stateToTable(label) {
+ const row = new Map();
+ const columns = [];
+ columns.push({ key: 'seqNum', title: '#' });
+ columns.push({ key: 'elapsedMs', title: 'Time (ms)' });
+ row.set('seqNum', this.table.rows.length + 1);
+ row.set('elapsedMs', new Date() - this.dateStart);
+ row.set('label', label);
+ for (const actor of this.actors) {
+ for (const [aKey, { name, value }] of actor.getValuesMap()) {
+ const key = `${actor.name}:${aKey}`;
+ columns.push({ key, title: name });
+ row.set(key, value);
+ }
+ }
+ columns.push({ key: 'label', title: '' });
+ this.table.setColumns(columns);
+ this.table.addRow(row);
+ }
+}
diff --git a/forum-network/src/classes/display/sequence.js b/forum-network/src/classes/display/sequence.js
new file mode 100644
index 0000000..456f165
--- /dev/null
+++ b/forum-network/src/classes/display/sequence.js
@@ -0,0 +1,74 @@
+import { hexToRGB } from '../../util.js';
+import { MermaidDiagram } from './mermaid.js';
+
+export class SequenceDiagram extends MermaidDiagram {
+ constructor(...args) {
+ super(...args);
+ this.activations = [];
+ this.sections = [];
+ }
+
+ async log(...args) {
+ this.sections.forEach(async (section, index) => {
+ const {
+ empty, r, g, b,
+ } = section;
+ if (empty) {
+ section.empty = false;
+ await super.log(`rect rgb(${r}, ${g}, ${b}) # ${index}`, false);
+ }
+ });
+ return super.log(...args);
+ }
+
+ activate(name) {
+ this.log(`activate ${name}`, false);
+ this.activations.push(name);
+ }
+
+ async deactivate(name) {
+ const index = this.activations.findLastIndex((n) => n === name);
+ if (index === -1) throw new Error(`${name} does not appear to be active!`);
+ this.activations.splice(index, 1);
+ await this.log(`deactivate ${name}`);
+ }
+
+ getDeactivationsText() {
+ const text = Array.from(this.activations).reverse().reduce((str, name) => str += `deactivate ${name}\n`, '');
+ return text;
+ }
+
+ async startSection(color = '#08252c') {
+ let { r, g, b } = hexToRGB(color);
+ for (let i = 0; i < this.sections.length; i++) {
+ r += (0xff - r) / 16;
+ g += (0xff - g) / 16;
+ b += (0xff - b) / 16;
+ }
+ this.sections.push({
+ empty: true, r, g, b,
+ });
+ }
+
+ async endSection() {
+ const section = this.sections.pop();
+ if (section && !section.empty) {
+ await this.log('end');
+ }
+ }
+
+ getSectionEndText() {
+ if (this.sections[this.sections.length - 1]?.empty) {
+ this.sections.pop();
+ }
+ return this.sections.map(() => 'end\n').join('');
+ }
+
+ getText() {
+ let text = super.getText();
+ if (!text.endsWith('\n')) text = `${text}\n`;
+ text += this.getDeactivationsText();
+ text += this.getSectionEndText();
+ return text;
+ }
+}
diff --git a/forum-network/src/classes/display/table.js b/forum-network/src/classes/display/table.js
new file mode 100644
index 0000000..10dbd0b
--- /dev/null
+++ b/forum-network/src/classes/display/table.js
@@ -0,0 +1,45 @@
+import { displayNumber } from '../../util.js';
+
+export class Table {
+ constructor(box) {
+ this.box = box;
+ this.columns = [];
+ this.rows = [];
+ this.table = box.el.appendChild(document.createElement('table'));
+ this.headings = this.table.appendChild(document.createElement('tr'));
+ }
+
+ setColumns(columns) {
+ if (JSON.stringify(columns) === JSON.stringify(this.columns)) {
+ return;
+ }
+ if (this.columns.length) {
+ this.table.innerHTML = '';
+ this.headings = this.table.appendChild(document.createElement('tr'));
+ this.columns = [];
+ }
+ this.columns = columns;
+ for (const { title } of columns) {
+ const heading = document.createElement('th');
+ this.headings.appendChild(heading);
+ heading.innerHTML = title ?? '';
+ }
+ if (this.rows.length) {
+ const { rows } = this;
+ this.rows = [];
+ for (const row of rows) {
+ this.addRow(row);
+ }
+ }
+ }
+
+ addRow(rowMap) {
+ this.rows.push(rowMap);
+ const row = this.table.appendChild(document.createElement('tr'));
+ for (const { key } of this.columns) {
+ const value = rowMap.get(key);
+ const cell = row.appendChild(document.createElement('td'));
+ cell.innerHTML = typeof value === 'number' ? displayNumber(value) : value ?? '';
+ }
+ }
+}
diff --git a/forum-network/src/classes/forum-node.js b/forum-network/src/classes/forum-network/forum-node.js
similarity index 91%
rename from forum-network/src/classes/forum-node.js
rename to forum-network/src/classes/forum-network/forum-node.js
index 832ad7d..f36032e 100644
--- a/forum-network/src/classes/forum-node.js
+++ b/forum-network/src/classes/forum-network/forum-node.js
@@ -1,18 +1,18 @@
-import { Action } from './action.js';
+import { Action } from '../display/action.js';
import {
Message, PostMessage, PeerMessage, messageFromJSON,
} from './message.js';
import { ForumView } from './forum-view.js';
import { NetworkNode } from './network-node.js';
-import { randomID } from '../util.js';
+import { randomID } from '../../util.js';
export class ForumNode extends NetworkNode {
- constructor(name) {
- super(name);
+ constructor(name, scene) {
+ super(name, scene);
this.forumView = new ForumView();
this.actions = {
...this.actions,
- storePost: new Action('store post'),
+ storePost: new Action('store post', scene),
};
}
diff --git a/forum-network/src/classes/forum-view.js b/forum-network/src/classes/forum-network/forum-view.js
similarity index 97%
rename from forum-network/src/classes/forum-view.js
rename to forum-network/src/classes/forum-network/forum-view.js
index 709e944..a7e19a0 100644
--- a/forum-network/src/classes/forum-view.js
+++ b/forum-network/src/classes/forum-network/forum-view.js
@@ -1,4 +1,4 @@
-import { WDAG } from './wdag.js';
+import { WDAG } from '../supporting/wdag.js';
class Author {
constructor() {
diff --git a/forum-network/src/classes/message.js b/forum-network/src/classes/forum-network/message.js
similarity index 92%
rename from forum-network/src/classes/message.js
rename to forum-network/src/classes/forum-network/message.js
index 03b7592..2e0ab81 100644
--- a/forum-network/src/classes/message.js
+++ b/forum-network/src/classes/forum-network/message.js
@@ -1,5 +1,5 @@
-import { CryptoUtil } from './crypto.js';
-import { PostContent } from './post-content.js';
+import { CryptoUtil } from '../util/crypto.js';
+import { PostContent } from '../util/post-content.js';
export class Message {
constructor(content) {
diff --git a/forum-network/src/classes/network-node.js b/forum-network/src/classes/forum-network/network-node.js
similarity index 83%
rename from forum-network/src/classes/network-node.js
rename to forum-network/src/classes/forum-network/network-node.js
index 8e96b5f..72072b9 100644
--- a/forum-network/src/classes/network-node.js
+++ b/forum-network/src/classes/forum-network/network-node.js
@@ -1,14 +1,14 @@
-import { Actor } from './actor.js';
-import { Action } from './action.js';
-import { CryptoUtil } from './crypto.js';
-import { PrioritizedQueue } from './prioritized-queue.js';
+import { Actor } from '../display/actor.js';
+import { Action } from '../display/action.js';
+import { CryptoUtil } from '../util/crypto.js';
+import { PrioritizedQueue } from '../util/prioritized-queue.js';
export class NetworkNode extends Actor {
- constructor(name) {
- super(name);
+ constructor(name, scene) {
+ super(name, scene);
this.queue = new PrioritizedQueue();
this.actions = {
- peerMessage: new Action('peer message'),
+ peerMessage: new Action('peer message', scene),
};
}
diff --git a/forum-network/src/classes/network.js b/forum-network/src/classes/forum-network/network.js
similarity index 100%
rename from forum-network/src/classes/network.js
rename to forum-network/src/classes/forum-network/network.js
diff --git a/forum-network/src/classes/block-consensus.js b/forum-network/src/classes/ideas/block-consensus.js
similarity index 100%
rename from forum-network/src/classes/block-consensus.js
rename to forum-network/src/classes/ideas/block-consensus.js
diff --git a/forum-network/src/classes/exchange.js b/forum-network/src/classes/ideas/exchange.js
similarity index 100%
rename from forum-network/src/classes/exchange.js
rename to forum-network/src/classes/ideas/exchange.js
diff --git a/forum-network/src/classes/finance.js b/forum-network/src/classes/ideas/finance.js
similarity index 100%
rename from forum-network/src/classes/finance.js
rename to forum-network/src/classes/ideas/finance.js
diff --git a/forum-network/src/classes/question.js b/forum-network/src/classes/ideas/question.js
similarity index 100%
rename from forum-network/src/classes/question.js
rename to forum-network/src/classes/ideas/question.js
diff --git a/forum-network/src/classes/storage.js b/forum-network/src/classes/ideas/storage.js
similarity index 100%
rename from forum-network/src/classes/storage.js
rename to forum-network/src/classes/ideas/storage.js
diff --git a/forum-network/src/classes/list.js b/forum-network/src/classes/list.js
deleted file mode 100644
index 8dc58ac..0000000
--- a/forum-network/src/classes/list.js
+++ /dev/null
@@ -1,9 +0,0 @@
-export class List {
- constructor() {
- this.items = [];
- }
-
- add(item) {
- this.items.push(item);
- }
-}
diff --git a/forum-network/src/classes/reputation-holder.js b/forum-network/src/classes/reputation-holder.js
deleted file mode 100644
index 7ad195e..0000000
--- a/forum-network/src/classes/reputation-holder.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { randomID } from '../util.js';
-import { Actor } from './actor.js';
-
-export class ReputationHolder extends Actor {
- constructor(name) {
- super(name);
- this.reputationPublicKey = `${name}_${randomID()}`;
- }
-}
diff --git a/forum-network/src/classes/reputation.js b/forum-network/src/classes/reputation.js
deleted file mode 100644
index 7718825..0000000
--- a/forum-network/src/classes/reputation.js
+++ /dev/null
@@ -1,88 +0,0 @@
-class Lock {
- constructor(tokens, duration) {
- this.dateCreated = new Date();
- this.tokens = tokens;
- this.duration = duration;
- }
-}
-
-class Reputation {
- constructor() {
- this.tokens = 0;
- this.locks = new Set();
- }
-
- addTokens(tokens) {
- if (this.tokens + tokens < 0) {
- throw new Error('Token balance can not become negative');
- }
- this.tokens += tokens;
- }
-
- lockTokens(tokens, duration) {
- if (tokens > this.getAvailableTokens()) {
- throw new Error('Can not lock more tokens than are available');
- }
- const lock = new Lock(tokens, duration);
- this.locks.add(lock);
- // TODO: Prune locks once expired
- }
-
- getTokens() {
- return this.tokens;
- }
-
- getAvailableTokens() {
- const now = new Date();
- const tokensLocked = Array.from(this.locks.values())
- .filter(({ dateCreated, duration }) => now - dateCreated < duration)
- .reduce((acc, cur) => acc += cur.tokens, 0);
- if (tokensLocked > this.tokens) {
- throw new Error('Assertion failure. tokensLocked > tokens');
- }
- return this.tokens - tokensLocked;
- }
-}
-
-export class Reputations extends Map {
- getTokens(reputationPublicKey) {
- const reputation = this.get(reputationPublicKey);
- if (!reputation) {
- return 0;
- }
- return reputation.getTokens();
- }
-
- getAvailableTokens(reputationPublicKey) {
- const reputation = this.get(reputationPublicKey);
- if (!reputation) {
- return 0;
- }
- return reputation.getAvailableTokens();
- }
-
- addTokens(reputationPublicKey, tokens) {
- const reputation = this.get(reputationPublicKey) ?? new Reputation();
- reputation.addTokens(tokens);
- this.set(reputationPublicKey, reputation);
- }
-
- lockTokens(reputationPublicKey, tokens, duration) {
- if (!tokens || !duration) {
- return;
- }
- const reputation = this.get(reputationPublicKey);
- if (!reputation) {
- throw new Error(`${reputationPublicKey} has no tokens to lock`);
- }
- reputation.lockTokens(tokens, duration);
- }
-
- getTotal() {
- return Array.from(this.values()).reduce((acc, cur) => acc += cur.getTokens(), 0);
- }
-
- getTotalAvailable() {
- return Array.from(this.values()).reduce((acc, cur) => acc += cur.getAvailableTokens(), 0);
- }
-}
diff --git a/forum-network/src/classes/scene.js b/forum-network/src/classes/scene.js
deleted file mode 100644
index e487ba1..0000000
--- a/forum-network/src/classes/scene.js
+++ /dev/null
@@ -1,239 +0,0 @@
-import mermaid from 'https://unpkg.com/mermaid@9.2.2/dist/mermaid.esm.min.mjs';
-import { Actor } from './actor.js';
-import { Action } from './action.js';
-import { debounce, hexToRGB } from '../util.js';
-import { CryptoUtil } from './crypto.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.logBoxPre = logBox.el.appendChild(document.createElement('pre'));
- this.inSection = 0;
- }
-
- async log(msg, render = true) {
- this.logBoxPre.textContent = `${this.logBoxPre.textContent}\n${msg}`;
- if (render) {
- await this.render();
- }
- return this;
- }
-
- async render() {
- const render = async () => {
- let text = this.logBoxPre.textContent;
- for (let i = 0; i < this.inSection; i++) {
- text += '\nend';
- }
- const graph = await mermaid.mermaidAPI.render(
- this.element.getId(),
- text,
- );
- this.renderBox.setInnerHTML(graph);
- };
- await debounce(render, 100);
- }
-}
-
-class Table {
- constructor(box) {
- this.box = box;
- this.columns = [];
- this.rows = [];
- this.table = box.el.appendChild(document.createElement('table'));
- this.headings = this.table.appendChild(document.createElement('tr'));
- }
-
- setColumns(columns) {
- if (JSON.stringify(columns) === JSON.stringify(this.columns)) {
- return;
- }
- if (this.columns.length) {
- this.table.innerHTML = '';
- this.headings = this.table.appendChild(document.createElement('tr'));
- this.columns = [];
- }
- this.columns = columns;
- for (const { title } of columns) {
- const heading = document.createElement('th');
- this.headings.appendChild(heading);
- heading.innerHTML = title ?? '';
- }
- if (this.rows.length) {
- const { rows } = this;
- this.rows = [];
- for (const row of rows) {
- this.addRow(row);
- }
- }
- }
-
- addRow(rowMap) {
- this.rows.push(rowMap);
- const row = this.table.appendChild(document.createElement('tr'));
- for (const { key } of this.columns) {
- const value = rowMap.get(key);
- const cell = row.appendChild(document.createElement('td'));
- cell.innerHTML = value ?? '';
- }
- }
-}
-
-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.middleSection = this.box.addBox('Middle section');
- this.box.addBox('Spacer').setInnerHTML(' ');
- this.actors = new Set();
- this.dateStart = new Date();
- this.flowcharts = new Map();
-
- 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',
- },
- });
-
- this.options = {
- edgeNodeColor: '#4d585c',
- };
- }
-
- withSequenceDiagram() {
- const box = this.box.addBox('Sequence diagram');
- this.box.addBox('Spacer').setInnerHTML(' ');
- const logBox = this.box.addBox('Sequence diagram text').addClass('dim');
- this.sequence = new MermaidDiagram(box, logBox);
- this.sequence.log('sequenceDiagram', false);
- return this;
- }
-
- withFlowchart({ direction = 'BT' } = {}) {
- const box = this.topSection.addBox('Flowchart').addClass('padded');
- this.box.addBox('Spacer').setInnerHTML(' ');
- const logBox = this.box.addBox('Flowchart text').addClass('dim');
- this.flowchart = new MermaidDiagram(box, logBox);
- this.flowchart.log(`graph ${direction}`, false);
- return this;
- }
-
- withAdditionalFlowchart({ id, name, direction = 'BT' } = {}) {
- const index = this.flowcharts.size;
- name = name ?? `Flowchart ${index}`;
- id = id ?? `flowchart_${CryptoUtil.randomUUID().slice(0, 4)}`;
- const container = this.middleSection.addBox(name).flex();
- const box = container.addBox('Flowchart').addClass('padded');
- const logBox = container.addBox('Flowchart text').addClass('dim');
- const flowchart = new MermaidDiagram(box, logBox);
- flowchart.log(`graph ${direction}`, false);
- this.flowcharts.set(id, flowchart);
- return this;
- }
-
- lastFlowchart() {
- if (!this.flowcharts.size) {
- if (this.flowchart) {
- return this.flowchart;
- }
- throw new Error('lastFlowchart: No additional flowcharts have been added.');
- }
- const flowcharts = Array.from(this.flowcharts.values());
- return flowcharts[flowcharts.length - 1];
- }
-
- withTable() {
- if (this.table) {
- return this;
- }
- const box = this.middleSection.addBox('Table').addClass('padded');
- this.box.addBox('Spacer').setInnerHTML(' ');
- this.table = new Table(box);
- return this;
- }
-
- async addActor(name) {
- const actor = new Actor(name);
- if (this.sequence) {
- await this.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');
- }
-
- stateToTable(label) {
- const row = new Map();
- const columns = [];
- columns.push({ key: 'seqNum', title: '#' });
- columns.push({ key: 'elapsedMs', title: 'Time (ms)' });
- row.set('seqNum', this.table.rows.length + 1);
- row.set('elapsedMs', new Date() - this.dateStart);
- row.set('label', label);
- for (const actor of this.actors) {
- for (const [aKey, { name, value }] of actor.getValuesMap()) {
- const key = `${actor.name}:${aKey}`;
- columns.push({ key, title: name });
- row.set(key, value);
- }
- }
- columns.push({ key: 'label', title: '' });
- this.table.setColumns(columns);
- this.table.addRow(row);
- }
-}
diff --git a/forum-network/src/classes/stake.js b/forum-network/src/classes/supporting/stake.js
similarity index 89%
rename from forum-network/src/classes/stake.js
rename to forum-network/src/classes/supporting/stake.js
index 5a5d02a..42eb016 100644
--- a/forum-network/src/classes/stake.js
+++ b/forum-network/src/classes/supporting/stake.js
@@ -1,4 +1,4 @@
-import params from '../params.js';
+import params from '../../params.js';
export class Stake {
constructor({
diff --git a/forum-network/src/classes/vm.js b/forum-network/src/classes/supporting/vm.js
similarity index 71%
rename from forum-network/src/classes/vm.js
rename to forum-network/src/classes/supporting/vm.js
index ee6f1a6..03e5dbf 100644
--- a/forum-network/src/classes/vm.js
+++ b/forum-network/src/classes/supporting/vm.js
@@ -1,4 +1,4 @@
-import { Action } from './action.js';
+import { Action } from '../display/action.js';
class ContractRecord {
constructor(id, instance) {
@@ -12,7 +12,8 @@ export class VMHandle {
this.vm = vm;
this.sender = sender;
this.actions = {
- call: new Action('call'),
+ call: new Action('call', vm.scene),
+ return: new Action('return', vm.scene),
};
}
@@ -20,17 +21,20 @@ export class VMHandle {
* @param {string} id Contract ID
* @param {string} method
*/
- callContract(id, method, ...args) {
+ async callContract(id, method, ...args) {
const instance = this.vm.getContractInstance(id);
const fn = instance[method];
if (!fn) throw new Error(`Contract ${id} method ${method} not found!`);
- this.actions.call.log(this.sender, instance, method);
- return fn.call(instance, this.sender, ...args);
+ await this.actions.call.log(this.sender, instance, method);
+ const result = await fn.call(instance, this.sender, ...args);
+ await this.actions.return.log(instance, this.sender, undefined, undefined, '-->>');
+ return result;
}
}
export class VM {
- constructor() {
+ constructor(scene) {
+ this.scene = scene;
this.contracts = new Map();
}
diff --git a/forum-network/src/classes/voter.js b/forum-network/src/classes/supporting/voter.js
similarity index 100%
rename from forum-network/src/classes/voter.js
rename to forum-network/src/classes/supporting/voter.js
diff --git a/forum-network/src/classes/wdag.js b/forum-network/src/classes/supporting/wdag.js
similarity index 95%
rename from forum-network/src/classes/wdag.js
rename to forum-network/src/classes/supporting/wdag.js
index 28871b8..bec7a34 100644
--- a/forum-network/src/classes/wdag.js
+++ b/forum-network/src/classes/supporting/wdag.js
@@ -25,16 +25,17 @@ export class Edge {
}
export class WDAG {
- constructor() {
+ constructor(scene) {
+ this.scene = scene;
this.vertices = new Map();
this.edgeLabels = new Map();
this.nextVertexId = 0;
- this.flowchart = window?.scene?.flowchart ?? null;
+ this.flowchart = scene?.flowchart;
}
withFlowchart() {
- window?.scene?.withAdditionalFlowchart();
- this.flowchart = window?.scene?.lastFlowchart();
+ this.scene?.withAdditionalFlowchart();
+ this.flowchart = this.scene?.lastFlowchart();
return this;
}
diff --git a/forum-network/src/classes/crypto.js b/forum-network/src/classes/util/crypto.js
similarity index 100%
rename from forum-network/src/classes/crypto.js
rename to forum-network/src/classes/util/crypto.js
diff --git a/forum-network/src/classes/post-content.js b/forum-network/src/classes/util/post-content.js
similarity index 100%
rename from forum-network/src/classes/post-content.js
rename to forum-network/src/classes/util/post-content.js
diff --git a/forum-network/src/classes/prioritized-queue.js b/forum-network/src/classes/util/prioritized-queue.js
similarity index 100%
rename from forum-network/src/classes/prioritized-queue.js
rename to forum-network/src/classes/util/prioritized-queue.js
diff --git a/forum-network/src/index.html b/forum-network/src/index.html
index 7f6fa7b..473347e 100644
--- a/forum-network/src/index.html
+++ b/forum-network/src/index.html
@@ -14,13 +14,13 @@
- Basic Sequencing
+ - Basic Sequencing 2
- WDAG
- Debounce
- Flowchart
diff --git a/forum-network/src/tests/all.test.html b/forum-network/src/tests/all.test.html
index 37c668f..82baba7 100644
--- a/forum-network/src/tests/all.test.html
+++ b/forum-network/src/tests/all.test.html
@@ -32,7 +32,7 @@
+
+
+
+
+
diff --git a/forum-network/src/tests/basic.test.html b/forum-network/src/tests/basic.test.html
index 01d88ca..a103f5e 100644
--- a/forum-network/src/tests/basic.test.html
+++ b/forum-network/src/tests/basic.test.html
@@ -9,198 +9,5 @@
+
+
+
-
+
+
-
+
-
diff --git a/forum-network/src/tests/basic2.test.html b/forum-network/src/tests/basic2.test.html
new file mode 100644
index 0000000..9b98448
--- /dev/null
+++ b/forum-network/src/tests/basic2.test.html
@@ -0,0 +1,13 @@
+
+
+
Debounce testForum Network test
+
Forum Network
+
+
+
+
+
diff --git a/forum-network/src/tests/debounce.test.html b/forum-network/src/tests/debounce.test.html
index bb184af..219c100 100644
--- a/forum-network/src/tests/debounce.test.html
+++ b/forum-network/src/tests/debounce.test.html
@@ -2,35 +2,35 @@
+
+
-
+
+
+
+
+
+
+
diff --git a/forum-network/src/tests/forum-network.test.html b/forum-network/src/tests/forum-network.test.html
index a6ab1db..9e531aa 100644
--- a/forum-network/src/tests/forum-network.test.html
+++ b/forum-network/src/tests/forum-network.test.html
@@ -2,12 +2,32 @@
+
+
+
+
+
+
+
+
diff --git a/forum-network/src/tests/scripts/availability.test.js b/forum-network/src/tests/scripts/availability.test.js
index 736d7ab..0247c82 100644
--- a/forum-network/src/tests/scripts/availability.test.js
+++ b/forum-network/src/tests/scripts/availability.test.js
@@ -1,53 +1,83 @@
-import { Box } from '../../classes/box.js';
-import { Scene } from '../../classes/scene.js';
-import { Expert } from '../../classes/expert.js';
+import { Box } from '../../classes/display/box.js';
+import { Scene } from '../../classes/display/scene.js';
+import { Expert } from '../../classes/actors/expert.js';
import { delay } from '../../util.js';
-import { DAO } from '../../classes/dao.js';
-import { Public } from '../../classes/public.js';
-import { PostContent } from '../../classes/post-content.js';
+import { DAO } from '../../classes/actors/dao.js';
+import { Public } from '../../classes/actors/public.js';
+import { PostContent } from '../../classes/util/post-content.js';
-const DELAY_INTERVAL = 500;
+const DELAY_INTERVAL = 100;
+const POOL_DURATION = 200;
let dao;
let experts;
let requestor;
+let scene;
+
const newExpert = async () => {
const index = experts.length;
const name = `Expert${index + 1}`;
- const expert = await new Expert(dao, name).initialize();
+ const expert = await new Expert(dao, name, scene).initialize();
+ expert.setValue(
+ 'rep',
+ () => dao.reputation.valueOwnedBy(expert.reputationPublicKey),
+ );
experts.push(expert);
return expert;
};
-async function setup() {
+const setup = async () => {
const rootElement = document.getElementById('scene');
const rootBox = new Box('rootBox', rootElement).flex();
- const scene = (window.scene = new Scene('Availability test', rootBox));
+ scene = new Scene('Availability test', rootBox);
scene.withSequenceDiagram();
scene.withFlowchart();
scene.withTable();
- experts = (window.experts = []);
- dao = (window.dao = new DAO('DGF'));
+ dao = new DAO('DGF', scene);
+ await dao.setValue('total rep', () => dao.reputation.getTotal());
+
+ experts = [];
+
await newExpert();
await newExpert();
- requestor = new Public('Public');
-}
+ requestor = new Public('Public', scene);
-const updateDisplayValues = async () => {
- for (const expert of experts) {
- await expert.setValue(
- 'rep',
- dao.reputation.valueOwnedBy(expert.reputationPublicKey),
- );
- }
- await dao.setValue('total rep', dao.reputation.getTotal());
- await window.scene.sequence.render();
-};
+ await delay(DELAY_INTERVAL);
-const updateDisplayValuesAndDelay = async (delayMs = DELAY_INTERVAL) => {
- await updateDisplayValues();
- await delay(delayMs);
+ // Experts gain initial reputation by submitting a post with fee
+ const { postId: postId1, pool: pool1 } = await experts[0].submitPostWithFee(
+ new PostContent({ hello: 'there' }).setTitle('Post 1'),
+ {
+ fee: 10,
+ duration: POOL_DURATION,
+ tokenLossRatio: 1,
+ },
+ );
+ await delay(POOL_DURATION);
+
+ await pool1.evaluateWinningConditions();
+ await delay(DELAY_INTERVAL);
+
+ dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(10);
+
+ const { pool: pool2 } = await experts[1].submitPostWithFee(
+ new PostContent({ hello: 'to you as well' })
+ .setTitle('Post 2')
+ .addCitation(postId1, 0.5),
+ {
+ fee: 10,
+ duration: POOL_DURATION,
+ tokenLossRatio: 1,
+ },
+ );
+ await delay(POOL_DURATION);
+
+ await pool2.evaluateWinningConditions();
+ await delay(DELAY_INTERVAL);
+
+ dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(15);
+ dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(5);
};
const getActiveWorker = async () => {
@@ -76,90 +106,64 @@ const voteForWorkEvidence = async (worker, pool) => {
}
};
-await setup();
+describe('Availability + Business', () => {
+ before(async () => {
+ await setup();
+ });
-await updateDisplayValuesAndDelay();
+ beforeEach(async () => {
+ // await scene.sequence.startSection();
+ });
-// Experts gain initial reputation by submitting a post with fee
-const { postId: postId1, pool: pool1 } = await experts[0].submitPostWithFee(
- new PostContent({ hello: 'there' }).setTitle('Post 1'),
- {
- fee: 10,
- duration: 1000,
- tokenLossRatio: 1,
- },
-);
-await updateDisplayValuesAndDelay(1000);
+ afterEach(async () => {
+ // await scene.sequence.endSection();
+ });
-await pool1.evaluateWinningConditions();
-await updateDisplayValuesAndDelay();
+ it('Experts can register their availability for some duration', async () => {
+ await experts[0].registerAvailability(1, 10000);
+ await experts[1].registerAvailability(1, 10000);
+ await delay(DELAY_INTERVAL);
+ });
-const { pool: pool2 } = await experts[1].submitPostWithFee(
- 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 experts[0].registerAvailability(1, 10000);
-await experts[1].registerAvailability(1, 10000);
-await updateDisplayValuesAndDelay();
-
-// Submit work request
-await requestor.submitRequest(
- dao.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(
- 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',
+ it('Public can submit a work request', async () => {
+ await requestor.submitRequest(
+ dao.business,
+ { fee: 100 },
+ { please: 'do some work' },
);
- } else {
- console.error('Unexpected error');
- throw e;
- }
-}
+ await delay(DELAY_INTERVAL);
+ });
+
+ it('Expert can submit work evidence', async () => {
+ // Receive work request
+ const { worker, request } = await getActiveWorker();
+ const pool3 = await worker.submitWork(
+ request.id,
+ {
+ here: 'is some evidence of work product',
+ },
+ {
+ tokenLossRatio: 1,
+ duration: POOL_DURATION,
+ },
+ );
+ await worker.deactivate();
+
+ // Stake on work evidence
+ await voteForWorkEvidence(worker, pool3);
+
+ // Wait for validation pool duration to elapse
+ await delay(POOL_DURATION);
+
+ // Distribute reputation awards and fees
+ await pool3.evaluateWinningConditions();
+ await delay(DELAY_INTERVAL);
+
+ // This should throw an exception since the pool is already resolved
+ try {
+ await pool3.evaluateWinningConditions();
+ } catch (e) {
+ e.should.match(/Validation pool has already been resolved/);
+ }
+ }).timeout(10000);
+});
diff --git a/forum-network/src/tests/scripts/basic.test.js b/forum-network/src/tests/scripts/basic.test.js
new file mode 100644
index 0000000..42b9aac
--- /dev/null
+++ b/forum-network/src/tests/scripts/basic.test.js
@@ -0,0 +1,67 @@
+import { Box } from '../../classes/display/box.js';
+import { Scene } from '../../classes/display/scene.js';
+import { Actor } from '../../classes/display/actor.js';
+import { Action } from '../../classes/display/action.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 run() {
+ const 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');
+
+ const webClient = new Actor('web client', scene);
+ const node1 = new Actor('node 1', scene);
+ const blockchain = new Actor('blockchain', scene);
+ const requestForumPage = new Action('requestForumPage', scene);
+ const readBlockchainData = new Action('readBlockchainData', scene);
+ const blockchainData = new Action('blockchainData', scene);
+ const forumPage = new Action('forumPage', scene);
+
+ 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');
+ });
+
+ setTimeout(() => {
+ webClient.send(node1, requestForumPage);
+ webClientStatus.set('Requested forum page');
+ }, randomDelay(500, 1500));
+
+ setInterval(() => {
+ webClient.send(node1, requestForumPage);
+ webClientStatus.set('Requested forum page');
+ }, randomDelay(6000, 12000));
+}());
diff --git a/forum-network/src/tests/scripts/basic2.test.js b/forum-network/src/tests/scripts/basic2.test.js
new file mode 100644
index 0000000..ae24b55
--- /dev/null
+++ b/forum-network/src/tests/scripts/basic2.test.js
@@ -0,0 +1,139 @@
+import { Box } from '../../classes/display/box.js';
+import { Scene } from '../../classes/display/scene.js';
+import { Actor } from '../../classes/display/actor.js';
+
+const rootElement = document.getElementById('basic');
+const rootBox = new Box('rootBox', rootElement).flex();
+
+function delay(min, max = min) {
+ const delayMs = min + Math.random() * (max - min);
+ return new Promise((resolve) => {
+ setTimeout(resolve, delayMs);
+ });
+}
+
+(async function run() {
+ const scene = new Scene('Scene 2', rootBox).withSequenceDiagram();
+
+ const webClient = new Actor('webClient', scene);
+
+ const nodes = [];
+ const memories = [];
+ const storages = [];
+
+ async function addNode() {
+ const idx = nodes.length;
+ const node = new Actor(`node${idx}`, scene);
+ const memory = new Actor(`memory${idx}`, scene);
+ const storage = new Actor(`storage${idx}`, scene);
+ 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];
+ }
+
+ await addNode();
+ await 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);
+}());
diff --git a/forum-network/src/tests/scripts/business.test.js b/forum-network/src/tests/scripts/business.test.js
index c99ab3e..ce87e31 100644
--- a/forum-network/src/tests/scripts/business.test.js
+++ b/forum-network/src/tests/scripts/business.test.js
@@ -1,6 +1,6 @@
-import { Business } from '../../classes/business.js';
-import { Scene } from '../../classes/scene.js';
-import { Box } from '../../classes/box.js';
+import { Business } from '../../classes/actors/business.js';
+import { Scene } from '../../classes/display/scene.js';
+import { Box } from '../../classes/display/box.js';
describe('Business', () => {
let scene;
diff --git a/forum-network/src/tests/scripts/debounce.test.js b/forum-network/src/tests/scripts/debounce.test.js
new file mode 100644
index 0000000..286d310
--- /dev/null
+++ b/forum-network/src/tests/scripts/debounce.test.js
@@ -0,0 +1,72 @@
+import { Box } from '../../classes/display/box.js';
+import { Scene } from '../../classes/display/scene.js';
+import { Action } from '../../classes/display/action.js';
+import { Actor } from '../../classes/display/actor.js';
+import { debounce, delay } from '../../util.js';
+
+describe('Debounce', () => {
+ let scene;
+ let caller;
+ let debouncer;
+ let method;
+
+ let call;
+ let execute;
+
+ before(() => {
+ const rootElement = document.getElementById('scene');
+ const rootBox = new Box('rootBox', rootElement).flex();
+
+ scene = new Scene('Debounce test', rootBox).withSequenceDiagram();
+ caller = new Actor('Caller', scene);
+ debouncer = new Actor('Debouncer', scene);
+ method = new Actor('Target method', scene);
+
+ call = new Action('call', scene);
+ execute = new Action('execute', scene);
+ });
+
+ it('Suppresses extra events that occur within the specified window', async () => {
+ let eventCount = 0;
+ const event = sinon.spy(async () => {
+ eventCount++;
+ await execute.log(debouncer, method, eventCount);
+ });
+
+ await scene.sequence.startSection();
+ await call.log(caller, debouncer, '1');
+ await debounce(event, 500);
+ await call.log(caller, debouncer, '2');
+ await debounce(event, 500);
+
+ await delay(500);
+ event.should.have.been.calledOnce;
+
+ await call.log(caller, debouncer, '3');
+ await debounce(event, 500);
+ await call.log(caller, debouncer, '4');
+ await debounce(event, 500);
+
+ eventCount.should.equal(2);
+ event.should.have.been.calledTwice;
+ await scene.sequence.endSection();
+ });
+
+ it('Propagates exceptions', async () => {
+ const event = sinon.spy(async () => {
+ await execute.log(debouncer, method, undefined, undefined, '-x');
+ throw new Error('An error occurs in the callback');
+ });
+ await scene.sequence.startSection();
+
+ try {
+ await call.log(caller, debouncer);
+ await debounce(event, 500);
+ } catch (e) {
+ event.should.have.been.calledOnce;
+ e.should.exist;
+ e.should.match(/An error occurs in the callback/);
+ }
+ await scene.sequence.endSection();
+ });
+});
diff --git a/forum-network/src/tests/scripts/forum-network.test.js b/forum-network/src/tests/scripts/forum-network.test.js
index 0933127..d1737c8 100644
--- a/forum-network/src/tests/scripts/forum-network.test.js
+++ b/forum-network/src/tests/scripts/forum-network.test.js
@@ -1,58 +1,76 @@
-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 { Network } from '../../classes/network.js';
+import { Box } from '../../classes/display/box.js';
+import { Scene } from '../../classes/display/scene.js';
+import { PostContent } from '../../classes/util/post-content.js';
+import { Expert } from '../../classes/actors/expert.js';
+import { ForumNode } from '../../classes/forum-network/forum-node.js';
+import { Network } from '../../classes/forum-network/network.js';
import { delay, randomID } from '../../util.js';
-const rootElement = document.getElementById('scene');
-const rootBox = new Box('rootBox', rootElement).flex();
+describe('Forum Network', () => {
+ let scene;
+ let author1;
+ let author2;
+ let forumNetwork;
+ let forumNode1;
+ let forumNode2;
+ let forumNode3;
+ let processInterval;
-window.scene = new Scene('Forum Network test', rootBox).withSequenceDiagram();
+ before(async () => {
+ const rootElement = document.getElementById('scene');
+ const rootBox = new Box('rootBox', rootElement).flex();
-window.author1 = await new Expert('author1', window.scene).initialize();
-window.author2 = await new Expert('author2', window.scene).initialize();
+ scene = new Scene('Forum Network test', rootBox).withSequenceDiagram();
-window.forumNetwork = new Network();
+ author1 = await new Expert(null, 'author1', scene).initialize();
+ author2 = await new Expert(null, 'author2', scene).initialize();
-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,
-);
+ forumNetwork = new Network();
-const processInterval = setInterval(async () => {
- await window.forumNode1.processNextMessage();
- await window.forumNode2.processNextMessage();
- await window.forumNode3.processNextMessage();
-}, 100);
+ forumNode1 = await new ForumNode('node1', scene).initialize(
+ forumNetwork,
+ );
+ forumNode2 = await new ForumNode('node2', scene).initialize(
+ forumNetwork,
+ );
+ forumNode3 = await new ForumNode('node3', scene).initialize(
+ forumNetwork,
+ );
-// const blockchain = new Blockchain();
+ processInterval = setInterval(async () => {
+ await forumNode1.processNextMessage();
+ await forumNode2.processNextMessage();
+ await forumNode3.processNextMessage();
+ }, 100);
+ });
-window.post1 = new PostContent({ message: 'hi' });
-window.post1.id = randomID();
-window.post2 = new PostContent({ message: 'hello' }).addCitation(
- window.post1.id,
- 1.0,
-);
+ after(() => {
+ clearInterval(processInterval);
+ });
-await delay(1000);
-await window.author1.submitPostViaNetwork(
- window.forumNode1,
- window.post1,
- 50,
-);
-await delay(1000);
-await window.author2.submitPostViaNetwork(
- window.forumNode2,
- window.post2,
- 100,
-);
+ // const blockchain = new Blockchain();
-await delay(1000);
-clearInterval(processInterval);
+ specify('Author can submit a post to the network', async () => {
+ const post1 = new PostContent({ message: 'hi' });
+ post1.id = randomID();
+ const post2 = new PostContent({ message: 'hello' }).addCitation(
+ post1.id,
+ 1.0,
+ );
+
+ await delay(1000);
+ await author1.submitPostViaNetwork(
+ forumNode1,
+ post1,
+ 50,
+ );
+ await delay(1000);
+ await author2.submitPostViaNetwork(
+ forumNode2,
+ post2,
+ 100,
+ );
+
+ await delay(1000);
+ }).timeout(10000);
+});
diff --git a/forum-network/src/tests/scripts/forum/forum.test-util.js b/forum-network/src/tests/scripts/forum/forum.test-util.js
index ce290c9..1f8cc97 100644
--- a/forum-network/src/tests/scripts/forum/forum.test-util.js
+++ b/forum-network/src/tests/scripts/forum/forum.test-util.js
@@ -1,13 +1,14 @@
-import { Box } from '../../../classes/box.js';
-import { Scene } from '../../../classes/scene.js';
-import { Expert } from '../../../classes/expert.js';
-import { PostContent } from '../../../classes/post-content.js';
+import { Box } from '../../../classes/display/box.js';
+import { Scene } from '../../../classes/display/scene.js';
+import { Expert } from '../../../classes/actors/expert.js';
+import { PostContent } from '../../../classes/util/post-content.js';
import { delay } from '../../../util.js';
import params from '../../../params.js';
-import { DAO } from '../../../classes/dao.js';
+import { DAO } from '../../../classes/actors/dao.js';
export class ForumTest {
constructor(options) {
+ this.scene = null;
this.dao = null;
this.experts = null;
this.posts = null;
@@ -18,19 +19,10 @@ export class ForumTest {
};
}
- async newExpert() {
- const index = this.experts.length;
- const name = `Expert${index + 1}`;
- const expert = await new Expert(this.dao, name).initialize();
- this.experts.push(expert);
- // expert.addValue('rep', () => this.dao.reputation.valueOwnedBy(expert.reputationPublicKey));
- return expert;
- }
-
async addPost(author, fee, citations = []) {
const postIndex = this.posts.length;
const title = `posts[${postIndex}]`;
- await window.scene.startSection();
+ await this.scene.sequence.startSection();
const postContent = new PostContent({}).setTitle(title);
for (const { postId, weight } of citations) {
@@ -48,16 +40,25 @@ export class ForumTest {
this.posts.push(postId);
await delay(this.options.poolDurationMs);
await pool.evaluateWinningConditions();
- await window.scene.endSection();
+ await this.scene.sequence.endSection();
await delay(this.options.defaultDelayMs);
return postId;
}
+ async newExpert() {
+ const index = this.experts.length;
+ const name = `Expert${index + 1}`;
+ const expert = await new Expert(this.dao, name, this.scene).initialize();
+ this.experts.push(expert);
+ // await expert.addComputedValue('rep', () => this.dao.reputation.valueOwnedBy(expert.reputationPublicKey));
+ return expert;
+ }
+
async setup() {
const rootElement = document.getElementById('scene');
const rootBox = new Box('rootBox', rootElement).flex();
- const scene = (window.scene = new Scene('Forum test', rootBox));
+ const scene = this.scene = new Scene('Forum test', rootBox);
scene.withSequenceDiagram();
scene.withFlowchart();
scene.withTable();
@@ -70,17 +71,17 @@ export class ForumTest {
scene.addDisplayValue('q4. leachingValue').set(params.leachingValue);
scene.addDisplayValue(' ');
- this.dao = (window.dao = new DAO('DAO'));
+ this.dao = new DAO('DAO', scene);
this.forum = this.dao.forum;
- this.experts = (window.experts = []);
- this.posts = (window.posts = []);
+ this.experts = [];
+ this.posts = [];
await this.newExpert();
// await newExpert();
// await newExpert();
- this.dao.addValue('total value', () => this.dao.reputation.getTotal());
- // this.dao.addValue('total reputation', () => this.dao.forum.getTotalValue());
+ await this.dao.addComputedValue('total value', () => this.dao.reputation.getTotal());
+ // await this.dao.addComputedValue('total reputation', () => this.dao.forum.getTotalValue());
this.dao.computeValues();
}
}
diff --git a/forum-network/src/tests/scripts/forum/forum4.test.js b/forum-network/src/tests/scripts/forum/forum4.test.js
index 0ae804d..c4c3c55 100644
--- a/forum-network/src/tests/scripts/forum/forum4.test.js
+++ b/forum-network/src/tests/scripts/forum/forum4.test.js
@@ -53,7 +53,6 @@ describe('Forum', () => {
await forumTest.addPost(experts[0], 10, [
{ postId: posts[2], weight: 1 },
]);
- console.log('test5', { posts });
forum.getPost(posts[0]).value.should.equal(0);
forum.getPost(posts[1]).value.should.equal(40);
forum.getPost(posts[2]).value.should.equal(0);
diff --git a/forum-network/src/tests/scripts/validation-pool.test.js b/forum-network/src/tests/scripts/validation-pool.test.js
index cbf9471..592a91a 100644
--- a/forum-network/src/tests/scripts/validation-pool.test.js
+++ b/forum-network/src/tests/scripts/validation-pool.test.js
@@ -1,9 +1,9 @@
-import { Box } from '../../classes/box.js';
-import { Scene } from '../../classes/scene.js';
-import { Expert } from '../../classes/expert.js';
-import { PostContent } from '../../classes/post-content.js';
+import { Box } from '../../classes/display/box.js';
+import { Scene } from '../../classes/display/scene.js';
+import { Expert } from '../../classes/actors/expert.js';
+import { PostContent } from '../../classes/util/post-content.js';
import { delay } from '../../util.js';
-import { DAO } from '../../classes/dao.js';
+import { DAO } from '../../classes/actors/dao.js';
const POOL_DURATION_MS = 100;
const DEFAULT_DELAY_MS = 100;
@@ -15,8 +15,8 @@ let dao;
async function newExpert() {
const index = experts.length;
const name = `Expert${index + 1}`;
- const expert = await new Expert(dao, name).initialize();
- expert.addValue('rep', () => dao.reputation.valueOwnedBy(expert.reputationPublicKey));
+ const expert = await new Expert(dao, name, scene).initialize();
+ await expert.addComputedValue('rep', () => dao.reputation.valueOwnedBy(expert.reputationPublicKey));
experts.push(expert);
return expert;
}
@@ -29,9 +29,9 @@ async function setup() {
scene.withSequenceDiagram();
scene.withTable();
- dao = new DAO('DGF');
+ dao = new DAO('DGF', scene);
- experts = (window.experts = []);
+ experts = [];
await newExpert();
await newExpert();
@@ -43,8 +43,16 @@ describe('Validation Pool', () => {
await setup();
});
+ beforeEach(async () => {
+ await scene.sequence.startSection();
+ });
+
+ afterEach(async () => {
+ await scene.sequence.endSection();
+ });
+
it('First expert can self-approve', async () => {
- scene.startSection();
+ await scene.sequence.startSection();
const { pool } = await experts[0].submitPostWithFee(new PostContent(), {
fee: 7,
duration: POOL_DURATION_MS,
@@ -64,14 +72,13 @@ describe('Validation Pool', () => {
throw e;
}
}
+ await scene.sequence.endSection();
await delay(POOL_DURATION_MS);
await pool.evaluateWinningConditions(); // Vote passes
await delay(DEFAULT_DELAY_MS);
- scene.endSection();
});
it('Failure example: second expert can not self-approve', async () => {
- scene.startSection();
try {
const { pool } = await experts[1].submitPostWithFee(new PostContent(), {
fee: 1,
@@ -84,11 +91,9 @@ describe('Validation Pool', () => {
} catch (e) {
e.message.should.match(/Quorum is not met/);
}
- scene.endSection();
});
it('Second expert must be approved by first expert', async () => {
- scene.startSection();
const { pool } = await experts[1].submitPostWithFee(new PostContent(), {
fee: 1,
duration: POOL_DURATION_MS,
@@ -102,6 +107,5 @@ describe('Validation Pool', () => {
await delay(POOL_DURATION_MS);
await pool.evaluateWinningConditions(); // Stake passes
await delay(DEFAULT_DELAY_MS);
- scene.endSection();
});
});
diff --git a/forum-network/src/tests/scripts/vm.test.js b/forum-network/src/tests/scripts/vm.test.js
index 69e4c8d..93baac5 100644
--- a/forum-network/src/tests/scripts/vm.test.js
+++ b/forum-network/src/tests/scripts/vm.test.js
@@ -1,22 +1,31 @@
-import { Actor } from '../../classes/actor.js';
-import { Box } from '../../classes/box.js';
-import { Scene } from '../../classes/scene.js';
-import { VM } from '../../classes/vm.js';
+import { Actor } from '../../classes/display/actor.js';
+import { Box } from '../../classes/display/box.js';
+import { Scene } from '../../classes/display/scene.js';
+import { VM } from '../../classes/supporting/vm.js';
-const rootElement = document.getElementById('scene');
-const rootBox = new Box('rootBox', rootElement).flex();
-window.scene = new Scene('VM test', rootBox).withSequenceDiagram();
+const contractIds = ['contract-id-1', 'contract-id-2'];
-const testContractId = 'test-contract-id';
-class TestContract extends Actor {
- constructor(vm, value) {
- super('TestContract');
+class Greeter extends Actor {
+ constructor(vm, value, scene) {
+ super('Greeter', scene);
this.vm = vm;
this.value = value;
}
- hello(sender) {
- return `${sender.name} ${this.value}`;
+ hello(sender, message) {
+ return `${sender.name} ${this.value}: ${message}`;
+ }
+}
+
+class Repeater extends Actor {
+ constructor(vm, greeter, scene) {
+ super('Repeater', scene);
+ this.vmHandle = vm.getHandle(this);
+ this.greeter = greeter;
+ }
+
+ forward(__sender, method, message) {
+ return this.vmHandle.callContract(this.greeter, method, message);
}
}
@@ -24,19 +33,38 @@ describe('VM', () => {
let vm;
let sender;
let vmHandle;
+ let scene;
before(() => {
- vm = (window.vm = new VM());
- sender = new Actor('Sender');
- vm.addContract(testContractId, TestContract, 'world');
+ const rootElement = document.getElementById('scene');
+ const rootBox = new Box('rootBox', rootElement).flex();
+ scene = new Scene('VM test', rootBox).withSequenceDiagram();
+ vm = new VM(scene);
+ sender = new Actor('Sender', scene);
+ vm.addContract(contractIds[0], Greeter, 'world', scene);
+ vm.addContract(contractIds[1], Repeater, contractIds[0], scene);
vmHandle = vm.getHandle(sender);
});
+ beforeEach(async () => {
+ await scene.sequence.startSection();
+ });
+
+ afterEach(async () => {
+ await scene.sequence.endSection();
+ });
+
it('Should exist', () => {
should.exist(vm);
});
- it('Call a contract method', () => {
- vmHandle.callContract(testContractId, 'hello').should.equal('Sender world');
+ it('Call a contract method', async () => {
+ (await Promise.resolve(vmHandle.callContract(contractIds[0], 'hello', 'good morning')))
+ .should.equal('Sender world: good morning');
+ });
+
+ it('Call a contract method which calls another contract method', async () => {
+ (await Promise.resolve(vmHandle.callContract(contractIds[1], 'forward', 'hello', 'good day')))
+ .should.equal('Repeater world: good day');
});
});
diff --git a/forum-network/src/tests/scripts/wdag.test.js b/forum-network/src/tests/scripts/wdag.test.js
index 34bf055..35f453b 100644
--- a/forum-network/src/tests/scripts/wdag.test.js
+++ b/forum-network/src/tests/scripts/wdag.test.js
@@ -1,6 +1,6 @@
-import { Box } from '../../classes/box.js';
-import { Scene } from '../../classes/scene.js';
-import { WDAG } from '../../classes/wdag.js';
+import { Box } from '../../classes/display/box.js';
+import { Scene } from '../../classes/display/scene.js';
+import { WDAG } from '../../classes/supporting/wdag.js';
const rootElement = document.getElementById('scene');
const rootBox = new Box('rootBox', rootElement).flex();
diff --git a/forum-network/src/tests/validation-pool.test.html b/forum-network/src/tests/validation-pool.test.html
index 72c90e1..7afaac2 100644
--- a/forum-network/src/tests/validation-pool.test.html
+++ b/forum-network/src/tests/validation-pool.test.html
@@ -15,8 +15,12 @@
-
-
+
+