eslint fixes

This commit is contained in:
Ladd Hoffman 2022-12-31 16:08:42 -06:00
parent d63a93562f
commit 59c10f1ac2
21 changed files with 202 additions and 159 deletions

View File

@ -3,13 +3,27 @@ module.exports = {
browser: true, browser: true,
es2021: true, es2021: true,
}, },
extends: 'airbnb-base', extends: ['airbnb-base'],
overrides: [], overrides: [],
parserOptions: { parserOptions: {
ecmaVersion: 'latest', ecmaVersion: 'latest',
sourceType: 'module', sourceType: 'module',
}, },
// plugins: ["import"],
rules: { rules: {
'import/extenstions': ['always'], 'import/extensions': ['error', 'always'],
'import/prefer-default-export': ['off'],
'import/no-unresolved': ['error', { ignore: ['^http'] }],
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'max-classes-per-file': ['off'],
'no-param-reassign': ['off'],
'no-plusplus': ['off'],
'no-restricted-syntax': ['off'],
'max-len': ['warn', 120],
'no-console': ['off'],
'no-return-assign': ['off'],
'no-multi-assign': ['off'],
'no-constant-condition': ['off'],
'no-await-in-loop': ['off'],
}, },
}; };

5
forum-network/notes.md Normal file
View File

@ -0,0 +1,5 @@
# Challenges
- Receiving payments
- Distributing payments to participants
- Computing updates to forum graph

View File

