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 @@
- 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 @@ + + +
+
+ + + +
+ 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 @@ - - + +