Add support for posts with multiple authors

This commit is contained in:
Ladd Hoffman 2023-04-15 11:50:57 -05:00
parent 0a954a01f3
commit 7ae5ff9b03
18 changed files with 278 additions and 175 deletions

View File

@ -5,18 +5,28 @@ 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, EPSILON, INCINERATOR_ADDRESS } from '../../util.js';
const CITATION = 'citation'; const EdgeTypes = {
const BALANCE = 'balance'; CITATION: 'citation',
BALANCE: 'balance',
AUTHOR: 'author',
};
const VertexTypes = {
POST: 'post',
AUTHOR: 'author',
};
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 +35,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 +61,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.setVertexDisplayLabel(this.id, this.getLabel());
}
} }
/** /**
@ -61,7 +79,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,32 +87,26 @@ 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() {
@ -108,38 +120,74 @@ export class Forum extends ReputationHolder {
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()); this.graph.setVertexDisplayLabel(post.id, 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. const authorVertex = this.graph.getVertex(publicKey)
post.tokenId = tokenId; ?? this.graph.addVertex(VertexTypes.AUTHOR, publicKey, { name: publicKey, publicKey }, publicKey);
const authorEdge = this.graph.addEdge(
EdgeTypes.AUTHOR,
postVertex,
authorVertex,
weight,
{ tokenId: authorTokenId },
);
console.log('addAuthorToGraph', { authorVertex, authorEdge });
};
// In the case of multiple authors, mint additional (empty) tokens.
// If no authors are specified, treat the sender as the sole author.
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.
if (publicKey === post.senderId) {
addAuthorToGraph(publicKey, weight, tokenId);
} else {
addAuthorToGraph(publicKey, weight, this.dao.reputation.mint(this.id, 0));
}
}
}
// TODO: Verify that cumulative author weight === 1
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 [authorTokenId, amount] of rewardsAccumulator) {
if (value < 0) { console.log('reward', { authorTokenId, amount });
this.dao.reputation.transferValueFrom(id, post.tokenId, -value); // The primary author gets the validation pool minted token.
} else { // So we don't need to transfer any reputation to the primary author.
this.dao.reputation.transferValueFrom(post.tokenId, id, value); // 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(tokenId, authorTokenId, amount);
}
} }
} }
// Transfer ownership of the minted/staked token, from the posts to the post author // Transfer ownership of the minted tokens to the authors
this.dao.reputation.transfer(this.id, post.authorPublicKey, post.tokenId); for (const authorEdge of postVertex.getEdges(EdgeTypes.AUTHOR, true)) {
// const toActor = this.scene?.findActor((actor) => actor.reputationPublicKey === post.authorPublicKey); const authorVertex = authorEdge.to;
// const value = this.dao.reputation.valueOf(post.tokenId); const { publicKey } = authorVertex.data;
const { tokenId: authorTokenId } = authorEdge.data;
this.dao.reputation.transfer(this.id, publicKey, authorTokenId);
}
} }
/** /**
@ -150,7 +198,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) {
@ -175,13 +223,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.
@ -225,7 +274,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(
@ -263,11 +317,15 @@ export class Forum extends ReputationHolder {
refundToInbound, refundToInbound,
}); });
// Award reputation to post author // Apply reputation effects to post authors, not to the post directly
rewardsAccumulator.set(post.tokenId, appliedIncrement); for (const authorEdge of postVertex.getEdges(EdgeTypes.AUTHOR, true)) {
const { weight, data: { tokenId } } = authorEdge;
const authorIncrement = weight * appliedIncrement;
rewardsAccumulator.set(tokenId, authorIncrement);
}
// Increment the value of the post // Increment the value of the post
await this.setPostValue(post, newValue); await post.setValue(newValue);
return refundToInbound; return refundToInbound;
} }

View File

@ -180,7 +180,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 +235,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

@ -58,12 +58,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 +76,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

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

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

@ -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) || [];
@ -60,7 +63,7 @@ export class ReputationTokenContract extends ERC721 {
const sourceAvailable = this.availableValueOf(fromTokenId); const sourceAvailable = this.availableValueOf(fromTokenId);
if (sourceAvailable < amount - EPSILON) { if (sourceAvailable < amount - EPSILON) {
throw new Error('Token value transfer: source has insufficient available value. ' throw new Error('Token value transfer: source has insufficient available value. '
+ `Needs ${amount}; has ${sourceAvailable}.`); + `Needs ${amount}; has ${sourceAvailable}.`);
} }
this.incrementValue(fromTokenId, -amount); this.incrementValue(fromTokenId, -amount);
this.incrementValue(toTokenId, amount); this.incrementValue(toTokenId, amount);

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,5 +1,7 @@
export class Vertex { export class Vertex {
constructor(id, data) { constructor(graph, type, id, data) {
this.graph = graph;
this.type = type;
this.id = id; this.id = id;
this.data = data; this.data = data;
this.edges = { this.edges = {
@ -8,19 +10,20 @@ export class Vertex {
}; };
} }
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,
); );
} }
} }
export class Edge { export class Edge {
constructor(label, from, to, weight) { constructor(type, from, to, weight, data) {
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;
} }
} }
@ -28,7 +31,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 +42,11 @@ export class WDAG {
return this; return this;
} }
addVertex(id, data, label) { setVertexDisplayLabel(id, label) {
this.flowchart?.log(`${id}[${label}]`);
}
addVertex(type, id, data, label) {
// 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 +55,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);
this.vertices.set(id, vertex); this.vertices.set(id, vertex);
this.flowchart?.log(`${id}[${label ?? id}]`); this.setVertexDisplayLabel(id, label ?? id);
return this; return vertex;
}
setVertexLabel(id, label) {
this.flowchart?.log(`${id}[${label}]`);
} }
getVertex(id) { getVertex(id) {
@ -74,25 +77,25 @@ export class WDAG {
return btoa([from.id, to.id]).replaceAll(/[^A-Za-z0-9]+/g, ''); 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); 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 = WDAG.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 }) { getEdgeHtml({ from, to }) {
let html = '<table>'; let html = '<table>';
for (const { label, weight } of this.getEdges(null, from, to)) { for (const { type, weight } of this.getEdges(null, from, to)) {
html += `<tr><td>${label}</td><td>${weight}</td></tr>`; html += `<tr><td>${type}</td><td>${weight}</td></tr>`;
} }
html += '</table>'; html += '</table>';
return html; return html;
@ -103,14 +106,14 @@ export class WDAG {
return `${edgeKey}(${this.getEdgeHtml(edge)})`; return `${edgeKey}(${this.getEdgeHtml(edge)})`;
} }
setEdgeWeight(label, from, to, weight) { setEdgeWeight(type, from, to, weight, data) {
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(type, from, to, weight, data);
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 = WDAG.getEdgeKey(edge);
edges.set(edgeKey, edge); edges.set(edgeKey, edge);
@ -118,26 +121,26 @@ export class WDAG {
return edge; return edge;
} }
addEdge(label, from, to, weight) { addEdge(type, from, to, weight, data) {
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);
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}`); this.flowchart?.log(`${from.id} --- ${this.getEdgeFlowchartNode(edge)} --> ${to.id}`);
this.flowchart?.log(`class ${WDAG.getEdgeKey(edge)} edge`); this.flowchart?.log(`class ${WDAG.getEdgeKey(edge)} edge`);
return this; return edge;
} }
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 +149,10 @@ export class WDAG {
}); });
} }
countVertices() { countVertices(type) {
return this.vertices.size; if (!type) {
return this.vertices.size;
}
return Array.from(this.vertices.values()).filter((vertex) => vertex.type === type).length;
} }
} }

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

@ -24,6 +24,7 @@
<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>
</ol> </ol>
</ul> </ul>
<ul> <ul>

View File

@ -36,6 +36,7 @@
<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 defer class="mocha-init"> <script defer class="mocha-init">
mocha.setup({ mocha.setup({
ui: 'bdd', ui: 'bdd',

View File

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

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

@ -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,
@ -77,11 +87,9 @@ export class ForumTest {
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()); // await this.dao.addComputedValue('total reputation', () => this.dao.forum.getTotalValue());
this.dao.computeValues(); this.dao.computeDisplayValues();
} }
} }

View File

@ -0,0 +1,39 @@
import { mochaRun } from '../../../util.js';
import { ForumTest } from './forum.test-util.js';
describe('Forum', function tests() {
this.timeout(0);
const forumTest = new ForumTest();
before(async () => {
await forumTest.setup();
});
context('Post with multiple authors', async () => {
let forum;
let experts;
let posts;
before(async () => {
forum = forumTest.forum;
experts = forumTest.experts;
posts = forumTest.posts;
await forumTest.newExpert();
await forumTest.newExpert();
});
it('Post1', 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);
});
});
});
mochaRun();

View File

@ -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);