Add support for posts with multiple authors

This commit is contained in:
Ladd Hoffman 2023-04-17 01:41:12 +00:00
parent f3037a766d
commit d4bdb1c435
57 changed files with 639 additions and 320 deletions

View File

@ -28,3 +28,19 @@ cardano -- "dynamic availability"?
staking pools -- does it make sense with reputation? staking pools -- does it make sense with reputation?
what about for governance voting -- what about for governance voting --
do we want a representative republic or a democracy? do we want a representative republic or a democracy?
---
# Protocol brainstorming
Each node must build/maintain a view of the history and/or state of the "smart contract" operations.
Nodes must sign messages to each other with asymmetric keys.
This is intended to be an open network that anyone can join.
Each node must verify the results reported by other nodes, and themselves report results to other nodes.
In order to receive payments, the network must solve the same problems that (other) block chains have solved, i.e. must prevent double-spend; must prevent tampering with the ledger.
Storage may be ranked into tiers, where there is core data essential to the integrity of the ledger; ancillary data that is important or desirable for review of the ledger; and supplementary data that is of variable importants for particular use cases, but does not compose the core fabric of the system.

View File

@ -0,0 +1,27 @@
# Ecosystem Health
How do we incentivize and reward the producers and maintainers of infrastructure? Of essential goods and services?
How do we reward pro-social behavior?
How do we encourage creativity?
Vision/Mission
Craig: Give people tools to enable them to better express their values by collaborating
truth
good
beauty
thought
action
perception
ideas
knowledge
beliefs
utility
evolution in the true sense -- most directions it can change will be detrimental

View File

@ -0,0 +1,5 @@
At the base layer, we need a peer to peer protocol that allows new nodes to join the network and send and receive messages. It must protect against denial of service attacks. It must support the establishment of consensus, to varying strengths.
We need a lightweight messaging solution to facilitate gathering information from the edges of the network, but we also need to protect against denial of service by malicious actors.
[gossipsub](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md) looks like a good protocol for this.

View File

@ -7,3 +7,28 @@ Matrix supports "Application Services", which are limited to funcion in a passiv
Synapse, a Matrix server implementation, supports "Modules" Synapse, a Matrix server implementation, supports "Modules"
The Matrix devs recognize the need for a robust reputation system and are in pursuit of funding and development for that purpose. The Matrix devs recognize the need for a robust reputation system and are in pursuit of funding and development for that purpose.
```mermaid
graph LR
subgraph Matrix
homeserver[Homeserver]
chainClient[Blockchain<br/>connected<br/>client]
publicClient[Public<br/>Matrix-only<br/>client]
end
blockchain[Blockchain]
%% subgraph Blockchain
%% forum[Forum]
%% post[Post]
%% availability[Availability]
%% wsc[WSC]
%% pool[Validation<br/>Pool]
%% end
publicClient --> homeserver
chainClient --> homeserver
chainClient --> blockchain
homeserver --> blockchain
```

View File

@ -0,0 +1,17 @@
# signature verification
# voting topologies
# client implementations
## example workflows
- retroactive sign flip
# storage scaling
# blockchain underpinnings
# reputation token lifetime / decay
- active/passive

View File

