import { WDAG } from './wdag.js'; import { Action } from './action.js'; import params from '../params.js'; import { ReputationHolder } from './reputation-holder.js'; import { EPSILON } from '../util.js'; import { Post } from './post.js'; const CITATION = 'citation'; const BALANCE = 'balance'; /** * Purpose: * - Forum: Maintain a directed, acyclic, graph of positively and negatively weighted citations. * and the value accrued via each post and citation. */ export class Forum extends ReputationHolder { constructor(dao, name) { super(name); this.dao = dao; this.id = this.reputationPublicKey; this.posts = new WDAG(); this.actions = { addPost: new Action('add post'), propagateValue: new Action('propagate'), transfer: new Action('transfer'), }; } 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.getLabel()); for (const { postId: citedPostId, weight } of post.citations) { this.posts.addEdge(CITATION, post.id, citedPostId, weight); } 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); this.posts.setVertexLabel(post.id, post.getLabel()); } getTotalValue() { return this.getPosts().reduce((total, { value }) => total += value, 0); } async onValidate({ postId, tokenId, }) { this.activate(); const initialValue = this.dao.reputation.valueOf(tokenId); const postVertex = this.posts.getVertex(postId); const post = postVertex.data; post.setStatus('Validated'); post.initialValue = initialValue; this.posts.setVertexLabel(post.id, 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 rewardsAccumulator = new Map(); // Compute rewards await this.propagateValue( { to: postVertex, from: { data: this } }, { 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); } else { this.dao.reputation.transferValueFrom(post.tokenId, id, value); } } // Transfer ownership of the minted/staked token, from the posts to the post author this.dao.reputation.transferFrom(this.id, post.authorPublicKey, post.tokenId); const toActor = window?.scene?.findActor((actor) => actor.reputationPublicKey === post.authorPublicKey); const value = this.dao.reputation.valueOf(post.tokenId); this.actions.transfer.log(this, toActor, `(${value})`); this.deactivate(); } /** * @param {Edge} edge * @param {Object} opaqueData */ async propagateValue(edge, { rewardsAccumulator, increment, depth = 0, initialNegative = false, }) { const postVertex = edge.to; const post = postVertex?.data; this.actions.propagateValue.log(edge.from.data, post, `(${increment})`); if (!!params.referenceChainLimit && depth > params.referenceChainLimit) { this.actions.propagateValue.log( edge.from.data, post, `referenceChainLimit (${params.referenceChainLimit}) reached`, null, '-x', ); return increment; } console.log('propagateValue start', { from: edge.from.id, to: edge.to.id, depth, value: post.value, increment, initialNegative, }); const propagate = async (positive) => { let totalOutboundAmount = 0; const citationEdges = postVertex.getEdges(CITATION, true) .filter(({ weight }) => (positive ? weight > 0 : weight < 0)); for (const citationEdge of citationEdges) { const { weight } = citationEdge; let outboundAmount = weight * increment; const balanceToOutbound = this.posts.getEdgeWeight(BALANCE, citationEdge.from, citationEdge.to) ?? 0; // We need to ensure that we at most undo the prior effects of this post if (initialNegative) { outboundAmount = outboundAmount < 0 ? Math.max(outboundAmount, -balanceToOutbound) : Math.min(outboundAmount, -balanceToOutbound); } if (Math.abs(outboundAmount) > EPSILON) { const refundFromOutbound = await this.propagateValue(citationEdge, { rewardsAccumulator, increment: outboundAmount, depth: depth + 1, initialNegative: initialNegative || (depth === 0 && outboundAmount < 0), }); outboundAmount -= refundFromOutbound; this.posts.setEdgeWeight(BALANCE, citationEdge.from, citationEdge.to, balanceToOutbound + outboundAmount); totalOutboundAmount += outboundAmount; } } return totalOutboundAmount; }; // First, leach value via negative citations const totalLeachingAmount = await propagate(false); increment -= totalLeachingAmount * params.leachingValue; // Now propagate value via positive citations const totalDonationAmount = await propagate(true); increment -= totalDonationAmount * params.leachingValue; // Apply the remaining increment to the present post const rawNewValue = post.value + increment; const newValue = Math.max(0, rawNewValue); const appliedIncrement = newValue - post.value; const refundToInbound = increment - appliedIncrement; console.log('propagateValue end', { depth, increment, rawNewValue, newValue, appliedIncrement, refundToInbound, }); // Award reputation to post author rewardsAccumulator.set(post.tokenId, appliedIncrement); // Increment the value of the post await this.setPostValue(post, newValue); return refundToInbound; } }