@ -1,5 +1,5 @@
import { Box } from "./classes/box.js"; import { Box } from './classes/box.js';
import { Scene } from "./classes/scene.js"; import { Scene } from './classes/scene.js';
const rootElement = document.getElementById('basic'); const rootElement = document.getElementById('basic');
const rootBox = new Box('rootBox', rootElement).flex(); const rootBox = new Box('rootBox', rootElement).flex();
@ -11,7 +11,9 @@ function randomDelay(min, max) {
function delay(min, max = min) { function delay(min, max = min) {
const delayMs = min + Math.random() * (max - min); const delayMs = min + Math.random() * (max - min);
return new Promise((resolve) => setTimeout(resolve, delayMs)); return new Promise((resolve) => {
setTimeout(resolve, delayMs);
});
} }
if (true) { if (true) {
@ -46,7 +48,7 @@ if (true) {
}, randomDelay(500, 1500)); }, randomDelay(500, 1500));
}); });
blockchain.on(readBlockchainData, (src, detail) => { blockchain.on(readBlockchainData, (src, _detail) => {
blockchainStatus.set('Processing request'); blockchainStatus.set('Processing request');
setTimeout(() => { setTimeout(() => {
blockchain.send(src, blockchainData, {}); blockchain.send(src, blockchainData, {});
@ -64,7 +66,7 @@ if (true) {
}, randomDelay(6000, 12000)); }, randomDelay(6000, 12000));
} }
(async function () { (async function run() {
const scene = new Scene('Scene 2', rootBox); const scene = new Scene('Scene 2', rootBox);
const webClient = scene.addActor('webClient'); const webClient = scene.addActor('webClient');
@ -162,12 +164,12 @@ if (true) {
}); });
node.on(memoryData, (_src, _data) => { node.on(memoryData, (_src, _data) => {
node.on(storageData, (_src, _data) => { node.on(storageData, (__src, __data) => {
if (detail?.readConcern === 'single') { if (detail?.readConcern === 'single') {
node.send(node, considerInfo, {}); node.send(node, considerInfo, {});
} else { } else {
const peer = getPeer(node); const peer = getPeer(node);
node.on(qualifiedOpinions, (_src, info) => { node.on(qualifiedOpinions, (___src, info) => {
node.send(node, considerInfo, info); node.send(node, considerInfo, info);
}); });
node.send(peer, seekTruth, { readConcern: 'single' }); node.send(peer, seekTruth, { readConcern: 'single' });

View File

@ -0,0 +1,6 @@
import { Actor } from './actor.js';
/**
* Purpose: Enable staking reputation to enter the pool of workers
*/
export class Availability extends Actor {}

View File

@ -1,9 +1,12 @@
import { Actor } from "./actor.js"; import { Actor } from './actor.js';
import { Reputations } from "./reputation.js"; import { Reputations } from './reputation.js';
import { ValidationPool } from "./validation-pool.js"; import { ValidationPool } from './validation-pool.js';
import params from "./params.js"; import params from './params.js';
import { Action } from "./action.js"; import { Action } from './action.js';
/**
* Purpose: Keep track of reputation holders
*/
export class Bench extends Actor { export class Bench extends Actor {
constructor(name, scene) { constructor(name, scene) {
super(name, scene); super(name, scene);
@ -12,7 +15,7 @@ export class Bench extends Actor {
this.reputations = new Reputations(); this.reputations = new Reputations();
this.actions = { this.actions = {
createValidationPool: new Action("create validation pool", scene), createValidationPool: new Action('create validation pool', scene),
}; };
this.activate(); this.activate();
@ -27,8 +30,7 @@ export class Bench extends Actor {
const thresholdSet = !!params.activeVoterThreshold; const thresholdSet = !!params.activeVoterThreshold;
return Array.from(this.voters.values()).filter((voter) => { return Array.from(this.voters.values()).filter((voter) => {
const hasVoted = !!voter.dateLastVote; const hasVoted = !!voter.dateLastVote;
const withinThreshold = const withinThreshold = now - voter.dateLastVote >= params.activeVoterThreshold;
now - voter.dateLastVote >= params.activeVoterThreshold;
return hasVoted && (!thresholdSet || withinThreshold); return hasVoted && (!thresholdSet || withinThreshold);
}); });
} }
@ -43,17 +45,13 @@ export class Bench extends Actor {
getTotalActiveReputation() { getTotalActiveReputation() {
return this.listActiveVoters() return this.listActiveVoters()
.map(({ reputationPublicKey }) => .map(({ reputationPublicKey }) => this.reputations.getTokens(reputationPublicKey))
this.reputations.getTokens(reputationPublicKey)
)
.reduce((acc, cur) => (acc += cur), 0); .reduce((acc, cur) => (acc += cur), 0);
} }
getTotalActiveAvailableReputation() { getTotalActiveAvailableReputation() {
return this.listActiveVoters() return this.listActiveVoters()
.map(({ reputationPublicKey }) => .map(({ reputationPublicKey }) => this.reputations.getAvailableTokens(reputationPublicKey))
this.reputations.getAvailableTokens(reputationPublicKey)
)
.reduce((acc, cur) => (acc += cur), 0); .reduce((acc, cur) => (acc += cur), 0);
} }
@ -66,7 +64,7 @@ export class Bench extends Actor {
tokenLossRatio, tokenLossRatio,
contentiousDebate, contentiousDebate,
signingPublicKey, signingPublicKey,
} },
) { ) {
const validationPoolNumber = this.validationPools.size + 1; const validationPoolNumber = this.validationPools.size + 1;
const validationPool = new ValidationPool( const validationPool = new ValidationPool(
@ -81,7 +79,7 @@ export class Bench extends Actor {
signingPublicKey, signingPublicKey,
}, },
`pool${validationPoolNumber}`, `pool${validationPoolNumber}`,
scene this.scene,
); );
this.validationPools.set(validationPool.id, validationPool); this.validationPools.set(validationPool.id, validationPool);
this.actions.createValidationPool.log(this, validationPool); this.actions.createValidationPool.log(this, validationPool);

View File

@ -1,4 +1,4 @@
import {DisplayValue} from "./display-value.js"; import { DisplayValue } from './display-value.js';
export class Box { export class Box {
constructor(name, parentEl, elementType = 'div') { constructor(name, parentEl, elementType = 'div') {
@ -47,7 +47,7 @@ export class Box {
} }
setId(id) { setId(id) {
this.el.id = (id || this.name).replace(/ /g, ""); this.el.id = (id || this.name).replace(/ /g, '');
return this; return this;
} }

View File

@ -0,0 +1,10 @@
import { Actor } from './actor.js';
/**
* Purpose: Enable fee-driven work requests, to be completed by workers from the availability pool
*/
export class Business extends Actor {
// constructor(name, scene) { super(name, scene); }
// submitRequest(fee) {}
}

View File

@ -4,7 +4,7 @@ export class CryptoUtil {
static hash = 'SHA-256'; static hash = 'SHA-256';
static async generateAsymmetricKey() { static async generateAsymmetricKey() {
return await window.crypto.subtle.generateKey( return window.crypto.subtle.generateKey(
{ {
name: CryptoUtil.algorithm, name: CryptoUtil.algorithm,
hash: CryptoUtil.hash, hash: CryptoUtil.hash,
@ -32,7 +32,7 @@ export class CryptoUtil {
// TODO: make a single TextEncoder instance and reuse it // TODO: make a single TextEncoder instance and reuse it
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const encoded = encoder.encode(content); const encoded = encoder.encode(content);
return await window.crypto.subtle.verify(CryptoUtil.algorithm, publicKey, signature, encoded); return window.crypto.subtle.verify(CryptoUtil.algorithm, publicKey, signature, encoded);
} }
static async exportKey(publicKey) { static async exportKey(publicKey) {
@ -44,7 +44,7 @@ export class CryptoUtil {
static async importKey(b64jwk) { static async importKey(b64jwk) {
// Convert base64 javascript web key to CryptoKey // Convert base64 javascript web key to CryptoKey
const jwk = JSON.parse(atob(b64jwk)); const jwk = JSON.parse(atob(b64jwk));
return await window.crypto.subtle.importKey( return window.crypto.subtle.importKey(
'jwk', 'jwk',
jwk, jwk,
{ {

View File

@ -1,6 +1,8 @@
import { Actor } from './actor.js'; import { Actor } from './actor.js';
import { Action } from './action.js'; import { Action } from './action.js';
import { Message, PostMessage, PeerMessage } from './message.js'; import {
Message, PostMessage, PeerMessage, messageFromJSON,
} from './message.js';
import { CryptoUtil } from './crypto.js'; import { CryptoUtil } from './crypto.js';
import { ForumView } from './forum-view.js'; import { ForumView } from './forum-view.js';
import { PrioritizedQueue } from './prioritized-queue.js'; import { PrioritizedQueue } from './prioritized-queue.js';
@ -51,7 +53,7 @@ export class ForumNode extends Actor {
if (!messageJson) { if (!messageJson) {
return null; return null;
} }
await this.processMessage(messageJson); return this.processMessage(messageJson);
} }
// Process a message from the queue // Process a message from the queue
@ -65,7 +67,7 @@ export class ForumNode extends Actor {
} }
const { publicKey } = messageJson; const { publicKey } = messageJson;
const message = Message.fromJSON(messageJson); const message = messageFromJSON(messageJson);
console.log(`${this.name}: processMessage`, message); console.log(`${this.name}: processMessage`, message);
if (message instanceof PostMessage) { if (message instanceof PostMessage) {

View File

@ -34,7 +34,7 @@ export class ForumView {
this.reputations.set(id, reputation); this.reputations.set(id, reputation);
} }
incrementReputation(publicKey, increment, reason) { incrementReputation(publicKey, increment, _reason) {
const reputation = this.getReputation(publicKey) || 0; const reputation = this.getReputation(publicKey) || 0;
return this.reputations.set(publicKey, reputation + increment); return this.reputations.set(publicKey, reputation + increment);
} }
@ -88,7 +88,8 @@ export class ForumView {
// in order to arrive at the current view. // in order to arrive at the current view.
// When an author stakes reputation on a post, if it's a non-binding stake, then it merely expresses opinion. // When an author stakes reputation on a post, if it's a non-binding stake, then it merely expresses opinion.
// If it's a binding stake, then they may lose the staked reputation as a result of other posts staking reputation against theirs. // If it's a binding stake, then they may lose the staked reputation as a result of other posts
// staking reputation against theirs.
citationFraction = 0.3; citationFraction = 0.3;
@ -104,7 +105,8 @@ export class ForumView {
// Some of the incoming reputation gets distributed among cited posts // Some of the incoming reputation gets distributed among cited posts
const distributeAmongCitations = amount * this.citationFraction; const distributeAmongCitations = amount * this.citationFraction;
// citation weights can be interpreted as a ratio, or we can somehow constrain the input to add up to some specified total. // citation weights can be interpreted as a ratio, or we can somehow constrain the input
// to add up to some specified total.
// It's easy enough to let them be on any arbitrary scale and just compute the ratios here. // It's easy enough to let them be on any arbitrary scale and just compute the ratios here.
const totalWeight = post.citations const totalWeight = post.citations
?.map(({ weight }) => weight) ?.map(({ weight }) => weight)

View File

@ -1,9 +1,10 @@
import { Actor } from "./actor.js"; import { Actor } from './actor.js';
import { Graph, Vertex } from "./graph.js"; import { Graph, Vertex } from './graph.js';
import params from "./params.js"; import params from './params.js';
class Post extends Vertex { class Post extends Vertex {
constructor(forum, id, authorId, citations) { constructor(forum, id, authorId, citations) {
super();
this.forum = forum; this.forum = forum;
this.id = id; this.id = id;
this.authorId = authorId; this.authorId = authorId;
@ -17,6 +18,9 @@ class Post extends Vertex {
} }
} }
/**
* Purpose: Maintain a directed, acyclic, weighted graph of posts referencing other posts
*/
export class Forum extends Actor { export class Forum extends Actor {
constructor(bench, name, scene) { constructor(bench, name, scene) {
super(name, scene); super(name, scene);
@ -24,11 +28,13 @@ export class Forum extends Actor {
this.posts = new Graph(); this.posts = new Graph();
} }
async addPost({ authorId, postId, citations = [], poolParams }) { async addPost({
authorId, postId, citations = [], poolParams,
}) {
const post = new Post(this, postId, authorId); const post = new Post(this, postId, authorId);
this.posts.addVertex(postId, post); this.posts.addVertex(postId, post);
for (const { postId: citedPostId, weight } of citations) { for (const { postId: citedPostId, weight } of citations) {
this.posts.addEdge("citation", postId, citedPostId, { weight }); this.posts.addEdge('citation', postId, citedPostId, { weight });
} }
// this.applyReputationEffects(post); // this.applyReputationEffects(post);
// initiateValidationPool(authorId, {postId, fee, duration, tokenLossRatio, contentiousDebate, signingPublicKey}) { // initiateValidationPool(authorId, {postId, fee, duration, tokenLossRatio, contentiousDebate, signingPublicKey}) {
@ -48,7 +54,7 @@ export class Forum extends Actor {
} }
distributeReputation(post, amount, depth = 0) { distributeReputation(post, amount, depth = 0) {
console.log("distributeReputation", { post, amount, depth }); console.log('distributeReputation', { post, amount, depth });
// Add the given value to the current post // Add the given value to the current post
post.value += amount; post.value += amount;
// Distribute a fraction of the added value among cited posts // Distribute a fraction of the added value among cited posts
@ -63,17 +69,17 @@ export class Forum extends Actor {
for (const { for (const {
to: citedPostId, to: citedPostId,
data: { weight }, data: { weight },
} of post.getEdges("citation", true)) { } of post.getEdges('citation', true)) {
const citedPost = this.getPost(citedPostId); const citedPost = this.getPost(citedPostId);
if (!citedPost) { if (!citedPost) {
throw new Error( throw new Error(
`Post ${post.postId} cites unknown post ${citedPostId}` `Post ${post.postId} cites unknown post ${citedPostId}`,
); );
} }
this.distributeReputation( this.distributeReputation(
citedPost, citedPost,
(weight / totalWeight) * distributeAmongCitations, (weight / totalWeight) * distributeAmongCitations,
depth + 1 depth + 1,
); );
} }
} }

View File

@ -8,8 +8,8 @@ export class Vertex {
} }
getEdges(label, away) { getEdges(label, away) {
return this.edges[away ? "from" : "to"].filter( return this.edges[away ? 'from' : 'to'].filter(
(edge) => edge.label === label (edge) => edge.label === label,
); );
} }
} }
@ -34,7 +34,7 @@ export class Graph {
addVertex(id, data) { addVertex(id, data) {
// Support simple case of auto-incremented numeric ids // Support simple case of auto-incremented numeric ids
if (typeof id === "object") { if (typeof id === 'object') {
data = id; data = id;
id = this.nextVertexId++; id = this.nextVertexId++;
} }
@ -85,8 +85,7 @@ export class Graph {
return edgeLabels.flatMap((edgeLabel) => { return edgeLabels.flatMap((edgeLabel) => {
const edges = this.edgeLabels.get(edgeLabel); const edges = this.edgeLabels.get(edgeLabel);
return Array.from(edges?.values() || []).filter((edge) => { return Array.from(edges?.values() || []).filter((edge) => {
const matchFrom = const matchFrom = from === null || from === undefined || from === edge.from;
from === null || from === undefined || from === edge.from;
const matchTo = to === null || to === undefined || to === edge.to; const matchTo = to === null || to === undefined || to === edge.to;
return matchFrom && matchTo; return matchFrom && matchTo;
}); });

View File

@ -1,16 +1,17 @@
import { Actor } from "./actor.js"; import { Actor } from './actor.js';
import { Action } from "./action.js";
import { PostMessage } from "./message.js"; import { Action } from './action.js';
import { CryptoUtil } from "./crypto.js"; import { PostMessage } from './message.js';
import { CryptoUtil } from './crypto.js';
export class Member extends Actor { export class Member extends Actor {
constructor(name, scene) { constructor(name, scene) {
super(name, scene); super(name, scene);
this.actions = { this.actions = {
submitPost: new Action("submit post", scene), submitPost: new Action('submit post', scene),
initiateValidationPool: new Action("initiate validation pool", scene), initiateValidationPool: new Action('initiate validation pool', scene),
castVote: new Action("cast vote", scene), castVote: new Action('cast vote', scene),
revealIdentity: new Action("reveal identity", scene), revealIdentity: new Action('reveal identity', scene),
}; };
this.validationPools = new Map(); this.validationPools = new Map();
} }
@ -19,7 +20,7 @@ export class Member extends Actor {
this.reputationKey = await CryptoUtil.generateAsymmetricKey(); this.reputationKey = await CryptoUtil.generateAsymmetricKey();
// this.reputationPublicKey = await CryptoUtil.exportKey(this.reputationKey.publicKey); // this.reputationPublicKey = await CryptoUtil.exportKey(this.reputationKey.publicKey);
this.reputationPublicKey = this.name; this.reputationPublicKey = this.name;
this.status.set("Initialized"); this.status.set('Initialized');
this.activate(); this.activate();
return this; return this;
} }
@ -40,7 +41,7 @@ export class Member extends Actor {
this.actions.initiateValidationPool.log( this.actions.initiateValidationPool.log(
this, this,
bench, bench,
`(fee: ${options.fee})` `(fee: ${options.fee})`,
); );
const pool = bench.initiateValidationPool(this.reputationPublicKey, { const pool = bench.initiateValidationPool(this.reputationPublicKey, {
...options, ...options,
@ -59,7 +60,7 @@ export class Member extends Actor {
this.actions.castVote.log( this.actions.castVote.log(
this, this,
validationPool, validationPool,
`(${position ? "for" : "against"}, stake: ${stake})` `(${position ? 'for' : 'against'}, stake: ${stake})`,
); );
validationPool.castVote(signingPublicKey, position, stake, lockingTime); validationPool.castVote(signingPublicKey, position, stake, lockingTime);
} }

View File

@ -14,23 +14,17 @@ export class Message {
} }
static async verify({ content, publicKey, signature }) { static async verify({ content, publicKey, signature }) {
return await CryptoUtil.verify(content, publicKey, signature); return CryptoUtil.verify(content, publicKey, signature);
} }
static contentFromJSON(data) { static contentFromJSON(data) {
return data; return data;
} }
contentToJSON(content) { static contentToJSON(content) {
return content; return content;
} }
static fromJSON({ type, content }) {
const messageType = messageTypes.get(type) || Message;
const messageContent = messageType.contentFromJSON(content);
return new messageType(messageContent);
}
toJSON() { toJSON() {
return { return {
type: this.type, type: this.type,
@ -51,7 +45,7 @@ export class PostMessage extends Message {
}; };
} }
contentToJSON({ post, stake }) { static contentToJSON({ post, stake }) {
return { return {
post: post.toJSON(), post: post.toJSON(),
stake, stake,
@ -67,3 +61,9 @@ const messageTypes = new Map([
['post', PostMessage], ['post', PostMessage],
['peer', PeerMessage], ['peer', PeerMessage],
]); ]);
export const messageFromJSON = ({ type, content }) => {
const MessageType = messageTypes.get(type) || Message;
const messageContent = MessageType.contentFromJSON(content);
return new MessageType(messageContent);
};

View File

@ -1,4 +1,22 @@
import { CryptoUtil } from "./crypto.js"; import { CryptoUtil } from './crypto.js';
export class Citation {
constructor(postId, weight) {
this.postId = postId;
this.weight = weight;
}
toJSON() {
return {
postId: this.postId,
weight: this.weight,
};
}
static fromJSON({ postId, weight }) {
return new Citation(postId, weight);
}
}
export class Post { export class Post {
constructor(content) { constructor(content) {
@ -28,21 +46,3 @@ export class Post {
return post; return post;
} }
} }
export class Citation {
constructor(postId, weight) {
this.postId = postId;
this.weight = weight;
}
toJSON() {
return {
postId: this.postId,
weight: this.weight,
};
}
static fromJSON({ postId, weight }) {
return new Citation(postId, weight);
}
}

View File

@ -14,14 +14,14 @@ class Reputation {
addTokens(tokens) { addTokens(tokens) {
if (this.tokens + tokens < 0) { if (this.tokens + tokens < 0) {
throw new Error(`Token balance can not become negative`); throw new Error('Token balance can not become negative');
} }
this.tokens += tokens; this.tokens += tokens;
} }
lockTokens(tokens, duration) { lockTokens(tokens, duration) {
if (tokens > this.getAvailableTokens()) { if (tokens > this.getAvailableTokens()) {
throw new Error("Can not lock more tokens than are available") throw new Error('Can not lock more tokens than are available');
} }
const lock = new Lock(tokens, duration); const lock = new Lock(tokens, duration);
this.locks.add(lock); this.locks.add(lock);
@ -35,10 +35,10 @@ class Reputation {
getAvailableTokens() { getAvailableTokens() {
const now = new Date(); const now = new Date();
const tokensLocked = Array.from(this.locks.values()) const tokensLocked = Array.from(this.locks.values())
.filter(({dateCreated, duration}) => now - dateCreated < duration) .filter(({ dateCreated, duration }) => now - dateCreated < duration)
.reduce((acc, cur) => acc += cur.tokens, 0); .reduce((acc, cur) => acc += cur.tokens, 0);
if (tokensLocked > this.tokens) { if (tokensLocked > this.tokens) {
throw new Error("Assertion failure. tokensLocked > tokens"); throw new Error('Assertion failure. tokensLocked > tokens');
} }
return this.tokens - tokensLocked; return this.tokens - tokensLocked;
} }

View File

@ -1,25 +1,25 @@
import { Actor } from "./actor.js"; import mermaid from 'https://unpkg.com/mermaid@9.2.2/dist/mermaid.esm.min.mjs';
import { Action } from "./action.js"; import { Actor } from './actor.js';
import { debounce } from "../util.js"; import { Action } from './action.js';
import mermaid from "https://unpkg.com/mermaid@9.2.2/dist/mermaid.esm.min.mjs"; import { debounce } from '../util.js';
export class Scene { export class Scene {
constructor(name, rootBox) { constructor(name, rootBox) {
this.name = name; this.name = name;
this.box = rootBox.addBox(name); this.box = rootBox.addBox(name);
this.titleBox = this.box.addBox().setInnerHTML(name); this.titleBox = this.box.addBox().setInnerHTML(name);
this.box.addBox("Spacer").setInnerHTML("&nbsp;"); this.box.addBox('Spacer').setInnerHTML('&nbsp;');
this.displayValuesBox = this.box.addBox(`${this.name}-values`); this.displayValuesBox = this.box.addBox(`${this.name}-values`);
this.box.addBox("Spacer").setInnerHTML("&nbsp;"); this.box.addBox('Spacer').setInnerHTML('&nbsp;');
this.actors = new Set(); this.actors = new Set();
this.seqDiagramContainer = this.box.addBox( this.seqDiagramContainer = this.box.addBox(
`${this.name}-seq-diagram-container` `${this.name}-seq-diagram-container`,
); );
this.seqDiagramElement = this.box this.seqDiagramElement = this.box
.addBox(`${this.name}-seq-diagram-element`) .addBox(`${this.name}-seq-diagram-element`)
.setId(); .setId();
this.seqDiagramBox = this.box.addBox(`${this.name}-seq-diagram`); this.seqDiagramBox = this.box.addBox(`${this.name}-seq-diagram`);
this.box.addBox("Spacer").setInnerHTML("&nbsp;"); this.box.addBox('Spacer').setInnerHTML('&nbsp;');
this.logBox = this.box.addBox(`${this.name}-log`); this.logBox = this.box.addBox(`${this.name}-log`);
mermaid.mermaidAPI.initialize({ startOnLoad: false }); mermaid.mermaidAPI.initialize({ startOnLoad: false });
this.dateLastRender = null; this.dateLastRender = null;
@ -63,7 +63,7 @@ export class Scene {
const dateStart = new Date(); const dateStart = new Date();
const graph = await mermaid.mermaidAPI.render( const graph = await mermaid.mermaidAPI.render(
this.seqDiagramElement.getId(), this.seqDiagramElement.getId(),
this.logBox.getInnerText() this.logBox.getInnerText(),
); );
this.seqDiagramBox.setInnerHTML(graph); this.seqDiagramBox.setInnerHTML(graph);
if (!this.dateLastRender) { if (!this.dateLastRender) {
@ -72,7 +72,7 @@ export class Scene {
console.log( console.log(
`renderSequenceDiagram time: ${ `renderSequenceDiagram time: ${
new Date() - dateStart new Date() - dateStart
} ms, time since last render: ${dateStart - this.dateLastRender}` } ms, time since last render: ${dateStart - this.dateLastRender}`,
); );
this.dateLastRender = dateStart; this.dateLastRender = dateStart;
}; };

View File

@ -1,14 +1,17 @@
import { CryptoUtil } from "./crypto.js"; import { CryptoUtil } from './crypto.js';
import { Vote } from "./vote.js"; import { Vote } from './vote.js';
import { Voter } from "./voter.js"; import { Voter } from './voter.js';
import { Actor } from "./actor.js"; import { Actor } from './actor.js';
import params from "./params.js"; import params from './params.js';
const ValidationPoolStates = Object.freeze({ const ValidationPoolStates = Object.freeze({
OPEN: "OPEN", OPEN: 'OPEN',
CLOSED: "CLOSED", CLOSED: 'CLOSED',
}); });
/**
* Purpose: Enable voting
*/
export class ValidationPool extends Actor { export class ValidationPool extends Actor {
constructor( constructor(
bench, bench,
@ -22,34 +25,34 @@ export class ValidationPool extends Actor {
contentiousDebate = false, contentiousDebate = false,
}, },
name, name,
scene scene,
) { ) {
super(name, scene); super(name, scene);
// If contentiousDebate = true, we will follow the progression defined by getTokenLossRatio() // If contentiousDebate = true, we will follow the progression defined by getTokenLossRatio()
if ( if (
!contentiousDebate && !contentiousDebate
(tokenLossRatio < 0 || && (tokenLossRatio < 0
tokenLossRatio > 1 || || tokenLossRatio > 1
[null, undefined].includes(tokenLossRatio)) || [null, undefined].includes(tokenLossRatio))
) { ) {
throw new Error( throw new Error(
`Token loss ratio must be in the range [0, 1]; got ${tokenLossRatio}` `Token loss ratio must be in the range [0, 1]; got ${tokenLossRatio}`,
); );
} }
if ( if (
duration < params.voteDuration.min || duration < params.voteDuration.min
(params.voteDuration.max && duration > params.voteDuration.max) || || (params.voteDuration.max && duration > params.voteDuration.max)
[null, undefined].includes(duration) || [null, undefined].includes(duration)
) { ) {
throw new Error( throw new Error(
`Duration must be in the range [${params.voteDuration.min}, ${ `Duration must be in the range [${params.voteDuration.min}, ${
params.voteDuration.max ?? "Inf" params.voteDuration.max ?? 'Inf'
}]; got ${duration}` }]; got ${duration}`,
); );
} }
this.postId = postId; this.postId = postId;
this.state = ValidationPoolStates.OPEN; this.state = ValidationPoolStates.OPEN;
this.setStatus("Open"); this.setStatus('Open');
this.votes = new Map(); this.votes = new Map();
this.voters = new Map(); this.voters = new Map();
this.bench = bench; this.bench = bench;
@ -76,7 +79,7 @@ export class ValidationPool extends Actor {
} }
if (this.duration && new Date() - this.dateStart > this.duration) { if (this.duration && new Date() - this.dateStart > this.duration) {
throw new Error( throw new Error(
`Validation pool ${this.id} has expired, no new votes may be cast` `Validation pool ${this.id} has expired, no new votes may be cast`,
); );
} }
this.votes.set(signingPublicKey, vote); this.votes.set(signingPublicKey, vote);
@ -85,18 +88,17 @@ export class ValidationPool extends Actor {
listVotes(position) { listVotes(position) {
return new Map( return new Map(
Array.from(this.votes.entries()).filter( Array.from(this.votes.entries()).filter(
([_, vote]) => vote.position === position ([_, vote]) => vote.position === position,
) ),
); );
} }
revealIdentity(signingPublicKey, reputationPublicKey) { revealIdentity(signingPublicKey, reputationPublicKey) {
if (!this.votes.get(signingPublicKey)) { if (!this.votes.get(signingPublicKey)) {
throw new Error("Must vote before revealing identity"); throw new Error('Must vote before revealing identity');
} }
const voter = const voter = this.bench.voters.get(reputationPublicKey)
this.bench.voters.get(reputationPublicKey) ?? ?? new Voter(reputationPublicKey);
new Voter(reputationPublicKey);
voter.addVoteRecord(this); voter.addVoteRecord(this);
this.bench.voters.set(reputationPublicKey, voter); this.bench.voters.set(reputationPublicKey, voter);
this.voters.set(signingPublicKey, voter); this.voters.set(signingPublicKey, voter);
@ -104,14 +106,14 @@ export class ValidationPool extends Actor {
// All voters have revealed their reputation public keys // All voters have revealed their reputation public keys
// Now we can evaluate winning conditions // Now we can evaluate winning conditions
this.state = ValidationPoolStates.CLOSED; this.state = ValidationPoolStates.CLOSED;
this.setStatus("Closed"); this.setStatus('Closed');
const result = this.evaluateWinningConditions(); const result = this.evaluateWinningConditions();
if (result === null) { if (result === null) {
this.setStatus("Closed - Quorum not met"); this.setStatus('Closed - Quorum not met');
this.scene.log(`note over ${this.name} : Quorum not met`); this.scene.log(`note over ${this.name} : Quorum not met`);
} else { } else {
this.setStatus(`Closed - ${result ? "Won" : "Lost"}`); this.setStatus(`Closed - ${result ? 'Won' : 'Lost'}`);
this.scene.log(`note over ${this.name} : ${result ? "Win" : "Lose"}`); this.scene.log(`note over ${this.name} : ${result ? 'Win' : 'Lose'}`);
this.applyTokenLocking(); this.applyTokenLocking();
this.distributeTokens(result); this.distributeTokens(result);
} }
@ -153,27 +155,23 @@ export class ValidationPool extends Actor {
this.bench.reputations.lockTokens( this.bench.reputations.lockTokens(
voter.reputationPublicKey, voter.reputationPublicKey,
stake, stake,
lockingTime lockingTime,
); );
// TODO: If there is an exception here, the voter may have voted incorrectly. Consider penalties. // TODO: If there is an exception here, the voter may have voted incorrectly. Consider penalties.
} }
} }
evaluateWinningConditions() { evaluateWinningConditions() {
const getVoteValue = ({ stake, lockingTime }) => const getVoteValue = ({ stake, lockingTime }) => stake * lockingTime ** params.lockingTimeExponent;
stake * Math.pow(lockingTime, params.lockingTimeExponent); const getTotalValue = (position) => Array.from(this.listVotes(position).values())
const getTotalValue = (position) =>
Array.from(this.listVotes(position).values())
.map(getVoteValue) .map(getVoteValue)
.reduce((acc, cur) => (acc += cur), 0); .reduce((acc, cur) => (acc += cur), 0);
const upvoteValue = getTotalValue(true); const upvoteValue = getTotalValue(true);
const downvoteValue = getTotalValue(false); const downvoteValue = getTotalValue(false);
const activeAvailableReputation = const activeAvailableReputation = this.bench.getTotalActiveAvailableReputation();
this.bench.getTotalActiveAvailableReputation();
const votePasses = upvoteValue >= params.winningRatio * downvoteValue; const votePasses = upvoteValue >= params.winningRatio * downvoteValue;
const quorumMet = const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation;
upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation;
return quorumMet ? votePasses : null; return quorumMet ? votePasses : null;
} }
@ -182,7 +180,6 @@ export class ValidationPool extends Actor {
// Reward the author // Reward the author
// TODO: If the vote fails, distribute tokens.author among winning voters // TODO: If the vote fails, distribute tokens.author among winning voters
if (result === true) { if (result === true) {
// console.log("awarding to author", {id: this.authorId, tokens: this.tokens.for});
this.bench.reputations.addTokens(this.authorId, this.tokens.for); this.bench.reputations.addTokens(this.authorId, this.tokens.for);
// Reward the vote winners, in proportion to their stakes // Reward the vote winners, in proportion to their stakes
const tokensForWinners = this.tokens.against; const tokensForWinners = this.tokens.against;
@ -196,7 +193,6 @@ export class ValidationPool extends Actor {
for (const [signingPublicKey, { stake }] of winningVotes.entries()) { for (const [signingPublicKey, { stake }] of winningVotes.entries()) {
const { reputationPublicKey } = this.voters.get(signingPublicKey); const { reputationPublicKey } = this.voters.get(signingPublicKey);
const reward = (tokensForWinners * stake) / totalStakes; const reward = (tokensForWinners * stake) / totalStakes;
// console.log("awarding to winning voter", {id: reputationPublicKey, tokens: reward, stake, totalStakes, tokensForWinners});
this.bench.reputations.addTokens(reputationPublicKey, reward); this.bench.reputations.addTokens(reputationPublicKey, reward);
} }
} }

View File

@ -1,13 +1,13 @@
import { Box } from './classes/box.js'; import { Box } from './classes/box.js';
import { Scene } from './classes/scene.js'; import { Scene } from './classes/scene.js';
import { debounce, delay } from "./util.js"; import { debounce, delay } from './util.js';
const rootElement = document.getElementById('debounce-test'); const rootElement = document.getElementById('debounce-test');
const rootBox = new Box('rootBox', rootElement).flex(); const rootBox = new Box('rootBox', rootElement).flex();
const scene = window.scene = new Scene('Debounce test', rootBox); const scene = window.scene = new Scene('Debounce test', rootBox);
const log = () => scene.log("event"); const log = () => scene.log('event');
debounce(log, 500); debounce(log, 500);
debounce(log, 500); debounce(log, 500);
await delay(500); await delay(500);

View File

@ -11,5 +11,7 @@ export const debounce = (fn, delay) => {
}; };
export const delay = async (ms) => { export const delay = async (ms) => {
await new Promise((resolve) => setTimeout(resolve, ms)); await new Promise((resolve) => {
setTimeout(resolve, ms);
});
}; };

View File

@ -8,9 +8,9 @@ const rootElement = document.getElementById('validation-pool');
const rootBox = new Box('rootBox', rootElement).flex(); const rootBox = new Box('rootBox', rootElement).flex();
const scene = window.scene = new Scene('Validation Pool test', rootBox).log('sequenceDiagram'); const scene = window.scene = new Scene('Validation Pool test', rootBox).log('sequenceDiagram');
const member1 = window.member1 = await new Member("Member1", scene).initialize(); const member1 = window.member1 = await new Member('Member1', scene).initialize();
const member2 = window.member2 = await new Member("Member2", scene).initialize(); const member2 = window.member2 = await new Member('Member2', scene).initialize();
const bench = window.bench = new Bench("Bench", scene); const bench = window.bench = new Bench('Bench', scene);
const updateDisplayValues = async () => { const updateDisplayValues = async () => {
member1.setValue('rep', bench.reputations.getTokens(member1.reputationPublicKey)); member1.setValue('rep', bench.reputations.getTokens(member1.reputationPublicKey));
@ -29,7 +29,7 @@ await delay(1000);
// First member can self-approve // First member can self-approve
{ {
const pool = await member1.initiateValidationPool(bench, {fee: 7, duration: 1000, tokenLossRatio: 1}); const pool = await member1.initiateValidationPool(bench, { fee: 7, duration: 1000, tokenLossRatio: 1 });
// await member1.castVote(pool, true, 0, 0); // await member1.castVote(pool, true, 0, 0);
await member1.revealIdentity(pool); // Vote passes await member1.revealIdentity(pool); // Vote passes
await updateDisplayValues(); await updateDisplayValues();
@ -38,23 +38,23 @@ await delay(1000);
// Failure example: second member can not self-approve // Failure example: second member can not self-approve
try { try {
const pool = await member2.initiateValidationPool(bench, {fee: 1, duration: 1000, tokenLossRatio: 1}); const pool = await member2.initiateValidationPool(bench, { fee: 1, duration: 1000, tokenLossRatio: 1 });
await member2.revealIdentity(pool); // Quorum not met! await member2.revealIdentity(pool); // Quorum not met!
await updateDisplayValues(); await updateDisplayValues();
await delay(1000); await delay(1000);
} catch(e) { } catch (e) {
if (e.message.match(/Quorum is not met/)) { if (e.message.match(/Quorum is not met/)) {
console.log("Caught expected error: Quorum not met") console.log('Caught expected error: Quorum not met');
} else { } else {
console.error("Unexpected error") console.error('Unexpected error');
throw e; throw e;
} }
} }
// Second member must be approved by first member // Second member must be approved by first member
{ {
const pool = await member2.initiateValidationPool(bench, {fee: 1, duration: 1000, tokenLossRatio: 1}); const pool = await member2.initiateValidationPool(bench, { fee: 1, duration: 1000, tokenLossRatio: 1 });
await member1.castVote(pool, {position: true, stake: 4, lockingTime: 0}); await member1.castVote(pool, { position: true, stake: 4, lockingTime: 0 });
await member1.revealIdentity(pool); await member1.revealIdentity(pool);
await member2.revealIdentity(pool); // Vote passes await member2.revealIdentity(pool); // Vote passes
await updateDisplayValues(); await updateDisplayValues();