151 lines
5.4 KiB
JavaScript
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;
|
|
}
|
|
}
|