diff --git a/forum-network/notes/rep.md b/forum-network/notes/rep.md index 8e5f445..018ca2c 100644 --- a/forum-network/notes/rep.md +++ b/forum-network/notes/rep.md @@ -48,3 +48,5 @@ If this is the only scenario in which new rep tokens are minted, and from then o then we probably want each token to store information about the history of its value. At a minimum this can be a list where each item includes the identifier of the corresponding validation pool, and the resulting increment/decrement of token value. Each validation pool can then keep a record of the reputation staked by each voter, and the identifier of the corresponding post. + +--- diff --git a/forum-network/src/classes/actor.js b/forum-network/src/classes/actor.js index d632297..8556ef7 100644 --- a/forum-network/src/classes/actor.js +++ b/forum-network/src/classes/actor.js @@ -6,6 +6,7 @@ export class Actor { this.status = this.scene.addDisplayValue(`${this.name} status`); this.status.set('Created'); this.values = new Map(); + this.valueFunctions = new Map(); this.active = 0; this.scene.registerActor(this); } @@ -50,13 +51,16 @@ export class Actor { return this; } - addValue(label) { + addValue(label, fn) { this.values.set(label, this.scene.addDisplayValue(`${this.name} ${label}`)); + if (fn) { + this.valueFunctions.set(label, fn); + } return this; } async setValue(label, value) { - if (typeof value === 'number') { + if (typeof value === 'number' && value.toString().length > 6) { value = value.toFixed(2); } let displayValue = this.values.get(label); @@ -70,4 +74,26 @@ export class Actor { displayValue.set(value); 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.toString().length > 6) { + value = value.toFixed(2); + } + if (value !== displayValue.get()) { + await this.scene.sequence.log(`note over ${this.name} : ${label} = ${value}`); + } + displayValue.set(value); + } + } + + getValuesMap() { + return new Map(Array.from(this.values.entries()) + .map(([key, displayValue]) => [key, { + name: displayValue.getName(), + value: displayValue.get(), + }])); + } } diff --git a/forum-network/src/classes/display-value.js b/forum-network/src/classes/display-value.js index 88c9173..7f5b48d 100644 --- a/forum-network/src/classes/display-value.js +++ b/forum-network/src/classes/display-value.js @@ -20,4 +20,8 @@ export class DisplayValue { get() { return this.value; } + + getName() { + return this.name; + } } diff --git a/forum-network/src/classes/scene.js b/forum-network/src/classes/scene.js index e6c0ce0..755a779 100644 --- a/forum-network/src/classes/scene.js +++ b/forum-network/src/classes/scene.js @@ -38,6 +38,50 @@ class MermaidDiagram { } } +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; @@ -46,6 +90,7 @@ export class Scene { 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').flex(); this.box.addBox('Spacer').setInnerHTML(' '); this.actors = new Set(); @@ -86,6 +131,16 @@ export class Scene { return this; } + withTable() { + if (this.table) { + return this; + } + const box = this.middleSection.addBox('Table'); + this.box.addBox('Spacer').setInnerHTML(' '); + this.table = new Table(box); + return this; + } + async addActor(name) { const actor = new Actor(name, this); if (this.sequence) { @@ -130,4 +185,22 @@ export class Scene { this.sequence.inSection--; this.sequence.log('end'); } + + stateToTable(label) { + const row = new Map(); + const columns = []; + columns.push({ key: 'seqNum', title: '#' }); + row.set('seqNum', this.table.rows.length + 1); + 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/validation-pool.js b/forum-network/src/classes/validation-pool.js index 9bbea84..5738882 100644 --- a/forum-network/src/classes/validation-pool.js +++ b/forum-network/src/classes/validation-pool.js @@ -85,6 +85,11 @@ export class ValidationPool extends ReputationHolder { amount: this.mintedValue * (1 - params.stakeForAuthor), tokenId: this.tokenId, }); + + // Keep a record of voters and their votes + const voter = this.bench.voters.get(reputationPublicKey) ?? new Voter(reputationPublicKey); + voter.addVoteRecord(this); + this.bench.voters.set(reputationPublicKey, voter); } getTokenLossRatio() { @@ -171,6 +176,12 @@ export class ValidationPool extends ReputationHolder { voter.addVoteRecord(this); this.bench.voters.set(reputationPublicKey, voter); } + + // Update computed display values + for (const voter of this.bench.voters.values()) { + const actor = this.scene.findActor((a) => a.reputationPublicKey === voter.reputationPublicKey); + await actor.computeValues(); + } } applyTokenLocking() { @@ -266,5 +277,17 @@ export class ValidationPool extends ReputationHolder { } console.log('pool complete'); + + // Update computed display values + for (const voter of this.bench.voters.values()) { + const actor = this.scene.findActor((a) => a.reputationPublicKey === voter.reputationPublicKey); + await actor.computeValues(); + } + await this.bench.computeValues(); + if (this.forum) { + await this.forum.computeValues(); + } + + this.scene.stateToTable(`validation pool ${this.name} complete`); } } diff --git a/forum-network/src/index.css b/forum-network/src/index.css index a8f687b..98c570b 100644 --- a/forum-network/src/index.css +++ b/forum-network/src/index.css @@ -32,7 +32,10 @@ a:visited { svg { width: 800px; } -.hidden { - /* display: none; */ - /* visibility: hidden; */ +th { + text-align: left; + padding: 10px; +} +td { + background-color: #0c2025; } diff --git a/forum-network/src/tests/availability.html b/forum-network/src/tests/availability.html index 56509bb..ac9ffcb 100644 --- a/forum-network/src/tests/availability.html +++ b/forum-network/src/tests/availability.html @@ -26,6 +26,7 @@ const scene = (window.scene = new Scene('Availability test', rootBox)); scene.withSequenceDiagram(); scene.withFlowchart(); + scene.withTable(); const experts = (window.experts = []); const newExpert = async () => { diff --git a/forum-network/src/tests/forum.html b/forum-network/src/tests/forum.html index 0fad2c4..0a2d596 100644 --- a/forum-network/src/tests/forum.html +++ b/forum-network/src/tests/forum.html @@ -24,6 +24,7 @@ const scene = (window.scene = new Scene('Forum test', rootBox)); scene.withSequenceDiagram(); scene.withFlowchart(); + scene.withTable(); scene.addDisplayValue('c3. stakeForAuthor').set(params.stakeForAuthor); scene.addDisplayValue('q2. revaluationLimit').set(params.revaluationLimit); @@ -48,22 +49,19 @@ const expert2 = await newExpert(); const expert3 = await newExpert(); - const updateDisplayValues = async () => { - for (const expert of experts) { - await expert.setValue( - 'rep', - bench.reputation.valueOwnedBy(expert.reputationPublicKey), - ); - } - await bench.setValue('total rep', bench.reputation.getTotal()); - await forum.setValue('total value', forum.getTotalValue()); - }; + bench.addValue('total rep', () => bench.reputation.getTotal()); + forum.addValue('total value', () => forum.getTotalValue()); + + for (const expert of experts) { + expert.addValue('rep', () => bench.reputation.valueOwnedBy(expert.reputationPublicKey)); + } const updateDisplayValuesAndDelay = async (delayMs) => { - await updateDisplayValues(); await delay(delayMs ?? DEFAULT_DELAY_INTERVAL); }; + scene.stateToTable('Initial state'); + await updateDisplayValuesAndDelay(); await scene.startSection(); diff --git a/forum-network/src/tests/validation-pool.html b/forum-network/src/tests/validation-pool.html index 4f9cc60..e776d49 100644 --- a/forum-network/src/tests/validation-pool.html +++ b/forum-network/src/tests/validation-pool.html @@ -19,7 +19,8 @@ const rootBox = new Box('rootBox', rootElement).flex(); const scene = (window.scene = new Scene('Validation Pool test', rootBox)); - await scene.withSequenceDiagram(); + scene.withSequenceDiagram(); + scene.withTable(); const expert1 = (window.expert1 = await new Expert( 'Expert1', scene, @@ -31,25 +32,6 @@ const forum = (window.forum = new Forum('Forum', scene)); const bench = (window.bench = new Bench(forum, 'Bench', scene)); - const updateDisplayValues = async () => { - await expert1.setValue( - 'rep', - bench.reputation.valueOwnedBy(expert1.reputationPublicKey), - ); - await expert2.setValue( - 'rep', - bench.reputation.valueOwnedBy(expert2.reputationPublicKey), - ); - await bench.setValue('total rep', bench.reputation.getTotal()); - // With params.lockingTimeExponent = 0 and params.activeVoterThreshold = null, - // these next 3 propetries are all equal to total rep - // await bench.setValue('available rep', bench.reputation.getTotalAvailable()); - // await bench.setValue('active rep', bench.getActiveReputation()); - // await bench.setValue('active available rep', bench.getActiveAvailableReputation()); - await scene.sequence.render(); - }; - - updateDisplayValues(); await delay(1000); // First expert can self-approve @@ -75,7 +57,6 @@ } await delay(1000); await pool.evaluateWinningConditions(); // Vote passes - await updateDisplayValues(); await delay(1000); } @@ -88,7 +69,6 @@ }); await delay(1000); await pool.evaluateWinningConditions(); // Quorum not met! - await updateDisplayValues(); await delay(1000); } catch (e) { if (e.message.match(/Quorum is not met/)) { @@ -113,13 +93,8 @@ }); await delay(1000); await pool.evaluateWinningConditions(); // Stake passes - await updateDisplayValues(); await delay(1000); } - await updateDisplayValues(); - scene.deactivateAll(); - - await updateDisplayValues();