dao-governance-framework/forum-network/src/classes/forum.js

151 lines
5.4 KiB
JavaScript

import { Actor } from './actor.js';
import { Graph } from './graph.js';
import { Action } from './action.js';
import { CryptoUtil } from './crypto.js';
import params from '../params.js';
import { ReputationHolder } from './reputation-holder.js';
class Post extends Actor {
constructor(forum, authorPublicKey, postContent) {
const index = forum.posts.countVertices();
const name = `Post${index + 1}`;
super(name, forum.scene);
this.id = postContent.id ?? `post_${CryptoUtil.randomUUID()}`;
this.authorPublicKey = authorPublicKey;
this.value = 0;
this.citations = postContent.citations;
this.title = postContent.title;
const revaluationTotal = this.citations.reduce((total, { weight }) => total += Math.abs(weight), 0);
if (revaluationTotal > params.revaluationLimit) {
throw new Error('Post revaluation total exceeds revaluation limit '
+ `(${revaluationTotal} > ${params.revaluationLimit})`);
}
if (this.citations.some(({ weight }) => Math.abs(weight) > 1)) {
throw new Error('Each citation weight must be in the range [-1, 1]');
}
}
}
/**
* Purpose: Maintain a directed, acyclic, weighted graph of posts referencing other posts
*/
export class Forum extends ReputationHolder {
constructor(name, scene) {
super(`forum_${CryptoUtil.randomUUID()}`, name, scene);
this.id = this.reputationPublicKey;
this.posts = new Graph(scene);
this.actions = {
addPost: new Action('add post', scene),
propagateValue: new Action('propagate value', this.scene),
transfer: new Action('transfer', this.scene),
};
}
async addPost(authorId, postContent) {
const post = new Post(this, authorId, postContent);
await this.actions.addPost.log(this, post);
this.posts.addVertex(post.id, post, post.title);
if (this.scene.flowchart) {
this.scene.flowchart.log(`${post.id} -- value --> ${post.id}_value[0]`);
}
for (const { postId: citedPostId, weight } of post.citations) {
this.posts.addEdge('citation', post.id, citedPostId, { weight });
if (this.scene.flowchart) {
this.scene.flowchart.log(`${post.id} -- ${weight} --> ${citedPostId}`);
}
}
return post.id;
}
getPost(postId) {
return this.posts.getVertexData(postId);
}
getPosts() {
return this.posts.getVerticesData();
}
async setPostValue(post, value) {
post.value = value;
await post.setValue('value', value);
if (this.scene.flowchart) {
this.scene.flowchart.log(`${post.id}_value[${value}]`);
}
}
getTotalValue() {
return this.getPosts().reduce((total, { value }) => total += value, 0);
}
async onValidate({
bench, pool, postId, tokenId,
}) {
this.activate();
const initialValue = bench.reputation.valueOf(tokenId);
if (this.scene.flowchart) {
this.scene.flowchart.log(`${postId}_initial_value[${initialValue}] -- initial value --> ${postId}`);
}
const post = this.getPost(postId);
post.setStatus('Validated');
// 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;
// Compute rewards
const rewardsAccumulator = new Map();
await this.propagateValue(rewardsAccumulator, pool, post, initialValue);
// Apply computed rewards to update values of tokens
for (const [id, value] of rewardsAccumulator) {
bench.reputation.transferValueFrom(post.tokenId, id, value);
}
// Transfer ownership of the minted/staked token, from the forum to the post author
bench.reputation.transferFrom(this.id, post.authorPublicKey, post.tokenId);
const toActor = this.scene.findActor((actor) => actor.reputationPublicKey === post.authorPublicKey);
const value = bench.reputation.valueOf(post.tokenId);
this.actions.transfer.log(this, toActor, `(value: ${value})`);
this.deactivate();
}
async propagateValue(rewardsAccumulator, fromActor, post, increment, depth = 0) {
if (params.referenceChainLimit >= 0 && depth > params.referenceChainLimit) {
return [];
}
this.actions.propagateValue.log(fromActor, post, `(${increment})`);
// Recursively distribute reputation to citations, according to weights
let totalOutboundAmount = 0;
let refundFromOutbound = 0;
for (const { postId: citedPostId, weight } of post.citations) {
const citedPost = this.getPost(citedPostId);
const outboundAmount = weight * increment;
totalOutboundAmount += outboundAmount;
refundFromOutbound += await this.propagateValue(rewardsAccumulator, post, citedPost, outboundAmount, depth + 1);
}
// Apply leaching value
const incrementAfterLeaching = increment - (totalOutboundAmount - refundFromOutbound) * params.leachingValue;
// Prevent value from decreasing below zero
const rawNewValue = post.value + incrementAfterLeaching;
const newValue = Math.max(0, rawNewValue);
// We "refund" the amount that could not be applied.
// Note that this will always be a negative quantity, because this situation only arises when increment is negative.
const refundToInbound = rawNewValue - newValue;
const appliedIncrement = newValue - post.value;
// Award reputation to post author
rewardsAccumulator.set(post.tokenId, appliedIncrement);
// Increment the value of the post
await this.setPostValue(post, newValue);
return refundToInbound;
}
}