Merge branch 'dev' into 'main'

Add support for posts with multiple authors

See merge request dao-governance-framework/science-publishing-dao!5
This commit is contained in:
Ladd Hoffman 2023-04-17 01:41:13 +00:00
commit 92bbab2a5b
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?
what about for governance voting --
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"
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 { 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 { EdgeTypes } from '../../util/constants.js';
import { displayNumber } from '../../util/helpers.js';
export class Expert extends ReputationHolder {
constructor(dao, name, scene) {
super(name, scene);
constructor(dao, name, scene, options) {
super(name, scene, options);
this.dao = dao;
this.actions = {
submitPostViaNetwork: new Action('submit post via network', scene),
@ -20,6 +22,25 @@ export class Expert extends ReputationHolder {
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() {
this.reputationKey = await CryptoUtil.generateAsymmetricKey();
// this.reputationPublicKey = await CryptoUtil.exportKey(this.reputationKey.publicKey);
@ -28,15 +49,6 @@ export class Expert extends ReputationHolder {
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) {
const post = await this.dao.forum.addPost(this.reputationPublicKey, postContent);
await this.actions.submitPost.log(this, post);
@ -49,14 +61,9 @@ export class Expert extends ReputationHolder {
async initiateValidationPool(poolOptions) {
// For now, make direct call rather than network
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.validationPools.set(pool.id, poolOptions);
await this.actions.initiateValidationPool.log(
this,
pool,
`(fee: ${poolOptions.fee}, stake: ${poolOptions.authorStakeAmount ?? 0})`,
);
return pool;
}

View File

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

View File

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

View File

@ -3,20 +3,22 @@ import { Action } from '../display/action.js';
import { Actor } from '../display/actor.js';
import params from '../../params.js';
import { ReputationHolder } from '../reputation/reputation-holder.js';
import { displayNumber, EPSILON, INCINERATOR_ADDRESS } from '../../util.js';
const CITATION = 'citation';
const BALANCE = 'balance';
import { displayNumber } from '../../util/helpers.js';
import {
EPSILON, INCINERATOR_ADDRESS, EdgeTypes, VertexTypes,
} from '../../util/constants.js';
class Post extends Actor {
constructor(forum, authorPublicKey, postContent) {
const index = forum.posts.countVertices();
constructor(forum, senderId, postContent) {
const index = forum.graph.countVertices(VertexTypes.POST);
const name = `Post${index + 1}`;
super(name, forum.scene);
this.forum = forum;
this.id = postContent.id ?? name;
this.authorPublicKey = authorPublicKey;
this.senderId = senderId;
this.value = 0;
this.initialValue = 0;
this.authors = postContent.authors;
this.citations = postContent.citations;
this.title = postContent.title;
const leachingTotal = this.citations
@ -25,6 +27,8 @@ class Post extends Actor {
const donationTotal = this.citations
.filter(({ weight }) => weight > 0)
.reduce((total, { weight }) => total += weight, 0);
// TODO: Move evaluation of these parameters to Validation Pool
if (leachingTotal > params.revaluationLimit) {
throw new Error('Post leaching total exceeds revaluation limit '
+ `(${leachingTotal} > ${params.revaluationLimit})`);
@ -49,6 +53,12 @@ class Post extends Actor {
</tr></table>`
.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);
this.dao = dao;
this.id = this.reputationPublicKey;
this.posts = new WDAG(scene);
this.graph = new WDAG(scene);
this.actions = {
propagate: new Action('propagate', scene),
confirm: new Action('confirm', scene),
@ -69,73 +79,120 @@ export class Forum extends ReputationHolder {
};
}
async addPost(authorId, postContent) {
console.log('addPost', { authorId, postContent });
const post = new Post(this, authorId, postContent);
this.posts.addVertex(post.id, post, post.getLabel());
async addPost(senderId, postContent) {
console.log('addPost', { senderId, postContent });
const post = new Post(this, senderId, postContent);
this.graph.addVertex(VertexTypes.POST, post.id, post, post.getLabel());
for (const { postId: citedPostId, weight } of post.citations) {
// Special case: Incinerator
if (citedPostId === INCINERATOR_ADDRESS && !this.posts.getVertex(INCINERATOR_ADDRESS)) {
this.posts.addVertex(INCINERATOR_ADDRESS, { name: 'Incinerator' }, 'Incinerator');
if (citedPostId === INCINERATOR_ADDRESS && !this.graph.getVertex(INCINERATOR_ADDRESS)) {
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;
}
getPost(postId) {
return this.posts.getVertexData(postId);
return this.graph.getVertexData(postId);
}
getPosts() {
return this.posts.getVerticesData();
}
async setPostValue(post, value) {
post.value = value;
await post.setValue('value', value);
this.posts.setVertexLabel(post.id, post.getLabel());
return this.graph.getVerticesData();
}
getTotalValue() {
return this.getPosts().reduce((total, { value }) => total += value, 0);
}
// getLatestContract(type) { }
// getContract(type) { }
async onValidate({
pool, postId, tokenId,
}) {
console.log('onValidate', { pool, postId, tokenId });
const initialValue = this.dao.reputation.valueOf(tokenId);
const postVertex = this.posts.getVertex(postId);
const postVertex = this.graph.getVertex(postId);
const post = postVertex.data;
post.setStatus('Validated');
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,
// so that its value can be updated by future validated posts.
post.tokenId = tokenId;
const addAuthorToGraph = (publicKey, weight, authorTokenId) => {
// For graph display purposes, we want to use the existing Expert actors from the current scene.
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();
// Compute rewards
// Compute reputation rewards
await this.propagateValue(
{ to: postVertex, from: { data: pool } },
{ rewardsAccumulator, increment: initialValue },
);
// Apply computed rewards to update values of tokens
for (const [id, value] of rewardsAccumulator) {
if (value < 0) {
this.dao.reputation.transferValueFrom(id, post.tokenId, -value);
for (const [authorEdge, amount] of rewardsAccumulator) {
const { to: authorVertex, data: { tokenId: authorTokenId } } = authorEdge;
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 {
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
this.dao.reputation.transfer(this.id, post.authorPublicKey, post.tokenId);
// const toActor = this.scene?.findActor((actor) => actor.reputationPublicKey === post.authorPublicKey);
// const value = this.dao.reputation.valueOf(post.tokenId);
const senderVertex = this.graph.getVertex(post.senderId);
const { data: sender } = senderVertex;
await sender.computeDisplayValues();
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,
}) {
const postVertex = edge.to;
const post = postVertex?.data;
const post = postVertex.data;
this.actions.propagate.log(edge.from.data, post, `(${increment})`);
if (!!params.referenceChainLimit && depth > params.referenceChainLimit) {
@ -171,13 +228,14 @@ export class Forum extends ReputationHolder {
const propagate = async (positive) => {
let totalOutboundAmount = 0;
const citationEdges = postVertex.getEdges(CITATION, true)
const citationEdges = postVertex.getEdges(EdgeTypes.CITATION, true)
.filter(({ weight }) => (positive ? weight > 0 : weight < 0));
for (const citationEdge of citationEdges) {
const { weight } = citationEdge;
let outboundAmount = weight * increment;
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;
// 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,
// 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;
this.actions.confirm.log(
@ -250,6 +313,17 @@ export class Forum extends ReputationHolder {
const appliedIncrement = newValue - post.value;
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', {
depth,
increment,
@ -259,12 +333,6 @@ export class Forum extends ReputationHolder {
refundToInbound,
});
// Award reputation to post author
rewardsAccumulator.set(post.tokenId, appliedIncrement);
// Increment the value of the post
await this.setPostValue(post, newValue);
return refundToInbound;
}
}

View File

@ -3,7 +3,7 @@ import { Stake } from '../supporting/stake.js';
import { Voter } from '../supporting/voter.js';
import params from '../../params.js';
import { Action } from '../display/action.js';
import { displayNumber } from '../../util.js';
import { displayNumber } from '../../util/helpers.js';
const ValidationPoolStates = Object.freeze({
OPEN: 'OPEN',
@ -27,16 +27,21 @@ export class ValidationPool extends ReputationHolder {
},
name,
scene,
fromActor,
) {
super(name, scene);
this.id = this.reputationPublicKey;
this.actions = {
initiate: new Action('initiate validation pool', scene),
reward: new Action('reward', scene),
transfer: new Action('transfer', 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
@ -90,8 +95,6 @@ export class ValidationPool extends ReputationHolder {
const voter = this.dao.experts.get(reputationPublicKey) ?? new Voter(reputationPublicKey);
voter.addVoteRecord(this);
this.dao.experts.set(reputationPublicKey, voter);
this.activate();
}
getTokenLossRatio() {
@ -180,7 +183,7 @@ export class ValidationPool extends ReputationHolder {
// Update computed display values
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) {
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`);

View File

@ -1,7 +1,13 @@
import { displayNumber } from '../../util.js';
import { displayNumber } from '../../util/helpers.js';
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!');
this.name = name;
this.scene = scene;
@ -11,6 +17,7 @@ export class Actor {
this.values = new Map();
this.valueFunctions = new Map();
this.active = 0;
this.options = options;
scene?.registerActor(this);
}
@ -58,12 +65,12 @@ export class Actor {
this.values.set(label, this.scene?.addDisplayValue(`${this.name} ${label}`));
if (fn) {
this.valueFunctions.set(label, fn);
await this.computeValues();
await this.computeDisplayValues();
}
return this;
}
async setValue(label, value) {
async setDisplayValue(label, value) {
if (typeof value === 'function') {
return this.addComputedValue(label, value);
}
@ -76,10 +83,10 @@ export class Actor {
return this;
}
async computeValues() {
async computeDisplayValues() {
for (const [label, fn] of this.valueFunctions.entries()) {
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 { randomID } from '../../util.js';
import { randomID } from '../../util/helpers.js';
export class Box {
constructor(name, parentEl, options = {}) {

View File

@ -1,4 +1,4 @@
import { displayNumber } from '../../util.js';
import { displayNumber } from '../../util/helpers.js';
export class DisplayValue {
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 { debounce } from '../../util.js';
import { debounce } from '../../util/helpers.js';
export class MermaidDiagram {
constructor(box, logBox) {

View File

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

View File

@ -1,4 +1,4 @@
import { hexToRGB } from '../../util.js';
import { hexToRGB } from '../../util/helpers.js';
import { MermaidDiagram } from './mermaid.js';
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 {
constructor(box) {

View File

@ -2,14 +2,12 @@ import { Action } from '../display/action.js';
import {
Message, PostMessage, PeerMessage, messageFromJSON,
} from './message.js';
import { ForumView } from './forum-view.js';
import { NetworkNode } from './network-node.js';
import { randomID } from '../util/util.js';
import { randomID } from '../../util/helpers.js';
export class ForumNode extends NetworkNode {
constructor(name, scene) {
super(name, scene);
this.forumView = new ForumView();
this.actions = {
...this.actions,
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 { PostContent } from '../util/post-content.js';
import { CryptoUtil } from '../supporting/crypto.js';
import { PostContent } from '../supporting/post-content.js';
export class Message {
constructor(content) {

View File

@ -38,7 +38,8 @@ export class NetworkNode extends Actor {
// Enqueue it for further processing.
async receiveMessage(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);
}

View File

@ -1,4 +1,4 @@
import { randomID } from '../util/util.js';
import { randomID } from '../util/util/helpers.js';
class Pledge {
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';
export class ReputationHolder extends Actor {
constructor(name, scene) {
super(name, scene);
constructor(name, scene, options) {
super(name, scene, options);
this.reputationPublicKey = `${name}_${randomID()}`;
}
}

View File

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

View File

@ -31,6 +31,7 @@ export class ERC721 {
}
mint(to, tokenId) {
console.log('ERC721.mint', { to, tokenId });
if (this.owners.get(tokenId)) {
throw new Error('ERC721: token already minted');
}
@ -60,9 +61,10 @@ export class ERC721 {
}
transfer(from, to, tokenId) {
console.log('ERC721.transfer', { from, to, tokenId });
const owner = this.owners.get(tokenId);
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(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) {
this.postId = postId;
this.weight = weight;
@ -17,11 +35,18 @@ export class Citation {
}
export class PostContent {
constructor(content) {
constructor(content = {}) {
this.content = content;
this.authors = [];
this.citations = [];
}
addAuthor(authorPublicKey, weight) {
const author = new Author(authorPublicKey, weight);
this.authors.push(author);
return this;
}
addCitation(postId, weight) {
const citation = new Citation(postId, weight);
this.citations.push(citation);
@ -36,6 +61,7 @@ export class PostContent {
toJSON() {
return {
content: this.content,
authors: this.authors.map((author) => author.toJSON()),
citations: this.citations.map((citation) => citation.toJSON()),
...(this.id ? { id: this.id } : {}),
title: this.title,
@ -43,9 +69,10 @@ export class PostContent {
}
static fromJSON({
id, content, citations, title,
id, content, authors, citations, title,
}) {
const post = new PostContent(content);
post.authors = authors.map((author) => Author.fromJSON(author));
post.citations = citations.map((citation) => Citation.fromJSON(citation));
post.id = id;
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 {
constructor(id, data) {
constructor(graph, type, id, data, options = {}) {
this.graph = graph;
this.type = type;
this.id = id;
this.data = data;
this.options = options;
this.edges = {
from: [],
to: [],
};
}
getEdges(label, away) {
getEdges(type, away) {
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 {
constructor(label, from, to, weight) {
constructor(graph, type, from, to, weight, data, options = {}) {
this.graph = graph;
this.from = from;
this.to = to;
this.label = label;
this.type = type;
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) {
this.scene = scene;
this.vertices = new Map();
this.edgeLabels = new Map();
this.edgeTypes = new Map();
this.nextVertexId = 0;
this.flowchart = scene?.flowchart;
}
@ -39,7 +82,7 @@ export class WDAG {
return this;
}
addVertex(id, data, label) {
addVertex(type, id, data, label, options) {
// Support simple case of auto-incremented numeric ids
if (typeof id === 'object') {
data = id;
@ -48,14 +91,10 @@ export class WDAG {
if (this.vertices.has(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.flowchart?.log(`${id}[${label ?? id}]`);
return this;
}
setVertexLabel(id, label) {
this.flowchart?.log(`${id}[${label}]`);
vertex.setDisplayLabel(label ?? id);
return vertex;
}
getVertex(id) {
@ -70,74 +109,55 @@ export class WDAG {
return Array.from(this.vertices.values()).map(({ data }) => data);
}
static getEdgeKey({ from, to }) {
return btoa([from.id, to.id]).replaceAll(/[^A-Za-z0-9]+/g, '');
}
getEdge(label, from, to) {
getEdge(type, from, to) {
from = from instanceof Vertex ? from : this.getVertex(from);
to = to instanceof Vertex ? to : this.getVertex(to);
if (!from || !to) {
return undefined;
}
const edges = this.edgeLabels.get(label);
const edgeKey = WDAG.getEdgeKey({ from, to });
const edges = this.edgeTypes.get(type);
const edgeKey = getEdgeKey({ from, to });
return edges?.get(edgeKey);
}
getEdgeWeight(label, from, to) {
return this.getEdge(label, from, to)?.weight;
getEdgeWeight(type, from, to) {
return this.getEdge(type, from, to)?.weight;
}
getEdgeHtml({ from, to }) {
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) {
setEdgeWeight(type, from, to, weight, data, options) {
from = from instanceof Vertex ? from : this.getVertex(from);
to = to instanceof Vertex ? to : this.getVertex(to);
const edge = new Edge(label, from, to, weight);
let edges = this.edgeLabels.get(label);
const edge = new Edge(this, type, from, to, weight, data, options);
let edges = this.edgeTypes.get(type);
if (!edges) {
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);
this.flowchart?.log(this.getEdgeFlowchartNode(edge));
edge.displayEdgeNode();
return edge;
}
addEdge(label, from, to, weight) {
addEdge(type, from, to, weight, data, options) {
from = from instanceof Vertex ? from : this.getVertex(from);
to = to instanceof Vertex ? to : this.getVertex(to);
if (this.getEdge(label, from, to)) {
throw new Error(`Edge ${label} from ${from.id} to ${to.id} already exists`);
if (this.getEdge(type, from, to)) {
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);
to.edges.to.push(edge);
this.flowchart?.log(`${from.id} --- ${this.getEdgeFlowchartNode(edge)} --> ${to.id}`);
this.flowchart?.log(`class ${WDAG.getEdgeKey(edge)} edge`);
return this;
edge.displayEdge();
return edge;
}
getEdges(label, from, to) {
getEdges(type, from, to) {
from = from instanceof Vertex ? from : this.getVertex(from);
to = to instanceof Vertex ? to : this.getVertex(to);
const edgeLabels = label ? [label] : Array.from(this.edgeLabels.keys());
return edgeLabels.flatMap((edgeLabel) => {
const edges = this.edgeLabels.get(edgeLabel);
const edgeTypes = type ? [type] : Array.from(this.edgeTypes.keys());
return edgeTypes.flatMap((edgeType) => {
const edges = this.edgeTypes.get(edgeType);
return Array.from(edges?.values() || []).filter((edge) => {
const matchFrom = from === null || from === undefined || from === edge.from;
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 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/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/forum10.test.html">Post with multiple authors</a></li>
<li><a href="./tests/forum11.test.html">Multiple posts with overlapping authors</a></li>
</ol>
</ul>
<ul>

View File

@ -36,6 +36,8 @@
<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/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">
mocha.setup({
ui: 'bdd',

View File

@ -14,7 +14,7 @@
import { Scene } from '../classes/display/scene.js';
import { Actor } from '../classes/display/actor.js';
import { Action } from '../classes/display/action.js';
import { delay } from '../util.js';
import { delay } from '../util/helpers.js';
const DEFAULT_DELAY_INTERVAL = 500;
@ -28,7 +28,7 @@
const actor2 = new Actor('B', scene);
const action1 = new Action('Action 1', scene);
await action1.log(actor1, actor2);
await actor1.setValue('value', 1);
await actor1.setDisplayValue('value', 1);
await scene.withFlowchart();
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 { TokenHolder } from '../classes/token-holder.js';
// import { ReputationToken } from '../classes/reputation-token.js';
import { delay } from '../util.js';
import { delay } from '../util/helpers.js';
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 { DAO } from '../../classes/dao/dao.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 { mochaRun } from '../../util.js';
import { mochaRun } from '../../util/helpers.js';
const DELAY_INTERVAL = 100;
const POOL_DURATION = 200;
@ -18,7 +18,7 @@ const newExpert = async () => {
const index = experts.length;
const name = `Expert${index + 1}`;
const expert = await new Expert(dao, name, scene).initialize();
expert.setValue(
expert.setDisplayValue(
'rep',
() => dao.reputation.valueOwnedBy(expert.reputationPublicKey),
);
@ -36,7 +36,7 @@ const setup = async () => {
scene.withTable();
dao = new DAO('DGF', scene);
await dao.setValue('total rep', () => dao.reputation.getTotal());
await dao.setDisplayValue('total rep', () => dao.reputation.getTotal());
experts = [];

View File

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

View File

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

View File

@ -1,10 +1,10 @@
import { Box } from '../../classes/display/box.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 { ForumNode } from '../../classes/forum-network/forum-node.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';
describe('Forum Network', function tests() {

View File

@ -1,7 +1,7 @@
import { Box } from '../../../classes/display/box.js';
import { Scene } from '../../../classes/display/scene.js';
import { Expert } from '../../../classes/actors/expert.js';
import { PostContent } from '../../../classes/util/post-content.js';
import { PostContent } from '../../../classes/supporting/post-content.js';
import params from '../../../params.js';
import { DAO } from '../../../classes/dao/dao.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 title = `posts[${postIndex}]`;
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) {
postContent.addCitation(postId, weight);
}
const { pool, postId } = await author.submitPostWithFee(
const { pool, postId } = await submitter.submitPostWithFee(
postContent,
{
fee,
@ -46,11 +56,15 @@ export class ForumTest {
}
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 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);
// await expert.addComputedValue('rep', () => this.dao.reputation.valueOwnedBy(expert.reputationPublicKey));
return expert;
}
@ -71,17 +85,14 @@ export class ForumTest {
scene.addDisplayValue('q4. leachingValue').set(params.leachingValue);
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.experts = [];
this.posts = [];
await this.newExpert();
// await newExpert();
// await newExpert();
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';
describe('Forum', function tests() {
this.timeout(0);
const forumTest = new ForumTest();
const forumTest = new ForumTest({ displayAuthors: false });
before(async () => {
await forumTest.setup();
await forumTest.newExpert();
await forumTest.newExpert();
});
context('Negative citation of a negative citation', async () => {
@ -19,14 +21,14 @@ describe('Forum', function tests() {
it('Post2 negatively cites Post1', async () => {
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[1]).value.should.equal(20);
});
it('Post3 negatively cites Post2, restoring Post1 post to its initial value', async () => {
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[1]).value.should.equal(0);
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();

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';
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';
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';
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';
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';
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';
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';
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';
describe('Forum', function tests() {

View File

@ -1,10 +1,10 @@
import { Box } from '../../classes/display/box.js';
import { Scene } from '../../classes/display/scene.js';
import { Expert } from '../../classes/actors/expert.js';
import { PostContent } from '../../classes/util/post-content.js';
import { PostContent } from '../../classes/supporting/post-content.js';
import { DAO } from '../../classes/dao/dao.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 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 { Scene } from '../../classes/display/scene.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'];

View File

@ -1,7 +1,7 @@
import { Box } from '../../classes/display/box.js';
import { Scene } from '../../classes/display/scene.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 rootBox = new Box('rootBox', rootElement).flex();
@ -15,11 +15,11 @@ describe('Query the graph', function tests() {
before(() => {
graph = (window.graph = new WDAG()).withFlowchart();
graph.addVertex({});
graph.addVertex({});
graph.addVertex({});
graph.addVertex({});
graph.addVertex({});
graph.addVertex('v1', {});
graph.addVertex('v1', {});
graph.addVertex('v1', {});
graph.addVertex('v1', {});
graph.addVertex('v1', {});
graph.addEdge('e1', 0, 1, 1);
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();
export const EPSILON = 2.23e-16;
export const INCINERATOR_ADDRESS = 0;
export const debounce = async (fn, delayMs) => {
const timer = timers.get(fn);
if (timer) {
@ -39,7 +33,7 @@ export const displayNumber = (value, decimals = 2) => (value.toString().length >
? value.toFixed(decimals)
: value);
export const randomID = () => CryptoUtil.randomUUID().replaceAll('-', '').slice(0, 8);
export const randomID = () => window.crypto.randomUUID().replaceAll('-', '').slice(0, 8);
export const mochaRun = () => {
if (mocha._state !== 'running') {