@ -1,11 +1,13 @@
import { Action } from '../display/action.js'; import { Action } from '../display/action.js';
import { PostMessage } from '../forum-network/message.js'; import { PostMessage } from '../forum-network/message.js';
import { CryptoUtil } from '../util/crypto.js'; import { CryptoUtil } from '../supporting/crypto.js';
import { ReputationHolder } from '../reputation/reputation-holder.js'; import { ReputationHolder } from '../reputation/reputation-holder.js';
import { EdgeTypes } from '../../util/constants.js';
import { displayNumber } from '../../util/helpers.js';
export class Expert extends ReputationHolder { export class Expert extends ReputationHolder {
constructor(dao, name, scene) { constructor(dao, name, scene, options) {
super(name, scene); super(name, scene, options);
this.dao = dao; this.dao = dao;
this.actions = { this.actions = {
submitPostViaNetwork: new Action('submit post via network', scene), submitPostViaNetwork: new Action('submit post via network', scene),
@ -20,6 +22,25 @@ export class Expert extends ReputationHolder {
this.tokens = []; this.tokens = [];
} }
getReputation() {
const authorVertex = this.dao.forum.graph.getVertex(this.reputationPublicKey);
if (!authorVertex) {
return 0;
}
const authorEdges = authorVertex.getEdges(EdgeTypes.AUTHOR, false);
const tokenValues = authorEdges.map(({ data: { tokenId } }) => this.dao.reputation.valueOf(tokenId));
return tokenValues.reduce((value, total) => total += value, 0);
}
getLabel() {
return `${this.name}
<table><tr>
<td>reputation</td>
<td>${displayNumber(this.getReputation())}</td>
</tr></table>`
.replaceAll(/\n\s*/g, '');
}
async initialize() { async initialize() {
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);
@ -28,15 +49,6 @@ export class Expert extends ReputationHolder {
return this; return this;
} }
async submitPostViaNetwork(forumNode, post, stake) {
// TODO: Include fee
const postMessage = new PostMessage({ post, stake });
await postMessage.sign(this.reputationKey);
await this.actions.submitPostViaNetwork.log(this, forumNode);
// For now, directly call forumNode.receiveMessage();
await forumNode.receiveMessage(JSON.stringify(postMessage.toJSON()));
}
async submitPostWithFee(postContent, poolOptions) { async submitPostWithFee(postContent, poolOptions) {
const post = await this.dao.forum.addPost(this.reputationPublicKey, postContent); const post = await this.dao.forum.addPost(this.reputationPublicKey, postContent);
await this.actions.submitPost.log(this, post); await this.actions.submitPost.log(this, post);
@ -49,14 +61,9 @@ export class Expert extends ReputationHolder {
async initiateValidationPool(poolOptions) { async initiateValidationPool(poolOptions) {
// For now, make direct call rather than network // For now, make direct call rather than network
poolOptions.reputationPublicKey = this.reputationPublicKey; poolOptions.reputationPublicKey = this.reputationPublicKey;
const pool = await this.dao.initiateValidationPool(poolOptions); const pool = await this.dao.initiateValidationPool(this, poolOptions);
this.tokens.push(pool.tokenId); this.tokens.push(pool.tokenId);
this.validationPools.set(pool.id, poolOptions); this.validationPools.set(pool.id, poolOptions);
await this.actions.initiateValidationPool.log(
this,
pool,
`(fee: ${poolOptions.fee}, stake: ${poolOptions.authorStakeAmount ?? 0})`,
);
return pool; return pool;
} }

View File

@ -1,6 +1,6 @@
import { Action } from '../display/action.js'; import { Action } from '../display/action.js';
import { Actor } from '../display/actor.js'; import { Actor } from '../display/actor.js';
import { CryptoUtil } from '../util/crypto.js'; import { CryptoUtil } from '../supporting/crypto.js';
class Worker { class Worker {
constructor(reputationPublicKey, tokenId, stakeAmount, duration) { constructor(reputationPublicKey, tokenId, stakeAmount, duration) {

View File

@ -1,7 +1,7 @@
import { randomID } from '../../util.js'; import { randomID } from '../../util/helpers.js';
import { Action } from '../display/action.js'; import { Action } from '../display/action.js';
import { Actor } from '../display/actor.js'; import { Actor } from '../display/actor.js';
import { PostContent } from '../util/post-content.js'; import { PostContent } from '../supporting/post-content.js';
class Request { class Request {
static nextSeq = 0; static nextSeq = 0;
@ -77,7 +77,7 @@ export class Business extends Actor {
// Initiate a validation pool for this work evidence. // Initiate a validation pool for this work evidence.
await this.actions.initiateValidationPool.log(this, this.dao); await this.actions.initiateValidationPool.log(this, this.dao);
const pool = await this.dao.initiateValidationPool({ const pool = await this.dao.initiateValidationPool(this, {
postId, postId,
fee: request.fee, fee: request.fee,
duration, duration,

View File

@ -13,8 +13,8 @@ import { Actor } from '../display/actor.js';
* - Reputation: Keep track of reputation accrued to each expert * - Reputation: Keep track of reputation accrued to each expert
*/ */
export class DAO extends Actor { export class DAO extends Actor {
constructor(name, scene) { constructor(name, scene, options) {
super(name, scene); super(name, scene, options);
/* Contracts */ /* Contracts */
this.forum = new Forum(this, 'Forum', scene); this.forum = new Forum(this, 'Forum', scene);
@ -55,10 +55,10 @@ export class DAO extends Actor {
.reduce((acc, cur) => (acc += cur), 0); .reduce((acc, cur) => (acc += cur), 0);
} }
async initiateValidationPool(poolOptions, stakeOptions) { async initiateValidationPool(fromActor, poolOptions, stakeOptions) {
const validationPoolNumber = this.validationPools.size + 1; const validationPoolNumber = this.validationPools.size + 1;
const name = `Pool${validationPoolNumber}`; const name = `Pool${validationPoolNumber}`;
const pool = new ValidationPool(this, poolOptions, name, this.scene); const pool = new ValidationPool(this, poolOptions, name, this.scene, fromActor);
this.validationPools.set(pool.id, pool); this.validationPools.set(pool.id, pool);
if (stakeOptions) { if (stakeOptions) {

View File

@ -3,20 +3,22 @@ import { Action } from '../display/action.js';
import { Actor } from '../display/actor.js'; import { Actor } from '../display/actor.js';
import params from '../../params.js'; import params from '../../params.js';
import { ReputationHolder } from '../reputation/reputation-holder.js'; import { ReputationHolder } from '../reputation/reputation-holder.js';
import { displayNumber, EPSILON, INCINERATOR_ADDRESS } from '../../util.js'; import { displayNumber } from '../../util/helpers.js';
import {
const CITATION = 'citation'; EPSILON, INCINERATOR_ADDRESS, EdgeTypes, VertexTypes,
const BALANCE = 'balance'; } from '../../util/constants.js';
class Post extends Actor { class Post extends Actor {
constructor(forum, authorPublicKey, postContent) { constructor(forum, senderId, postContent) {
const index = forum.posts.countVertices(); const index = forum.graph.countVertices(VertexTypes.POST);
const name = `Post${index + 1}`; const name = `Post${index + 1}`;
super(name, forum.scene); super(name, forum.scene);
this.forum = forum;
this.id = postContent.id ?? name; this.id = postContent.id ?? name;
this.authorPublicKey = authorPublicKey; this.senderId = senderId;
this.value = 0; this.value = 0;
this.initialValue = 0; this.initialValue = 0;
this.authors = postContent.authors;
this.citations = postContent.citations; this.citations = postContent.citations;
this.title = postContent.title; this.title = postContent.title;
const leachingTotal = this.citations const leachingTotal = this.citations
@ -25,6 +27,8 @@ class Post extends Actor {
const donationTotal = this.citations const donationTotal = this.citations
.filter(({ weight }) => weight > 0) .filter(({ weight }) => weight > 0)
.reduce((total, { weight }) => total += weight, 0); .reduce((total, { weight }) => total += weight, 0);
// TODO: Move evaluation of these parameters to Validation Pool
if (leachingTotal > params.revaluationLimit) { if (leachingTotal > params.revaluationLimit) {
throw new Error('Post leaching total exceeds revaluation limit ' throw new Error('Post leaching total exceeds revaluation limit '
+ `(${leachingTotal} > ${params.revaluationLimit})`); + `(${leachingTotal} > ${params.revaluationLimit})`);
@ -49,6 +53,12 @@ class Post extends Actor {
</tr></table>` </tr></table>`
.replaceAll(/\n\s*/g, ''); .replaceAll(/\n\s*/g, '');
} }
async setValue(value) {
this.value = value;
await this.setDisplayValue('value', value);
this.forum.graph.getVertex(this.id).setDisplayLabel(this.getLabel());
}
} }
/** /**
@ -61,7 +71,7 @@ export class Forum extends ReputationHolder {
super(name, scene); super(name, scene);
this.dao = dao; this.dao = dao;
this.id = this.reputationPublicKey; this.id = this.reputationPublicKey;
this.posts = new WDAG(scene); this.graph = new WDAG(scene);
this.actions = { this.actions = {
propagate: new Action('propagate', scene), propagate: new Action('propagate', scene),
confirm: new Action('confirm', scene), confirm: new Action('confirm', scene),
@ -69,73 +79,120 @@ export class Forum extends ReputationHolder {
}; };
} }
async addPost(authorId, postContent) { async addPost(senderId, postContent) {
console.log('addPost', { authorId, postContent }); console.log('addPost', { senderId, postContent });
const post = new Post(this, authorId, postContent); const post = new Post(this, senderId, postContent);
this.posts.addVertex(post.id, post, post.getLabel()); this.graph.addVertex(VertexTypes.POST, post.id, post, post.getLabel());
for (const { postId: citedPostId, weight } of post.citations) { for (const { postId: citedPostId, weight } of post.citations) {
// Special case: Incinerator // Special case: Incinerator
if (citedPostId === INCINERATOR_ADDRESS && !this.posts.getVertex(INCINERATOR_ADDRESS)) { if (citedPostId === INCINERATOR_ADDRESS && !this.graph.getVertex(INCINERATOR_ADDRESS)) {
this.posts.addVertex(INCINERATOR_ADDRESS, { name: 'Incinerator' }, 'Incinerator'); this.graph.addVertex(VertexTypes.POST, INCINERATOR_ADDRESS, { name: 'Incinerator' }, 'Incinerator');
} }
this.posts.addEdge(CITATION, post.id, citedPostId, weight); this.graph.addEdge(EdgeTypes.CITATION, post.id, citedPostId, weight);
} }
return post; return post;
} }
getPost(postId) { getPost(postId) {
return this.posts.getVertexData(postId); return this.graph.getVertexData(postId);
} }
getPosts() { getPosts() {
return this.posts.getVerticesData(); return this.graph.getVerticesData();
}
async setPostValue(post, value) {
post.value = value;
await post.setValue('value', value);
this.posts.setVertexLabel(post.id, post.getLabel());
} }
getTotalValue() { getTotalValue() {
return this.getPosts().reduce((total, { value }) => total += value, 0); return this.getPosts().reduce((total, { value }) => total += value, 0);
} }
// getLatestContract(type) { }
// getContract(type) { }
async onValidate({ async onValidate({
pool, postId, tokenId, pool, postId, tokenId,
}) { }) {
console.log('onValidate', { pool, postId, tokenId });
const initialValue = this.dao.reputation.valueOf(tokenId); const initialValue = this.dao.reputation.valueOf(tokenId);
const postVertex = this.posts.getVertex(postId); const postVertex = this.graph.getVertex(postId);
const post = postVertex.data; const post = postVertex.data;
post.setStatus('Validated'); post.setStatus('Validated');
post.initialValue = initialValue; post.initialValue = initialValue;
this.posts.setVertexLabel(post.id, post.getLabel()); postVertex.setDisplayLabel(post.getLabel());
// Store a reference to the reputation token associated with this post, const addAuthorToGraph = (publicKey, weight, authorTokenId) => {
// so that its value can be updated by future validated posts. // For graph display purposes, we want to use the existing Expert actors from the current scene.
post.tokenId = tokenId; const author = this.scene.findActor(({ reputationPublicKey }) => reputationPublicKey === publicKey);
author.setDisplayValue('reputation', () => author.getReputation());
const authorVertex = this.graph.getVertex(publicKey)
?? this.graph.addVertex(VertexTypes.AUTHOR, publicKey, author, author.getLabel(), {
hide: author.options.hide,
});
this.graph.addEdge(
EdgeTypes.AUTHOR,
postVertex,
authorVertex,
weight,
{ tokenId: authorTokenId },
{ hide: author.options.hide },
);
};
// In the case of multiple authors, mint additional (empty) tokens.
// If no authors are specified, treat the sender as the sole author.
// TODO: Verify that cumulative author weight == 1.
if (!post.authors?.length) {
addAuthorToGraph(post.senderId, 1, tokenId);
} else {
for (const { publicKey, weight } of post.authors) {
// If the sender is also listed among the authors, do not mint them an additional token.
const authorTokenId = (publicKey === post.senderId) ? tokenId : this.dao.reputation.mint(this.id, 0);
addAuthorToGraph(publicKey, weight, authorTokenId);
}
// If the sender is not an author, they will end up with the minted token but with zero value.
if (!post.authors.find(({ publicKey }) => publicKey === post.senderId)) {
addAuthorToGraph(post.senderId, 0, tokenId);
}
}
const rewardsAccumulator = new Map(); const rewardsAccumulator = new Map();
// Compute rewards // Compute reputation rewards
await this.propagateValue( await this.propagateValue(
{ to: postVertex, from: { data: pool } }, { to: postVertex, from: { data: pool } },
{ rewardsAccumulator, increment: initialValue }, { rewardsAccumulator, increment: initialValue },
); );
// Apply computed rewards to update values of tokens // Apply computed rewards to update values of tokens
for (const [id, value] of rewardsAccumulator) { for (const [authorEdge, amount] of rewardsAccumulator) {
if (value < 0) { const { to: authorVertex, data: { tokenId: authorTokenId } } = authorEdge;
this.dao.reputation.transferValueFrom(id, post.tokenId, -value); const { data: author } = authorVertex;
// The primary author gets the validation pool minted token.
// So we don't need to transfer any reputation to the primary author.
// Their reward will be the remaining balance after all other transfers.
if (authorTokenId !== tokenId) {
if (amount < 0) {
this.dao.reputation.transferValueFrom(authorTokenId, tokenId, -amount);
} else { } else {
this.dao.reputation.transferValueFrom(post.tokenId, id, value); this.dao.reputation.transferValueFrom(tokenId, authorTokenId, amount);
}
await author.computeDisplayValues();
authorVertex.setDisplayLabel(author.getLabel());
} }
} }
// Transfer ownership of the minted/staked token, from the posts to the post author const senderVertex = this.graph.getVertex(post.senderId);
this.dao.reputation.transfer(this.id, post.authorPublicKey, post.tokenId); const { data: sender } = senderVertex;
// const toActor = this.scene?.findActor((actor) => actor.reputationPublicKey === post.authorPublicKey); await sender.computeDisplayValues();
// const value = this.dao.reputation.valueOf(post.tokenId); senderVertex.setDisplayLabel(sender.getLabel());
// Transfer ownership of the minted tokens to the authors
for (const authorEdge of postVertex.getEdges(EdgeTypes.AUTHOR, true)) {
const authorVertex = authorEdge.to;
const author = authorVertex.data;
const { tokenId: authorTokenId } = authorEdge.data;
this.dao.reputation.transfer(this.id, author.reputationPublicKey, authorTokenId);
}
} }
/** /**
@ -146,7 +203,7 @@ export class Forum extends ReputationHolder {
rewardsAccumulator, increment, depth = 0, initialNegative = false, rewardsAccumulator, increment, depth = 0, initialNegative = false,
}) { }) {
const postVertex = edge.to; const postVertex = edge.to;
const post = postVertex?.data; const post = postVertex.data;
this.actions.propagate.log(edge.from.data, post, `(${increment})`); this.actions.propagate.log(edge.from.data, post, `(${increment})`);
if (!!params.referenceChainLimit && depth > params.referenceChainLimit) { if (!!params.referenceChainLimit && depth > params.referenceChainLimit) {
@ -171,13 +228,14 @@ export class Forum extends ReputationHolder {
const propagate = async (positive) => { const propagate = async (positive) => {
let totalOutboundAmount = 0; let totalOutboundAmount = 0;
const citationEdges = postVertex.getEdges(CITATION, true) const citationEdges = postVertex.getEdges(EdgeTypes.CITATION, true)
.filter(({ weight }) => (positive ? weight > 0 : weight < 0)); .filter(({ weight }) => (positive ? weight > 0 : weight < 0));
for (const citationEdge of citationEdges) { for (const citationEdge of citationEdges) {
const { weight } = citationEdge; const { weight } = citationEdge;
let outboundAmount = weight * increment; let outboundAmount = weight * increment;
if (Math.abs(outboundAmount) > EPSILON) { if (Math.abs(outboundAmount) > EPSILON) {
const balanceToOutbound = this.posts.getEdgeWeight(BALANCE, citationEdge.from, citationEdge.to) ?? 0; const balanceToOutbound = this.graph.getEdgeWeight(EdgeTypes.BALANCE, citationEdge.from, citationEdge.to)
?? 0;
let refundFromOutbound = 0; let refundFromOutbound = 0;
// Special case: Incineration. // Special case: Incineration.
@ -221,7 +279,12 @@ export class Forum extends ReputationHolder {
// Keep a record of the effect of the reputation transferred along this edge in the graph, // Keep a record of the effect of the reputation transferred along this edge in the graph,
// so that later, negative citations can be constrained to at most undo these effects. // so that later, negative citations can be constrained to at most undo these effects.
this.posts.setEdgeWeight(BALANCE, citationEdge.from, citationEdge.to, balanceToOutbound + outboundAmount); this.graph.setEdgeWeight(
EdgeTypes.BALANCE,
citationEdge.from,
citationEdge.to,
balanceToOutbound + outboundAmount,
);
totalOutboundAmount += outboundAmount; totalOutboundAmount += outboundAmount;
this.actions.confirm.log( this.actions.confirm.log(
@ -250,6 +313,17 @@ export class Forum extends ReputationHolder {
const appliedIncrement = newValue - post.value; const appliedIncrement = newValue - post.value;
const refundToInbound = increment - appliedIncrement; const refundToInbound = increment - appliedIncrement;
// Apply reputation effects to post authors, not to the post directly
for (const authorEdge of postVertex.getEdges(EdgeTypes.AUTHOR, true)) {
const { weight, to: { data: author } } = authorEdge;
const authorIncrement = weight * appliedIncrement;
rewardsAccumulator.set(authorEdge, authorIncrement);
this.actions.propagate.log(post, author, `(${authorIncrement})`);
}
// Increment the value of the post
await post.setValue(newValue);
console.log('propagateValue end', { console.log('propagateValue end', {
depth, depth,
increment, increment,
@ -259,12 +333,6 @@ export class Forum extends ReputationHolder {
refundToInbound, refundToInbound,
}); });
// Award reputation to post author
rewardsAccumulator.set(post.tokenId, appliedIncrement);
// Increment the value of the post
await this.setPostValue(post, newValue);
return refundToInbound; return refundToInbound;
} }
} }

View File

@ -3,7 +3,7 @@ import { Stake } from '../supporting/stake.js';
import { Voter } from '../supporting/voter.js'; import { Voter } from '../supporting/voter.js';
import params from '../../params.js'; import params from '../../params.js';
import { Action } from '../display/action.js'; import { Action } from '../display/action.js';
import { displayNumber } from '../../util.js'; import { displayNumber } from '../../util/helpers.js';
const ValidationPoolStates = Object.freeze({ const ValidationPoolStates = Object.freeze({
OPEN: 'OPEN', OPEN: 'OPEN',
@ -27,16 +27,21 @@ export class ValidationPool extends ReputationHolder {
}, },
name, name,
scene, scene,
fromActor,
) { ) {
super(name, scene); super(name, scene);
this.id = this.reputationPublicKey; this.id = this.reputationPublicKey;
this.actions = { this.actions = {
initiate: new Action('initiate validation pool', scene),
reward: new Action('reward', scene), reward: new Action('reward', scene),
transfer: new Action('transfer', scene), transfer: new Action('transfer', scene),
mint: new Action('mint', scene), mint: new Action('mint', scene),
}; };
this.actions.initiate.log(fromActor, this, `(fee: ${fee})`);
this.activate();
// 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
@ -90,8 +95,6 @@ export class ValidationPool extends ReputationHolder {
const voter = this.dao.experts.get(reputationPublicKey) ?? new Voter(reputationPublicKey); const voter = this.dao.experts.get(reputationPublicKey) ?? new Voter(reputationPublicKey);
voter.addVoteRecord(this); voter.addVoteRecord(this);
this.dao.experts.set(reputationPublicKey, voter); this.dao.experts.set(reputationPublicKey, voter);
this.activate();
} }
getTokenLossRatio() { getTokenLossRatio() {
@ -180,7 +183,7 @@ export class ValidationPool extends ReputationHolder {
// Update computed display values // Update computed display values
const actor = this.scene?.findActor((a) => a.reputationPublicKey === voter.reputationPublicKey); const actor = this.scene?.findActor((a) => a.reputationPublicKey === voter.reputationPublicKey);
await actor.computeValues(); await actor.computeDisplayValues();
} }
} }
@ -235,9 +238,9 @@ export class ValidationPool extends ReputationHolder {
if (!actor) { if (!actor) {
throw new Error('Actor not found!'); throw new Error('Actor not found!');
} }
await actor.computeValues(); await actor.computeDisplayValues();
} }
await this.dao.computeValues(); await this.dao.computeDisplayValues();
this.scene?.stateToTable(`validation pool ${this.name} complete`); this.scene?.stateToTable(`validation pool ${this.name} complete`);

View File

@ -1,7 +1,13 @@
import { displayNumber } from '../../util.js'; import { displayNumber } from '../../util/helpers.js';
export class Actor { export class Actor {
constructor(name, scene) { /**
* @param {string} name
* @param {Scene} scene
* @param {boolean} options.announce
* @param {boolean} options.hide
*/
constructor(name, scene, options = {}) {
if (!scene) throw new Error('An actor without a scene!'); if (!scene) throw new Error('An actor without a scene!');
this.name = name; this.name = name;
this.scene = scene; this.scene = scene;
@ -11,6 +17,7 @@ export class Actor {
this.values = new Map(); this.values = new Map();
this.valueFunctions = new Map(); this.valueFunctions = new Map();
this.active = 0; this.active = 0;
this.options = options;
scene?.registerActor(this); scene?.registerActor(this);
} }
@ -58,12 +65,12 @@ export class Actor {
this.values.set(label, this.scene?.addDisplayValue(`${this.name} ${label}`)); this.values.set(label, this.scene?.addDisplayValue(`${this.name} ${label}`));
if (fn) { if (fn) {
this.valueFunctions.set(label, fn); this.valueFunctions.set(label, fn);
await this.computeValues(); await this.computeDisplayValues();
} }
return this; return this;
} }
async setValue(label, value) { async setDisplayValue(label, value) {
if (typeof value === 'function') { if (typeof value === 'function') {
return this.addComputedValue(label, value); return this.addComputedValue(label, value);
} }
@ -76,10 +83,10 @@ export class Actor {
return this; return this;
} }
async computeValues() { async computeDisplayValues() {
for (const [label, fn] of this.valueFunctions.entries()) { for (const [label, fn] of this.valueFunctions.entries()) {
const value = fn(); const value = fn();
await this.setValue(label, value); await this.setDisplayValue(label, value);
} }
} }

View File

@ -1,5 +1,5 @@
import { DisplayValue } from './display-value.js'; import { DisplayValue } from './display-value.js';
import { randomID } from '../../util.js'; import { randomID } from '../../util/helpers.js';
export class Box { export class Box {
constructor(name, parentEl, options = {}) { constructor(name, parentEl, options = {}) {

View File

@ -1,4 +1,4 @@
import { displayNumber } from '../../util.js'; import { displayNumber } from '../../util/helpers.js';
export class DisplayValue { export class DisplayValue {
constructor(name, box) { constructor(name, box) {

View File

@ -1,5 +1,5 @@
import mermaid from 'https://unpkg.com/mermaid@9.2.2/dist/mermaid.esm.min.mjs'; import mermaid from 'https://unpkg.com/mermaid@9.2.2/dist/mermaid.esm.min.mjs';
import { debounce } from '../../util.js'; import { debounce } from '../../util/helpers.js';
export class MermaidDiagram { export class MermaidDiagram {
constructor(box, logBox) { constructor(box, logBox) {

View File

@ -1,5 +1,5 @@
import { Action } from './action.js'; import { Action } from './action.js';
import { CryptoUtil } from '../util/crypto.js'; import { CryptoUtil } from '../supporting/crypto.js';
import { MermaidDiagram } from './mermaid.js'; import { MermaidDiagram } from './mermaid.js';
import { SequenceDiagram } from './sequence.js'; import { SequenceDiagram } from './sequence.js';
import { Table } from './table.js'; import { Table } from './table.js';
@ -88,7 +88,9 @@ export class Scene {
registerActor(actor) { registerActor(actor) {
this.actors.add(actor); this.actors.add(actor);
// this.sequence?.log(`participant ${actor.name}`); if (actor.options.announce) {
this.sequence?.log(`participant ${actor.name}`);
}
} }
findActor(fn) { findActor(fn) {
@ -114,12 +116,14 @@ export class Scene {
row.set('elapsedMs', new Date() - this.dateStart); row.set('elapsedMs', new Date() - this.dateStart);
row.set('label', label); row.set('label', label);
for (const actor of this.actors) { for (const actor of this.actors) {
if (!actor.options.hide) {
for (const [aKey, { name, value }] of actor.getValuesMap()) { for (const [aKey, { name, value }] of actor.getValuesMap()) {
const key = `${actor.name}:${aKey}`; const key = `${actor.name}:${aKey}`;
columns.push({ key, title: name }); columns.push({ key, title: name });
row.set(key, value); row.set(key, value);
} }
} }
}
columns.push({ key: 'label', title: '' }); columns.push({ key: 'label', title: '' });
this.table.setColumns(columns); this.table.setColumns(columns);
this.table.addRow(row); this.table.addRow(row);

View File

@ -1,4 +1,4 @@
import { hexToRGB } from '../../util.js'; import { hexToRGB } from '../../util/helpers.js';
import { MermaidDiagram } from './mermaid.js'; import { MermaidDiagram } from './mermaid.js';
export class SequenceDiagram extends MermaidDiagram { export class SequenceDiagram extends MermaidDiagram {

View File

@ -1,4 +1,4 @@
import { displayNumber } from '../../util.js'; import { displayNumber } from '../../util/helpers.js';
export class Table { export class Table {
constructor(box) { constructor(box) {

View File

@ -2,14 +2,12 @@ import { Action } from '../display/action.js';
import { import {
Message, PostMessage, PeerMessage, messageFromJSON, Message, PostMessage, PeerMessage, messageFromJSON,
} from './message.js'; } from './message.js';
import { ForumView } from './forum-view.js';
import { NetworkNode } from './network-node.js'; import { NetworkNode } from './network-node.js';
import { randomID } from '../util/util.js'; import { randomID } from '../../util/helpers.js';
export class ForumNode extends NetworkNode { export class ForumNode extends NetworkNode {
constructor(name, scene) { constructor(name, scene) {
super(name, scene); super(name, scene);
this.forumView = new ForumView();
this.actions = { this.actions = {
...this.actions, ...this.actions,
storePost: new Action('store post', scene), storePost: new Action('store post', scene),

View File

@ -1,67 +0,0 @@
import { WDAG } from '../supporting/wdag.js';
class Author {
constructor() {
this.posts = new Map();
this.reputation = 0;
}
}
class PostVertex {
constructor(id, author, stake, content, citations) {
this.id = id;
this.author = author;
this.content = content;
this.stake = stake;
this.citations = citations;
this.reputation = 0;
}
}
export class ForumView {
constructor() {
this.reputations = new Map();
this.posts = new WDAG();
this.authors = new Map();
}
getReputation(id) {
return this.reputations.get(id);
}
setReputation(id, reputation) {
this.reputations.set(id, reputation);
}
incrementReputation(publicKey, increment, _reason) {
const reputation = this.getReputation(publicKey) || 0;
return this.reputations.set(publicKey, reputation + increment);
}
getOrInitializeAuthor(authorId) {
let author = this.authors.get(authorId);
if (!author) {
author = new Author(authorId);
this.authors.set(authorId, author);
}
return author;
}
addPost(authorId, postId, postContent, stake) {
const { citations = [], content } = postContent;
const author = this.getOrInitializeAuthor(authorId);
const postVertex = new PostVertex(postId, author, stake, content, citations);
this.posts.addVertex(postId, postVertex);
for (const { postId: citedPostId, weight } of citations) {
this.posts.addEdge('citation', postId, citedPostId, weight);
}
}
getPost(postId) {
return this.posts.getVertexData(postId);
}
getPosts() {
return this.posts.getVertices();
}
}

View File

@ -1,5 +1,5 @@
import { CryptoUtil } from '../util/crypto.js'; import { CryptoUtil } from '../supporting/crypto.js';
import { PostContent } from '../util/post-content.js'; import { PostContent } from '../supporting/post-content.js';
export class Message { export class Message {
constructor(content) { constructor(content) {

View File

@ -38,7 +38,8 @@ export class NetworkNode extends Actor {
// Enqueue it for further processing. // Enqueue it for further processing.
async receiveMessage(messageStr) { async receiveMessage(messageStr) {
const messageJson = JSON.parse(messageStr); const messageJson = JSON.parse(messageStr);
const senderReputation = this.forumView.getReputation(messageJson.publicKey) || 0; // const senderReputation = this.forumView.getReputation(messageJson.publicKey) || 0;
const senderReputation = 0;
this.queue.add(messageJson, senderReputation); this.queue.add(messageJson, senderReputation);
} }

View File

@ -1,4 +1,4 @@
import { randomID } from '../util/util.js'; import { randomID } from '../util/util/helpers.js';
class Pledge { class Pledge {
constructor({ stake, duration }) { constructor({ stake, duration }) {

View File

@ -1,9 +1,9 @@
import { randomID } from '../../util.js'; import { randomID } from '../../util/helpers.js';
import { Actor } from '../display/actor.js'; import { Actor } from '../display/actor.js';
export class ReputationHolder extends Actor { export class ReputationHolder extends Actor {
constructor(name, scene) { constructor(name, scene, options) {
super(name, scene); super(name, scene, options);
this.reputationPublicKey = `${name}_${randomID()}`; this.reputationPublicKey = `${name}_${randomID()}`;
} }
} }

View File

@ -1,6 +1,6 @@
import { ERC721 } from '../supporting/erc721.js'; import { ERC721 } from '../supporting/erc721.js';
import { randomID } from '../../util/helpers.js';
import { EPSILON, randomID } from '../../util.js'; import { EPSILON } from '../../util/constants.js';
class Lock { class Lock {
constructor(tokenId, amount, duration) { constructor(tokenId, amount, duration) {
@ -36,6 +36,9 @@ export class ReputationTokenContract extends ERC721 {
incrementValue(tokenId, increment, context) { incrementValue(tokenId, increment, context) {
const value = this.values.get(tokenId); const value = this.values.get(tokenId);
if (value === undefined) {
throw new Error(`Token not found: ${tokenId}`);
}
const newValue = value + increment; const newValue = value + increment;
const history = this.histories.get(tokenId) || []; const history = this.histories.get(tokenId) || [];
@ -76,7 +79,11 @@ export class ReputationTokenContract extends ERC721 {
} }
valueOf(tokenId) { valueOf(tokenId) {
return this.values.get(tokenId); const value = this.values.get(tokenId);
if (value === undefined) {
throw new Error(`Token not found: ${tokenId}`);
}
return value;
} }
availableValueOf(tokenId) { availableValueOf(tokenId) {

View File

@ -31,6 +31,7 @@ export class ERC721 {
} }
mint(to, tokenId) { mint(to, tokenId) {
console.log('ERC721.mint', { to, tokenId });
if (this.owners.get(tokenId)) { if (this.owners.get(tokenId)) {
throw new Error('ERC721: token already minted'); throw new Error('ERC721: token already minted');
} }
@ -60,9 +61,10 @@ export class ERC721 {
} }
transfer(from, to, tokenId) { transfer(from, to, tokenId) {
console.log('ERC721.transfer', { from, to, tokenId });
const owner = this.owners.get(tokenId); const owner = this.owners.get(tokenId);
if (owner !== from) { if (owner !== from) {
throw new Error('ERC721: transfer from incorrect owner'); throw new Error(`ERC721: transfer from incorrect owner ${from}; should be ${owner}`);
} }
this.incrementBalance(from, -1); this.incrementBalance(from, -1);
this.incrementBalance(to, 1); this.incrementBalance(to, 1);

View File

@ -1,4 +1,22 @@
export class Citation { class Author {
constructor(publicKey, weight) {
this.publicKey = publicKey;
this.weight = weight;
}
toJSON() {
return {
publicKey: this.publicKey,
weight: this.weight,
};
}
static fromJSON({ publicKey, weight }) {
return new Author(publicKey, weight);
}
}
class Citation {
constructor(postId, weight) { constructor(postId, weight) {
this.postId = postId; this.postId = postId;
this.weight = weight; this.weight = weight;
@ -17,11 +35,18 @@ export class Citation {
} }
export class PostContent { export class PostContent {
constructor(content) { constructor(content = {}) {
this.content = content; this.content = content;
this.authors = [];
this.citations = []; this.citations = [];
} }
addAuthor(authorPublicKey, weight) {
const author = new Author(authorPublicKey, weight);
this.authors.push(author);
return this;
}
addCitation(postId, weight) { addCitation(postId, weight) {
const citation = new Citation(postId, weight); const citation = new Citation(postId, weight);
this.citations.push(citation); this.citations.push(citation);
@ -36,6 +61,7 @@ export class PostContent {
toJSON() { toJSON() {
return { return {
content: this.content, content: this.content,
authors: this.authors.map((author) => author.toJSON()),
citations: this.citations.map((citation) => citation.toJSON()), citations: this.citations.map((citation) => citation.toJSON()),
...(this.id ? { id: this.id } : {}), ...(this.id ? { id: this.id } : {}),
title: this.title, title: this.title,
@ -43,9 +69,10 @@ export class PostContent {
} }
static fromJSON({ static fromJSON({
id, content, citations, title, id, content, authors, citations, title,
}) { }) {
const post = new PostContent(content); const post = new PostContent(content);
post.authors = authors.map((author) => Author.fromJSON(author));
post.citations = citations.map((citation) => Citation.fromJSON(citation)); post.citations = citations.map((citation) => Citation.fromJSON(citation));
post.id = id; post.id = id;
post.title = title; post.title = title;

View File

@ -1,26 +1,69 @@
const getEdgeKey = ({ from, to }) => btoa([from.id, to.id]).replaceAll(/[^A-Za-z0-9]+/g, '');
export class Vertex { export class Vertex {
constructor(id, data) { constructor(graph, type, id, data, options = {}) {
this.graph = graph;
this.type = type;
this.id = id; this.id = id;
this.data = data; this.data = data;
this.options = options;
this.edges = { this.edges = {
from: [], from: [],
to: [], to: [],
}; };
} }
getEdges(label, away) { getEdges(type, away) {
return this.edges[away ? 'from' : 'to'].filter( return this.edges[away ? 'from' : 'to'].filter(
(edge) => edge.label === label, (edge) => edge.type === type,
); );
} }
setDisplayLabel(label) {
if (this.options.hide) {
return;
}
this.graph.flowchart?.log(`${this.id}[${label}]`);
}
} }
export class Edge { export class Edge {
constructor(label, from, to, weight) { constructor(graph, type, from, to, weight, data, options = {}) {
this.graph = graph;
this.from = from; this.from = from;
this.to = to; this.to = to;
this.label = label; this.type = type;
this.weight = weight; this.weight = weight;
this.data = data;
this.options = options;
}
getHtml() {
let html = '<table>';
for (const { type, weight } of this.graph.getEdges(null, this.from, this.to)) {
html += `<tr><td>${type}</td><td>${weight}</td></tr>`;
}
html += '</table>';
return html;
}
getFlowchartNode() {
return `${getEdgeKey(this)}(${this.getHtml()})`;
}
displayEdgeNode() {
if (this.options.hide) {
return;
}
this.graph.flowchart?.log(this.getFlowchartNode());
}
displayEdge() {
if (this.options.hide) {
return;
}
this.graph.flowchart?.log(`${this.from.id} --- ${this.getFlowchartNode()} --> ${this.to.id}`);
this.graph.flowchart?.log(`class ${getEdgeKey(this)} edge`);
} }
} }
@ -28,7 +71,7 @@ export class WDAG {
constructor(scene) { constructor(scene) {
this.scene = scene; this.scene = scene;
this.vertices = new Map(); this.vertices = new Map();
this.edgeLabels = new Map(); this.edgeTypes = new Map();
this.nextVertexId = 0; this.nextVertexId = 0;
this.flowchart = scene?.flowchart; this.flowchart = scene?.flowchart;
} }
@ -39,7 +82,7 @@ export class WDAG {
return this; return this;
} }
addVertex(id, data, label) { addVertex(type, id, data, label, options) {
// 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;
@ -48,14 +91,10 @@ export class WDAG {
if (this.vertices.has(id)) { if (this.vertices.has(id)) {
throw new Error(`Vertex already exists with id: ${id}`); throw new Error(`Vertex already exists with id: ${id}`);
} }
const vertex = new Vertex(id, data); const vertex = new Vertex(this, type, id, data, options);
this.vertices.set(id, vertex); this.vertices.set(id, vertex);
this.flowchart?.log(`${id}[${label ?? id}]`); vertex.setDisplayLabel(label ?? id);
return this; return vertex;
}
setVertexLabel(id, label) {
this.flowchart?.log(`${id}[${label}]`);
} }
getVertex(id) { getVertex(id) {
@ -70,74 +109,55 @@ export class WDAG {
return Array.from(this.vertices.values()).map(({ data }) => data); return Array.from(this.vertices.values()).map(({ data }) => data);
} }
static getEdgeKey({ from, to }) { getEdge(type, from, to) {
return btoa([from.id, to.id]).replaceAll(/[^A-Za-z0-9]+/g, '');
}
getEdge(label, from, to) {
from = from instanceof Vertex ? from : this.getVertex(from); from = from instanceof Vertex ? from : this.getVertex(from);
to = to instanceof Vertex ? to : this.getVertex(to); to = to instanceof Vertex ? to : this.getVertex(to);
if (!from || !to) { if (!from || !to) {
return undefined; return undefined;
} }
const edges = this.edgeLabels.get(label); const edges = this.edgeTypes.get(type);
const edgeKey = WDAG.getEdgeKey({ from, to }); const edgeKey = getEdgeKey({ from, to });
return edges?.get(edgeKey); return edges?.get(edgeKey);
} }
getEdgeWeight(label, from, to) { getEdgeWeight(type, from, to) {
return this.getEdge(label, from, to)?.weight; return this.getEdge(type, from, to)?.weight;
} }
getEdgeHtml({ from, to }) { setEdgeWeight(type, from, to, weight, data, options) {
let html = '<table>';
for (const { label, weight } of this.getEdges(null, from, to)) {
html += `<tr><td>${label}</td><td>${weight}</td></tr>`;
}
html += '</table>';
return html;
}
getEdgeFlowchartNode(edge) {
const edgeKey = WDAG.getEdgeKey(edge);
return `${edgeKey}(${this.getEdgeHtml(edge)})`;
}
setEdgeWeight(label, from, to, weight) {
from = from instanceof Vertex ? from : this.getVertex(from); from = from instanceof Vertex ? from : this.getVertex(from);
to = to instanceof Vertex ? to : this.getVertex(to); to = to instanceof Vertex ? to : this.getVertex(to);
const edge = new Edge(label, from, to, weight); const edge = new Edge(this, type, from, to, weight, data, options);
let edges = this.edgeLabels.get(label); let edges = this.edgeTypes.get(type);
if (!edges) { if (!edges) {
edges = new Map(); edges = new Map();
this.edgeLabels.set(label, edges); this.edgeTypes.set(type, edges);
} }
const edgeKey = WDAG.getEdgeKey(edge); const edgeKey = getEdgeKey(edge);
edges.set(edgeKey, edge); edges.set(edgeKey, edge);
this.flowchart?.log(this.getEdgeFlowchartNode(edge)); edge.displayEdgeNode();
return edge; return edge;
} }
addEdge(label, from, to, weight) { addEdge(type, from, to, weight, data, options) {
from = from instanceof Vertex ? from : this.getVertex(from); from = from instanceof Vertex ? from : this.getVertex(from);
to = to instanceof Vertex ? to : this.getVertex(to); to = to instanceof Vertex ? to : this.getVertex(to);
if (this.getEdge(label, from, to)) { if (this.getEdge(type, from, to)) {
throw new Error(`Edge ${label} from ${from.id} to ${to.id} already exists`); throw new Error(`Edge ${type} from ${from.id} to ${to.id} already exists`);
} }
const edge = this.setEdgeWeight(label, from, to, weight); const edge = this.setEdgeWeight(type, from, to, weight, data, options);
from.edges.from.push(edge); from.edges.from.push(edge);
to.edges.to.push(edge); to.edges.to.push(edge);
this.flowchart?.log(`${from.id} --- ${this.getEdgeFlowchartNode(edge)} --> ${to.id}`); edge.displayEdge();
this.flowchart?.log(`class ${WDAG.getEdgeKey(edge)} edge`); return edge;
return this;
} }
getEdges(label, from, to) { getEdges(type, from, to) {
from = from instanceof Vertex ? from : this.getVertex(from); from = from instanceof Vertex ? from : this.getVertex(from);
to = to instanceof Vertex ? to : this.getVertex(to); to = to instanceof Vertex ? to : this.getVertex(to);
const edgeLabels = label ? [label] : Array.from(this.edgeLabels.keys()); const edgeTypes = type ? [type] : Array.from(this.edgeTypes.keys());
return edgeLabels.flatMap((edgeLabel) => { return edgeTypes.flatMap((edgeType) => {
const edges = this.edgeLabels.get(edgeLabel); const edges = this.edgeTypes.get(edgeType);
return Array.from(edges?.values() || []).filter((edge) => { return Array.from(edges?.values() || []).filter((edge) => {
const matchFrom = from === null || from === undefined || from === edge.from; const matchFrom = 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;
@ -146,7 +166,10 @@ export class WDAG {
}); });
} }
countVertices() { countVertices(type) {
if (!type) {
return this.vertices.size; return this.vertices.size;
} }
return Array.from(this.vertices.values()).filter((vertex) => vertex.type === type).length;
}
} }

View File

@ -1,24 +0,0 @@
export class PrioritizedQueue {
constructor() {
this.buffer = [];
}
// Add an item to the buffer, ahead of the next lowest priority item
add(message, priority) {
const idx = this.buffer.findIndex((item) => item.priority < priority);
if (idx < 0) {
this.buffer.push({ message, priority });
} else {
this.buffer.splice(idx, 0, { message, priority });
}
}
// Return the highest priority item in the buffer
pop() {
if (!this.buffer.length) {
return null;
}
const item = this.buffer.shift();
return item.message;
}
}

View File

@ -24,6 +24,8 @@
<li><a href="./tests/forum7.test.html">Negatively cite a zero-valued post</a></li> <li><a href="./tests/forum7.test.html">Negatively cite a zero-valued post</a></li>
<li><a href="./tests/forum8.test.html">Incinerate reputation</a></li> <li><a href="./tests/forum8.test.html">Incinerate reputation</a></li>
<li><a href="./tests/forum9.test.html">Use incineration to achieve more balanced reweighting</a></li> <li><a href="./tests/forum9.test.html">Use incineration to achieve more balanced reweighting</a></li>
<li><a href="./tests/forum10.test.html">Post with multiple authors</a></li>
<li><a href="./tests/forum11.test.html">Multiple posts with overlapping authors</a></li>
</ol> </ol>
</ul> </ul>
<ul> <ul>

View File

@ -36,6 +36,8 @@
<script type="module" src="./scripts/forum/forum7.test.js"></script> <script type="module" src="./scripts/forum/forum7.test.js"></script>
<script type="module" src="./scripts/forum/forum8.test.js"></script> <script type="module" src="./scripts/forum/forum8.test.js"></script>
<script type="module" src="./scripts/forum/forum9.test.js"></script> <script type="module" src="./scripts/forum/forum9.test.js"></script>
<script type="module" src="./scripts/forum/forum10.test.js"></script>
<script type="module" src="./scripts/forum/forum11.test.js"></script>
<script defer class="mocha-init"> <script defer class="mocha-init">
mocha.setup({ mocha.setup({
ui: 'bdd', ui: 'bdd',

View File

@ -14,7 +14,7 @@
import { Scene } from '../classes/display/scene.js'; import { Scene } from '../classes/display/scene.js';
import { Actor } from '../classes/display/actor.js'; import { Actor } from '../classes/display/actor.js';
import { Action } from '../classes/display/action.js'; import { Action } from '../classes/display/action.js';
import { delay } from '../util.js'; import { delay } from '../util/helpers.js';
const DEFAULT_DELAY_INTERVAL = 500; const DEFAULT_DELAY_INTERVAL = 500;
@ -28,7 +28,7 @@
const actor2 = new Actor('B', scene); const actor2 = new Actor('B', scene);
const action1 = new Action('Action 1', scene); const action1 = new Action('Action 1', scene);
await action1.log(actor1, actor2); await action1.log(actor1, actor2);
await actor1.setValue('value', 1); await actor1.setDisplayValue('value', 1);
await scene.withFlowchart(); await scene.withFlowchart();
await scene.flowchart.log('A --> B'); await scene.flowchart.log('A --> B');

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<head>
<title>Forum test 10</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css" />
<link type="text/css" rel="stylesheet" href="../index.css" />
</head>
<body>
<h2><a href="../">DGF Tests</a></h2>
<div id="mocha"></div>
<div id="scene"></div>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/radash/10.7.0/radash.js"
integrity="sha512-S207zKWG3iqXqe6msO7/Mr8X3DzzF4u8meFlokHjGtBPTGUhgzVo0lpcqEy0GoiMUdcoct+H+SqzoLsxXbynzg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://unpkg.com/mocha/mocha.js"></script>
<script src="https://unpkg.com/chai/chai.js"></script>
<script type="module" src="./scripts/forum/forum10.test.js"></script>
<script defer class="mocha-init">
mocha.setup({
ui: 'bdd',
});
chai.should();
</script>

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<head>
<title>Forum test 11</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css" />
<link type="text/css" rel="stylesheet" href="../index.css" />
</head>
<body>
<h2><a href="../">DGF Tests</a></h2>
<div id="mocha"></div>
<div id="scene"></div>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/radash/10.7.0/radash.js"
integrity="sha512-S207zKWG3iqXqe6msO7/Mr8X3DzzF4u8meFlokHjGtBPTGUhgzVo0lpcqEy0GoiMUdcoct+H+SqzoLsxXbynzg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://unpkg.com/mocha/mocha.js"></script>
<script src="https://unpkg.com/chai/chai.js"></script>
<script type="module" src="./scripts/forum/forum11.test.js"></script>
<script defer class="mocha-init">
mocha.setup({
ui: 'bdd',
});
chai.should();
</script>

View File

@ -15,7 +15,7 @@
// import { ValidationPool } from '../classes/validation-pool.js'; // import { ValidationPool } from '../classes/validation-pool.js';
// import { TokenHolder } from '../classes/token-holder.js'; // import { TokenHolder } from '../classes/token-holder.js';
// import { ReputationToken } from '../classes/reputation-token.js'; // import { ReputationToken } from '../classes/reputation-token.js';
import { delay } from '../util.js'; import { delay } from '../util/helpers.js';
const DEFAULT_DELAY_INTERVAL = 500; const DEFAULT_DELAY_INTERVAL = 500;

View File

@ -3,9 +3,9 @@ import { Scene } from '../../classes/display/scene.js';
import { Expert } from '../../classes/actors/expert.js'; import { Expert } from '../../classes/actors/expert.js';
import { DAO } from '../../classes/dao/dao.js'; import { DAO } from '../../classes/dao/dao.js';
import { Public } from '../../classes/actors/public.js'; import { Public } from '../../classes/actors/public.js';
import { PostContent } from '../../classes/util/post-content.js'; import { PostContent } from '../../classes/supporting/post-content.js';
import { delayOrWait } from '../../classes/display/controls.js'; import { delayOrWait } from '../../classes/display/controls.js';
import { mochaRun } from '../../util.js'; import { mochaRun } from '../../util/helpers.js';
const DELAY_INTERVAL = 100; const DELAY_INTERVAL = 100;
const POOL_DURATION = 200; const POOL_DURATION = 200;
@ -18,7 +18,7 @@ const newExpert = async () => {
const index = experts.length; const index = experts.length;
const name = `Expert${index + 1}`; const name = `Expert${index + 1}`;
const expert = await new Expert(dao, name, scene).initialize(); const expert = await new Expert(dao, name, scene).initialize();
expert.setValue( expert.setDisplayValue(
'rep', 'rep',
() => dao.reputation.valueOwnedBy(expert.reputationPublicKey), () => dao.reputation.valueOwnedBy(expert.reputationPublicKey),
); );
@ -36,7 +36,7 @@ const setup = async () => {
scene.withTable(); scene.withTable();
dao = new DAO('DGF', scene); dao = new DAO('DGF', scene);
await dao.setValue('total rep', () => dao.reputation.getTotal()); await dao.setDisplayValue('total rep', () => dao.reputation.getTotal());
experts = []; experts = [];

View File

@ -1,7 +1,7 @@
import { Business } from '../../classes/dao/business.js'; import { Business } from '../../classes/dao/business.js';
import { Scene } from '../../classes/display/scene.js'; import { Scene } from '../../classes/display/scene.js';
import { Box } from '../../classes/display/box.js'; import { Box } from '../../classes/display/box.js';
import { mochaRun } from '../../util.js'; import { mochaRun } from '../../util/helpers.js';
describe('Business', function tests() { describe('Business', function tests() {
this.timeout(0); this.timeout(0);

View File

@ -2,7 +2,7 @@ import { Box } from '../../classes/display/box.js';
import { Scene } from '../../classes/display/scene.js'; import { Scene } from '../../classes/display/scene.js';
import { Action } from '../../classes/display/action.js'; import { Action } from '../../classes/display/action.js';
import { Actor } from '../../classes/display/actor.js'; import { Actor } from '../../classes/display/actor.js';
import { debounce, delay, mochaRun } from '../../util.js'; import { debounce, delay, mochaRun } from '../../util/helpers.js';
describe('Debounce', () => { describe('Debounce', () => {
let scene; let scene;

View File

@ -1,10 +1,10 @@
import { Box } from '../../classes/display/box.js'; import { Box } from '../../classes/display/box.js';
import { Scene } from '../../classes/display/scene.js'; import { Scene } from '../../classes/display/scene.js';
import { PostContent } from '../../classes/util/post-content.js'; import { PostContent } from '../../classes/supporting/post-content.js';
import { Expert } from '../../classes/actors/expert.js'; import { Expert } from '../../classes/actors/expert.js';
import { ForumNode } from '../../classes/forum-network/forum-node.js'; import { ForumNode } from '../../classes/forum-network/forum-node.js';
import { Network } from '../../classes/forum-network/network.js'; import { Network } from '../../classes/forum-network/network.js';
import { mochaRun, randomID } from '../../util.js'; import { mochaRun, randomID } from '../../util/helpers.js';
import { delayOrWait } from '../../classes/display/controls.js'; import { delayOrWait } from '../../classes/display/controls.js';
describe('Forum Network', function tests() { describe('Forum Network', function tests() {

View File

@ -1,7 +1,7 @@
import { Box } from '../../../classes/display/box.js'; import { Box } from '../../../classes/display/box.js';
import { Scene } from '../../../classes/display/scene.js'; import { Scene } from '../../../classes/display/scene.js';
import { Expert } from '../../../classes/actors/expert.js'; import { Expert } from '../../../classes/actors/expert.js';
import { PostContent } from '../../../classes/util/post-content.js'; import { PostContent } from '../../../classes/supporting/post-content.js';
import params from '../../../params.js'; import params from '../../../params.js';
import { DAO } from '../../../classes/dao/dao.js'; import { DAO } from '../../../classes/dao/dao.js';
import { delayOrWait } from '../../../classes/display/controls.js'; import { delayOrWait } from '../../../classes/display/controls.js';
@ -19,17 +19,27 @@ export class ForumTest {
}; };
} }
async addPost(author, fee, citations = []) { async addPost(authors, fee, citations = []) {
const postIndex = this.posts.length; const postIndex = this.posts.length;
const title = `posts[${postIndex}]`; const title = `posts[${postIndex}]`;
await this.scene.sequence.startSection(); await this.scene.sequence.startSection();
const postContent = new PostContent({}).setTitle(title); const postContent = new PostContent().setTitle(title);
const submitter = Array.isArray(authors) ? authors[0].author : authors;
if (Array.isArray(authors)) {
for (const { author, weight } of authors) {
console.log('author', { author, weight });
postContent.addAuthor(author.reputationPublicKey, weight);
}
}
for (const { postId, weight } of citations) { for (const { postId, weight } of citations) {
postContent.addCitation(postId, weight); postContent.addCitation(postId, weight);
} }
const { pool, postId } = await author.submitPostWithFee( const { pool, postId } = await submitter.submitPostWithFee(
postContent, postContent,
{ {
fee, fee,
@ -46,11 +56,15 @@ export class ForumTest {
} }
async newExpert() { async newExpert() {
// Hide by default, for simplicity of rendering first 9 forum tests
const options = {
hide: !this.options.displayAuthors,
announce: this.options.displayAuthors,
};
const index = this.experts.length; const index = this.experts.length;
const name = `Expert${index + 1}`; const name = `Expert${index + 1}`;
const expert = await new Expert(this.dao, name, this.scene).initialize(); const expert = await new Expert(this.dao, name, this.scene, options).initialize();
this.experts.push(expert); this.experts.push(expert);
// await expert.addComputedValue('rep', () => this.dao.reputation.valueOwnedBy(expert.reputationPublicKey));
return expert; return expert;
} }
@ -71,17 +85,14 @@ export class ForumTest {
scene.addDisplayValue('q4. leachingValue').set(params.leachingValue); scene.addDisplayValue('q4. leachingValue').set(params.leachingValue);
scene.addDisplayValue('&nbsp;'); scene.addDisplayValue('&nbsp;');
this.dao = new DAO('DAO', scene); // If we're going to announce experts, announce the DAO so it appears first.
this.dao = new DAO('DAO', scene, { announce: this.options.displayAuthors });
this.forum = this.dao.forum; this.forum = this.dao.forum;
this.experts = []; this.experts = [];
this.posts = []; this.posts = [];
await this.newExpert(); await this.newExpert();
// await newExpert();
// await newExpert();
await this.dao.addComputedValue('total value', () => this.dao.reputation.getTotal()); await this.dao.addComputedValue('total value', () => this.dao.reputation.getTotal());
// await this.dao.addComputedValue('total reputation', () => this.dao.forum.getTotalValue());
this.dao.computeValues();
} }
} }

View File

@ -1,13 +1,15 @@
import { mochaRun } from '../../../util.js'; import { mochaRun } from '../../../util/helpers.js';
import { ForumTest } from './forum.test-util.js'; import { ForumTest } from './forum.test-util.js';
describe('Forum', function tests() { describe('Forum', function tests() {
this.timeout(0); this.timeout(0);
const forumTest = new ForumTest(); const forumTest = new ForumTest({ displayAuthors: false });
before(async () => { before(async () => {
await forumTest.setup(); await forumTest.setup();
await forumTest.newExpert();
await forumTest.newExpert();
}); });
context('Negative citation of a negative citation', async () => { context('Negative citation of a negative citation', async () => {
@ -19,14 +21,14 @@ describe('Forum', function tests() {
it('Post2 negatively cites Post1', async () => { it('Post2 negatively cites Post1', async () => {
const { forum, experts, posts } = forumTest; const { forum, experts, posts } = forumTest;
await forumTest.addPost(experts[0], 10, [{ postId: posts[0], weight: -1 }]); await forumTest.addPost(experts[1], 10, [{ postId: posts[0], weight: -1 }]);
forum.getPost(posts[0]).value.should.equal(0); forum.getPost(posts[0]).value.should.equal(0);
forum.getPost(posts[1]).value.should.equal(20); forum.getPost(posts[1]).value.should.equal(20);
}); });
it('Post3 negatively cites Post2, restoring Post1 post to its initial value', async () => { it('Post3 negatively cites Post2, restoring Post1 post to its initial value', async () => {
const { forum, experts, posts } = forumTest; const { forum, experts, posts } = forumTest;
await forumTest.addPost(experts[0], 10, [{ postId: posts[1], weight: -1 }]); await forumTest.addPost(experts[2], 10, [{ postId: posts[1], weight: -1 }]);
forum.getPost(posts[0]).value.should.equal(10); forum.getPost(posts[0]).value.should.equal(10);
forum.getPost(posts[1]).value.should.equal(0); forum.getPost(posts[1]).value.should.equal(0);
forum.getPost(posts[2]).value.should.equal(20); forum.getPost(posts[2]).value.should.equal(20);
@ -34,11 +36,4 @@ describe('Forum', function tests() {
}); });
}); });
// await addPost(experts[0], 10);
// await addPost(experts[0], 10, [{ postId: posts[3], weight: -1 }]);
// await addPost(experts[0], 10, [{ postId: posts[4], weight: -1 }]);
// await addPost(expert3, 'Post 4', 100, [{ postId: postId2, weight: -1 }]);
// await addPost(expert1, 'Post 5', 100, [{ postId: postId3, weight: -1 }]);
mochaRun(); mochaRun();

View File

@ -0,0 +1,44 @@
import { mochaRun } from '../../../util/helpers.js';
import { ForumTest } from './forum.test-util.js';
describe('Forum', function tests() {
this.timeout(0);
const forumTest = new ForumTest({ displayAuthors: true });
before(async () => {
await forumTest.setup();
await forumTest.newExpert();
await forumTest.newExpert();
});
context('Post with multiple authors', async () => {
let forum;
let experts;
let posts;
let dao;
before(async () => {
forum = forumTest.forum;
experts = forumTest.experts;
posts = forumTest.posts;
dao = forumTest.dao;
});
it('Post1 has three authors and reputation is distributed among them', async () => {
const authors = [
{ author: experts[0], weight: 0.5 },
{ author: experts[1], weight: 0.25 },
{ author: experts[2], weight: 0.25 },
];
await forumTest.addPost(authors, 10);
forum.getPost(posts[0]).value.should.equal(10);
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(5);
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(2.5);
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey).should.equal(2.5);
});
});
});
mochaRun();

View File

@ -0,0 +1,56 @@
import { mochaRun } from '../../../util/helpers.js';
import { ForumTest } from './forum.test-util.js';
describe('Forum', function tests() {
this.timeout(0);
const forumTest = new ForumTest({ displayAuthors: true });
before(async () => {
await forumTest.setup();
await forumTest.newExpert();
await forumTest.newExpert();
});
context('Multiple posts with overlapping authors', async () => {
let forum;
let experts;
let posts;
let dao;
before(async () => {
forum = forumTest.forum;
experts = forumTest.experts;
posts = forumTest.posts;
dao = forumTest.dao;
});
it('Post1 with two authors', async () => {
const authors = [
{ author: experts[0], weight: 0.5 },
{ author: experts[1], weight: 0.5 },
];
await forumTest.addPost(authors, 10);
forum.getPost(posts[0]).value.should.equal(10);
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(5);
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(5);
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey).should.equal(0);
});
it('Post2 with two authors, one shared with Post1', async () => {
const authors = [
{ author: experts[1], weight: 0.5 },
{ author: experts[2], weight: 0.5 },
];
await forumTest.addPost(authors, 10);
forum.getPost(posts[0]).value.should.equal(10);
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(5);
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(10);
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey).should.equal(5);
});
});
});
mochaRun();

View File

@ -1,4 +1,4 @@
import { mochaRun } from '../../../util.js'; import { mochaRun } from '../../../util/helpers.js';
import { ForumTest } from './forum.test-util.js'; import { ForumTest } from './forum.test-util.js';
describe('Forum', function tests() { describe('Forum', function tests() {

View File

@ -1,4 +1,4 @@
import { mochaRun } from '../../../util.js'; import { mochaRun } from '../../../util/helpers.js';
import { ForumTest } from './forum.test-util.js'; import { ForumTest } from './forum.test-util.js';
describe('Forum', function tests() { describe('Forum', function tests() {

View File

@ -1,4 +1,4 @@
import { mochaRun } from '../../../util.js'; import { mochaRun } from '../../../util/helpers.js';
import { ForumTest } from './forum.test-util.js'; import { ForumTest } from './forum.test-util.js';
describe('Forum', function tests() { describe('Forum', function tests() {

View File

@ -1,4 +1,4 @@
import { mochaRun } from '../../../util.js'; import { mochaRun } from '../../../util/helpers.js';
import { ForumTest } from './forum.test-util.js'; import { ForumTest } from './forum.test-util.js';
describe('Forum', function tests() { describe('Forum', function tests() {

View File

@ -1,4 +1,5 @@
import { EPSILON, mochaRun } from '../../../util.js'; import { mochaRun } from '../../../util/helpers.js';
import { EPSILON } from '../../../util/constants.js';
import { ForumTest } from './forum.test-util.js'; import { ForumTest } from './forum.test-util.js';
describe('Forum', function tests() { describe('Forum', function tests() {

View File

@ -1,4 +1,4 @@
import { mochaRun } from '../../../util.js'; import { mochaRun } from '../../../util/helpers.js';
import { ForumTest } from './forum.test-util.js'; import { ForumTest } from './forum.test-util.js';
describe('Forum', function tests() { describe('Forum', function tests() {

View File

@ -1,4 +1,5 @@
import { INCINERATOR_ADDRESS, mochaRun } from '../../../util.js'; import { mochaRun } from '../../../util/helpers.js';
import { INCINERATOR_ADDRESS } from '../../../util/constants.js';
import { ForumTest } from './forum.test-util.js'; import { ForumTest } from './forum.test-util.js';
describe('Forum', function tests() { describe('Forum', function tests() {

View File

@ -1,4 +1,5 @@
import { INCINERATOR_ADDRESS, mochaRun } from '../../../util.js'; import { mochaRun } from '../../../util/helpers.js';
import { INCINERATOR_ADDRESS } from '../../../util/constants.js';
import { ForumTest } from './forum.test-util.js'; import { ForumTest } from './forum.test-util.js';
describe('Forum', function tests() { describe('Forum', function tests() {

View File

@ -1,10 +1,10 @@
import { Box } from '../../classes/display/box.js'; import { Box } from '../../classes/display/box.js';
import { Scene } from '../../classes/display/scene.js'; import { Scene } from '../../classes/display/scene.js';
import { Expert } from '../../classes/actors/expert.js'; import { Expert } from '../../classes/actors/expert.js';
import { PostContent } from '../../classes/util/post-content.js'; import { PostContent } from '../../classes/supporting/post-content.js';
import { DAO } from '../../classes/dao/dao.js'; import { DAO } from '../../classes/dao/dao.js';
import { delayOrWait } from '../../classes/display/controls.js'; import { delayOrWait } from '../../classes/display/controls.js';
import { mochaRun } from '../../util.js'; import { mochaRun } from '../../util/helpers.js';
const POOL_DURATION_MS = 100; const POOL_DURATION_MS = 100;
const DEFAULT_DELAY_MS = 100; const DEFAULT_DELAY_MS = 100;

View File

@ -2,7 +2,7 @@ import { Actor } from '../../classes/display/actor.js';
import { Box } from '../../classes/display/box.js'; import { Box } from '../../classes/display/box.js';
import { Scene } from '../../classes/display/scene.js'; import { Scene } from '../../classes/display/scene.js';
import { VM } from '../../classes/supporting/vm.js'; import { VM } from '../../classes/supporting/vm.js';
import { mochaRun } from '../../util.js'; import { mochaRun } from '../../util/helpers.js';
const contractIds = ['contract-id-1', 'contract-id-2']; const contractIds = ['contract-id-1', 'contract-id-2'];

View File

@ -1,7 +1,7 @@
import { Box } from '../../classes/display/box.js'; import { Box } from '../../classes/display/box.js';
import { Scene } from '../../classes/display/scene.js'; import { Scene } from '../../classes/display/scene.js';
import { WDAG } from '../../classes/supporting/wdag.js'; import { WDAG } from '../../classes/supporting/wdag.js';
import { mochaRun } from '../../util.js'; import { mochaRun } from '../../util/helpers.js';
const rootElement = document.getElementById('scene'); const rootElement = document.getElementById('scene');
const rootBox = new Box('rootBox', rootElement).flex(); const rootBox = new Box('rootBox', rootElement).flex();
@ -15,11 +15,11 @@ describe('Query the graph', function tests() {
before(() => { before(() => {
graph = (window.graph = new WDAG()).withFlowchart(); graph = (window.graph = new WDAG()).withFlowchart();
graph.addVertex({}); graph.addVertex('v1', {});
graph.addVertex({}); graph.addVertex('v1', {});
graph.addVertex({}); graph.addVertex('v1', {});
graph.addVertex({}); graph.addVertex('v1', {});
graph.addVertex({}); graph.addVertex('v1', {});
graph.addEdge('e1', 0, 1, 1); graph.addEdge('e1', 0, 1, 1);
graph.addEdge('e1', 2, 1, 0.5); graph.addEdge('e1', 2, 1, 0.5);

View File

@ -0,0 +1,14 @@
export const EPSILON = 2.23e-16;
export const INCINERATOR_ADDRESS = 0;
export const EdgeTypes = {
CITATION: 'citation',
BALANCE: 'balance',
AUTHOR: 'author',
};
export const VertexTypes = {
POST: 'post',
AUTHOR: 'author',
};

View File

@ -1,11 +1,5 @@
import { CryptoUtil } from './classes/util/crypto.js';
const timers = new Map(); const timers = new Map();
export const EPSILON = 2.23e-16;
export const INCINERATOR_ADDRESS = 0;
export const debounce = async (fn, delayMs) => { export const debounce = async (fn, delayMs) => {
const timer = timers.get(fn); const timer = timers.get(fn);
if (timer) { if (timer) {
@ -39,7 +33,7 @@ export const displayNumber = (value, decimals = 2) => (value.toString().length >
? value.toFixed(decimals) ? value.toFixed(decimals)
: value); : value);
export const randomID = () => CryptoUtil.randomUUID().replaceAll('-', '').slice(0, 8); export const randomID = () => window.crypto.randomUUID().replaceAll('-', '').slice(0, 8);
export const mochaRun = () => { export const mochaRun = () => {
if (mocha._state !== 'running') { if (mocha._state !== 'running') {