This commit is contained in:
Ladd Hoffman 2023-08-02 14:54:31 -05:00
parent 629274476c
commit 435633a893
35 changed files with 1106 additions and 347 deletions

17
notes/reputation-types.md Normal file
View File

@ -0,0 +1,17 @@
Reputation is comprised of non-fungible tokens associated with specific forum graph post -> author edges.
Therefore in principle, all information about the context of a given rep token can be derived by inspecting the forum graph.
However, in practice, the computational costs and the difficulty of preserving complete records will increase over time.
It is for this reason that we compute the current value of a given rep token and store that value.
Although the value could be recomputed when needed, it would be (unpredictably) expensive and time-consuming to do so.
In its current, singular form, all instances of reputation within a given DAO have equal power, assuming equal numeric value.
However, the question arises: what would it take to support the ability to initiate a validation pool in which the power of a reputation token
depends on something more than just its numeric value?
This would be something specified when a validation pool is initiated.
Suppose we support the notion of distinct types of reputation within a given DAO.
Let's say we have reputation type A and B.
Let's say we have a validation pool that requires reputation type A to vote, and mints reputation type A.
That means governance is separated.

View File

@ -25,7 +25,7 @@ export class Expert extends ReputationHolder {
return 0;
}
const authorEdges = authorVertex.getEdges(EdgeTypes.AUTHOR, false);
const tokenValues = authorEdges.map(({ data: { tokenId } }) => this.dao.reputation.valueOf(tokenId));
const tokenValues = authorEdges.map(({ data: { tokenAddress } }) => this.dao.reputation.valueOf(tokenAddress));
return tokenValues.reduce((value, total) => total += value, 0);
}
@ -42,7 +42,7 @@ export class Expert extends ReputationHolder {
await this.actions.submitPost.log(this, post);
const postId = post.id;
const pool = await this.initiateValidationPool({ fee, postId }, params);
this.tokens.push(pool.tokenId);
this.tokens.push(pool.tokenAddress);
return { postId, pool };
}
@ -53,7 +53,7 @@ export class Expert extends ReputationHolder {
postId,
fee,
}, params);
this.tokens.push(pool.tokenId);
this.tokens.push(pool.tokenAddress);
return pool;
}
@ -68,7 +68,7 @@ export class Expert extends ReputationHolder {
`(${position ? 'for' : 'against'}, stake: ${amount})`,
);
return validationPool.stake(this.reputationPublicKey, {
position, amount, lockingTime, tokenId: this.tokens[0],
position, amount, lockingTime, tokenAddress: this.tokens[0],
});
}
@ -80,7 +80,7 @@ export class Expert extends ReputationHolder {
);
this.workerId = await this.dao.availability.register(this.reputationPublicKey, {
stakeAmount,
tokenId: this.tokens[0],
tokenAddress: this.tokens[0],
duration,
});
}

View File

@ -3,9 +3,9 @@ import { Actor } from '../display/actor.js';
import { CryptoUtil } from '../supporting/crypto.js';
class Worker {
constructor(reputationPublicKey, tokenId, stakeAmount, duration) {
constructor(reputationPublicKey, tokenAddress, stakeAmount, duration) {
this.reputationPublicKey = reputationPublicKey;
this.tokenId = tokenId;
this.tokenAddress = tokenAddress;
this.stakeAmount = stakeAmount;
this.duration = duration;
this.available = true;
@ -28,11 +28,11 @@ export class Availability extends Actor {
this.workers = new Map();
}
register(reputationPublicKey, { stakeAmount, tokenId, duration }) {
register(reputationPublicKey, { stakeAmount, tokenAddress, duration }) {
// TODO: Should be signed by token owner
this.dao.reputation.lock(tokenId, stakeAmount, duration);
this.dao.reputation.lock(tokenAddress, stakeAmount, duration);
const workerId = CryptoUtil.randomUUID();
this.workers.set(workerId, new Worker(reputationPublicKey, tokenId, stakeAmount, duration));
this.workers.set(workerId, new Worker(reputationPublicKey, tokenAddress, stakeAmount, duration));
return workerId;
}

View File

@ -87,7 +87,7 @@ export class Business extends Actor {
});
await pool.stake(reputationPublicKey, {
tokenId: request.worker.tokenId,
tokenAddress: request.worker.tokenAddress,
amount: request.worker.stakeAmount,
position: true,
});

View File

@ -5,6 +5,7 @@ import { Availability } from './availability.js';
import { Business } from './business.js';
import { Voter } from '../supporting/voter.js';
import { Actor } from '../display/actor.js';
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
/**
* Purpose:
@ -49,15 +50,13 @@ export class DAO extends Actor {
});
}
getActiveReputation() {
/**
* @param {number} param0.reputationTypeId
* @returns {number}
*/
getActiveAvailableReputation({ reputationTypeId = DEFAULT_REP_TOKEN_TYPE_ID }) {
return this.listActiveVoters()
.map(({ reputationPublicKey }) => this.reputation.valueOwnedBy(reputationPublicKey))
.reduce((acc, cur) => (acc += cur), 0);
}
getActiveAvailableReputation() {
return this.listActiveVoters()
.map(({ reputationPublicKey }) => this.reputation.availableValueOwnedBy(reputationPublicKey))
.map(({ reputationPublicKey }) => this.reputation.availableValueOwnedBy(reputationPublicKey, reputationTypeId))
.reduce((acc, cur) => (acc += cur), 0);
}

View File

@ -15,8 +15,8 @@ class Post extends Actor {
this.forum = forum;
this.id = postContent.id ?? name;
this.senderId = senderId;
this.value = 0;
this.initialValue = 0;
this.values = new Map();
this.initialValues = new Map();
this.authors = postContent.authors;
this.citations = postContent.citations;
this.title = postContent.title;
@ -83,16 +83,21 @@ export class Forum extends ReputationHolder {
// getContract(type) { }
async onValidate({
pool, postId, tokenId, referenceChainLimit, leachingValue,
pool, postId, tokenAddress, referenceChainLimit, leachingValue,
}) {
console.log('onValidate', { pool, postId, tokenId });
const initialValue = this.dao.reputation.valueOf(tokenId);
console.log('onValidate', { pool, postId, tokenAddress });
// What we have here now is an ERC-1155 rep token, which can contain multiple reputation types.
// ERC-1155 supports a batch transfer operation, so it makes sense to leverage that.
const initialValues = pool.reputationTypeIds
.map((tokenTypeId) => this.dao.reputation.valueOf(tokenAddress, tokenTypeId));
const postVertex = this.graph.getVertex(postId);
const post = postVertex.data;
post.setStatus('Validated');
post.initialValue = initialValue;
post.initialValues = initialValues;
const addAuthorToGraph = (publicKey, weight, authorTokenId) => {
const addAuthorToGraph = (publicKey, weight, authorTokenAddress) => {
// 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());
@ -105,7 +110,7 @@ export class Forum extends ReputationHolder {
postVertex,
authorVertex,
weight,
{ tokenId: authorTokenId },
{ tokenAddress: authorTokenAddress },
{ hide: author.options.hide },
);
};
@ -114,16 +119,18 @@ export class Forum extends ReputationHolder {
// 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);
addAuthorToGraph(post.senderId, 1, tokenAddress);
} 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);
const authorTokenAddress = (publicKey === post.senderId)
? tokenAddress
: this.dao.reputation.mintBatch(this.id, pool.reputationTypeIds, pool.reputationTypeIds.map(() => 0));
addAuthorToGraph(publicKey, weight, authorTokenAddress);
}
// 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);
addAuthorToGraph(post.senderId, 0, tokenAddress);
}
}
@ -134,7 +141,7 @@ export class Forum extends ReputationHolder {
{ to: postVertex, from: { data: pool } },
{
rewardsAccumulator,
increment: initialValue,
increments: initialValues,
referenceChainLimit,
leachingValue,
},
@ -142,16 +149,16 @@ export class Forum extends ReputationHolder {
// Apply computed rewards to update values of tokens
for (const [authorEdge, amount] of rewardsAccumulator) {
const { to: authorVertex, data: { tokenId: authorTokenId } } = authorEdge;
const { to: authorVertex, data: { tokenAddress: authorTokenAddress } } = 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 (authorTokenAddress !== tokenAddress) {
if (amount < 0) {
this.dao.reputation.transferValueFrom(authorTokenId, tokenId, -amount);
this.dao.reputation.transferValueFrom(authorTokenAddress, tokenAddress, -amount);
} else {
this.dao.reputation.transferValueFrom(tokenId, authorTokenId, amount);
this.dao.reputation.transferValueFrom(tokenAddress, authorTokenAddress, amount);
}
await author.computeDisplayValues((label, value) => authorVertex.setProperty(label, value));
authorVertex.displayVertex();
@ -167,8 +174,8 @@ export class Forum extends ReputationHolder {
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);
const { tokenAddress: authorTokenAddress } = authorEdge.data;
this.dao.reputation.transfer(this.id, author.reputationPublicKey, authorTokenAddress);
}
}
@ -178,7 +185,7 @@ export class Forum extends ReputationHolder {
*/
async propagateValue(edge, {
rewardsAccumulator,
increment,
increments,
depth = 0,
initialNegative = false,
referenceChainLimit,
@ -186,7 +193,8 @@ export class Forum extends ReputationHolder {
}) {
const postVertex = edge.to;
const post = postVertex.data;
this.actions.propagate.log(edge.from.data, post, `(${increment})`);
const incrementsStr = `(${increments.join(')(')})`;
this.actions.propagate.log(edge.from.data, post, incrementsStr);
if (!!referenceChainLimit && depth > referenceChainLimit) {
this.actions.propagate.log(
@ -196,7 +204,7 @@ export class Forum extends ReputationHolder {
null,
'-x',
);
return increment;
return increments;
}
console.log('propagateValue start', {
@ -204,95 +212,104 @@ export class Forum extends ReputationHolder {
to: edge.to.id,
depth,
value: post.value,
increment,
increments,
initialNegative,
});
const propagate = async (positive) => {
let totalOutboundAmount = 0;
const totalOutboundAmounts = increments.map(() => 0);
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.graph.getEdgeWeight(EdgeTypes.BALANCE, citationEdge.from, citationEdge.to)
const outboundAmounts = increments.map((increment) => weight * increment);
const refundsFromOutbound = increments.map(() => 0);
for (let idx = 0; idx < outboundAmounts.length; idx++) {
let outboundAmount = outboundAmounts[idx];
if (Math.abs(outboundAmount) > EPSILON) {
const balanceToOutbound = this.graph.getEdgeWeight(EdgeTypes.BALANCE, citationEdge.from, citationEdge.to)
?? 0;
let refundFromOutbound = 0;
let refundFromOutbound = 0;
// Special case: Incineration.
if (citationEdge.to.id === INCINERATOR_ADDRESS) {
// Special case: Incineration.
if (citationEdge.to.id === INCINERATOR_ADDRESS) {
// Only a positive amount may be incinerated! Otherwise the sink could be used as a source.
if (outboundAmount < 0) {
this.scene?.flowchart?.log(`style ${citationEdge.from.id} fill:#620000`);
this.actions.propagate.log(
citationEdge.from.data,
{ name: 'Incinerator' },
`(${increment})`,
undefined,
'-x',
);
throw new Error('Incinerator can only receive positive citations!');
}
// Reputation sent to the incinerator is burned! This means it is deducted from the sender,
// without increasing the value of any other token.
this.actions.propagate.log(citationEdge.from.data, { name: 'Incinerator' }, `(${increment})`);
} else {
if (outboundAmount < 0) {
this.scene?.flowchart?.log(`style ${citationEdge.from.id} fill:#620000`);
this.actions.propagate.log(
citationEdge.from.data,
{ name: 'Incinerator' },
incrementsStr,
undefined,
'-x',
);
throw new Error('Incinerator can only receive positive citations!');
}
// Reputation sent to the incinerator is burned! This means it is deducted from the sender,
// without increasing the value of any other token.
this.actions.propagate.log(citationEdge.from.data, { name: 'Incinerator' }, incrementsStr);
} else {
// 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 (initialNegative) {
outboundAmount = outboundAmount < 0
? Math.max(outboundAmount, -balanceToOutbound)
: Math.min(outboundAmount, -balanceToOutbound);
}
// Recursively propagate reputation effects
refundFromOutbound = await this.propagateValue(citationEdge, {
rewardsAccumulator,
increment: outboundAmount,
depth: depth + 1,
initialNegative: initialNegative || (depth === 0 && outboundAmount < 0),
referenceChainLimit,
leachingValue,
});
// Any excess (negative) amount that could not be propagated,
// i.e. because a cited post has been reduced to zero value,
// is retained by the citing post.
outboundAmount -= refundFromOutbound;
}
// Recursively propagate reputation effects
refundFromOutbound = await this.propagateValue(citationEdge, {
rewardsAccumulator,
increment: outboundAmount,
depth: depth + 1,
initialNegative: initialNegative || (depth === 0 && outboundAmount < 0),
referenceChainLimit,
leachingValue,
});
// Any excess (negative) amount that could not be propagated,
// i.e. because a cited post has been reduced to zero value,
// is retained by the citing post.
outboundAmount -= refundFromOutbound;
// 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.graph.setEdgeWeight(
EdgeTypes.BALANCE,
citationEdge.from,
citationEdge.to,
balanceToOutbound + outboundAmount,
);
refundsFromOutbound[idx] = refundFromOutbound;
totalOutboundAmounts[idx] += outboundAmount;
}
// 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.graph.setEdgeWeight(
EdgeTypes.BALANCE,
citationEdge.from,
citationEdge.to,
balanceToOutbound + outboundAmount,
);
totalOutboundAmount += outboundAmount;
const refundStr = refundsFromOutbound.map((refund) => displayNumber(refund)).join('/');
this.actions.confirm.log(
citationEdge.to.data,
citationEdge.from.data,
`(refund: ${displayNumber(refundFromOutbound)}, leach: ${outboundAmount * leachingValue})`,
`(refund: ${refundStr}, leach: ${outboundAmount * leachingValue})`,
undefined,
'-->>',
);
}
}
return totalOutboundAmount;
return totalOutboundAmounts;
};
// First, leach value via negative citations
const totalLeachingAmount = await propagate(false);
increment -= totalLeachingAmount * leachingValue;
const totalLeachingAmounts = await propagate(false);
for (let idx = 0; idx < totalLeachingAmounts.length; idx++) {
increments[idx] -= totalLeachingAmounts[idx] * leachingValue;
}
// Now propagate value via positive citations
const totalDonationAmount = await propagate(true);
increment -= totalDonationAmount * leachingValue;
for (let idx = 0; idx < totalDonationAmounts.length; idx++) {
increments[idx] -= totalDonationAmounts[idx] * leachingValue;
}
// Apply the remaining increment to the present post
const rawNewValue = post.value + increment;
const rawNewValues = post.value + increment;
const newValue = Math.max(0, rawNewValue);
const appliedIncrement = newValue - post.value;
const refundToInbound = increment - appliedIncrement;

View File

@ -2,6 +2,7 @@ import { ReputationHolder } from '../reputation/reputation-holder.js';
import { Stake } from '../supporting/stake.js';
import { Action } from '../display/action.js';
import { displayNumber } from '../../util/helpers.js';
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
const params = {
/* Validation Pool parameters */
@ -51,6 +52,7 @@ export class ValidationPool extends ReputationHolder {
duration,
tokenLossRatio,
contentiousDebate = false,
reputationTypes,
},
name,
scene,
@ -69,6 +71,16 @@ export class ValidationPool extends ReputationHolder {
this.actions.initiate.log(fromActor, this, `(fee: ${fee})`);
this.activate();
// Supporting a simplified use case, if the reputation type is not specified let's use a default
this.reputationTypes = reputationTypes ?? [{ reputationTypeId: DEFAULT_REP_TOKEN_TYPE_ID, weight: 1 }];
// Normalize so reputation weights sum to 1
{
const weightTotal = this.reputationTypes.reduce((total, { weight }) => total += weight, 0);
for (const reputationType of this.reputationTypes) {
reputationType.weight /= weightTotal;
}
}
// If contentiousDebate = true, we will follow the progression defined by getTokenLossRatio()
if (
!contentiousDebate
@ -130,22 +142,34 @@ export class ValidationPool extends ReputationHolder {
this.duration = duration;
this.tokenLossRatio = tokenLossRatio;
this.contentiousDebate = contentiousDebate;
this.mintedValue = fee * params.mintingRatio();
this.tokenId = this.dao.reputation.mint(this.id, this.mintedValue);
// Tokens minted "for" the post go toward stake of author voting for their own post.
// Also, author can provide additional stakes, e.g. availability stakes for work evidence post.
this.stake(this.id, {
position: true,
amount: this.mintedValue * params.stakeForAuthor,
tokenId: this.tokenId,
});
this.stake(this.id, {
position: false,
amount: this.mintedValue * (1 - params.stakeForAuthor),
tokenId: this.tokenId,
});
this.actions.mint.log(this, this, `(${this.mintedValue})`);
const mintTotal = fee * params.mintingRatio();
const reputationTypeIds = this.reputationTypes
.map(({ reputationTypeId }) => reputationTypeId);
const mintValues = this.reputationTypes
.map(({ weight }) => mintTotal * weight);
console.log('validation pool constructor', { reputationTypeIds, mintValues });
this.tokenAddress = this.dao.reputation.mintBatch(this.id, reputationTypeIds, mintValues);
this.reputationTypeIds = reputationTypeIds;
// Minted tokens are staked for/against the post at configured ratio
// Each type of reputation is staked in the proportions specified by the `reputationTypes` parameter
for (const { reputationTypeId, weight } of this.reputationTypes) {
this.stake(this.id, {
position: true,
amount: mintTotal * params.stakeForAuthor * weight,
tokenAddress: this.tokenAddress,
reputationTypeId,
});
this.stake(this.id, {
position: false,
amount: this.mintedValue * (1 - params.stakeForAuthor) * weight,
tokenAddress: this.tokenAddress,
reputationTypeId,
});
}
this.actions.mint.log(this, this, `(${mintTotal})`);
// Keep a record of voters and their votes
this.dao.addVoteRecord(reputationPublicKey, this);
@ -174,39 +198,38 @@ export class ValidationPool extends ReputationHolder {
}
/**
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
* @param {boolean} options.outcome: null --> all entries. Otherwise filters to position === outcome.
* @param {boolean} options.tokenTypeId: null --> all entries. Otherwise filters to the given token type.
* @param {boolean} options.excludeSystem: Whether to exclude votes cast during pool initialization
* @returns stake[]
*/
getStakes(outcome, { excludeSystem }) {
getStakes({ outcome, tokenTypeId, excludeSystem }) {
return Array.from(this.stakes.values())
.filter(({ tokenId }) => !excludeSystem || tokenId !== this.tokenId)
.filter((stake) => tokenTypeId === null || stake.tokenTypeId === tokenTypeId)
.filter(({ tokenAddress }) => !excludeSystem || tokenAddress !== this.tokenAddress)
.filter(({ position }) => outcome === null || position === outcome);
}
/**
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
* @param {boolean} options.outcome: null --> all entries. Otherwise filters to position === outcome.
* @returns number
*/
getTotalStakedOnPost(outcome) {
return this.getStakes(outcome, { excludeSystem: false })
.map((stake) => stake.getStakeValue({ lockingTimeExponent: params.lockingTimeExponent }))
.reduce((acc, cur) => (acc += cur), 0);
getStakedAmount({ outcome, tokenTypeId }) {
return this.getStakes({ outcome, tokenTypeId, excludeSystem: false })
.map((stake) => stake.getAmount({ lockingTimeExponent: params.lockingTimeExponent }))
.reduce((total, amount) => (total += amount), 0);
}
/**
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
* @returns number
* Stake reputation in favor of a given outcome for this validation pool.
*
* @param {*} reputationPublicKey
* @param {object} opts
*/
getTotalValueOfStakesForOutcome(outcome) {
return this.getStakes(outcome, { excludeSystem: false })
.reduce((total, { amount }) => (total += amount), 0);
}
// TODO: This can be handled as a hook on receipt of reputation token transfer
async stake(reputationPublicKey, {
tokenId, position, amount, lockingTime = 0,
tokenAddress, tokenTypeId = DEFAULT_REP_TOKEN_TYPE_ID, position, amount, lockingTime = 0,
}) {
// TODO: This can be handled as a hook on receipt of reputation token transfer
if (this.state === ValidationPoolStates.CLOSED) {
throw new Error(`Validation pool ${this.id} is closed.`);
}
@ -217,17 +240,17 @@ export class ValidationPool extends ReputationHolder {
);
}
if (reputationPublicKey !== this.dao.reputation.ownerOf(tokenId)) {
if (reputationPublicKey !== this.dao.reputation.ownerOf(tokenAddress)) {
throw new Error('Reputation may only be staked by its owner!');
}
const stake = new Stake({
tokenId, position, amount, lockingTime,
tokenAddress, tokenTypeId, position, amount, lockingTime,
});
this.stakes.add(stake);
// Transfer staked amount from the sender to the validation pool
this.dao.reputation.transferValueFrom(tokenId, this.tokenId, amount);
this.dao.reputation.transferValueFrom(tokenAddress, this.tokenAddress, tokenTypeId, amount);
// Keep a record of voters and their votes
if (reputationPublicKey !== this.id) {
@ -243,8 +266,10 @@ export class ValidationPool extends ReputationHolder {
// Before evaluating the winning conditions,
// we need to make sure any staked tokens are locked for the
// specified amounts of time.
for (const { tokenId, amount, lockingTime } of this.stakes.values()) {
this.dao.reputation.lock(tokenId, amount, lockingTime);
for (const {
tokenAddress, tokenTypeId, amount, lockingTime,
} of this.stakes.values()) {
this.dao.reputation.lock(tokenAddress, tokenTypeId, amount, lockingTime);
// TODO: If there is an exception here, the voter may have voted incorrectly. Consider penalties.
}
}
@ -261,23 +286,33 @@ export class ValidationPool extends ReputationHolder {
this.state = ValidationPoolStates.CLOSED;
this.setStatus('Closed');
const upvoteValue = this.getTotalValueOfStakesForOutcome(true);
const downvoteValue = this.getTotalValueOfStakesForOutcome(false);
const activeAvailableReputation = this.dao.getActiveAvailableReputation();
const votePasses = upvoteValue >= params.winningRatio * downvoteValue;
// Votes should be scaled by weights of this.reputationTypes
const upvoteValue = this.reputationTypes.reduce((value, { reputationTypeId, weight }) => {
value += this.getStakedAmount({ outcome: true, reputationTypeId }) * weight;
return value;
}, 0);
const downvoteValue = this.reputationTypes.reduce((value, { reputationTypeId, weight }) => {
value += this.getStakedAmount({ outcome: false, reputationTypeId }) * weight;
return value;
}, 0);
const activeAvailableReputation = this.reputationTypes.reduce((value, { reputationTypeId, weight }) => {
value += this.dao.getActiveAvailableReputation({ reputationTypeId }) * weight;
return value;
}, 0);
const outcome = upvoteValue >= params.winningRatio * downvoteValue;
const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation;
const result = {
votePasses,
outcome,
upvoteValue,
downvoteValue,
};
if (quorumMet) {
this.setStatus(`Resolved - ${votePasses ? 'Won' : 'Lost'}`);
this.scene?.sequence.log(`note over ${this.name} : ${votePasses ? 'Win' : 'Lose'}`);
this.setStatus(`Resolved - ${outcome ? 'Won' : 'Lost'}`);
this.scene?.sequence.log(`note over ${this.name} : ${outcome ? 'Win' : 'Lose'}`);
this.applyTokenLocking();
await this.distributeReputation({ votePasses });
await this.distributeReputation({ outcome });
// TODO: distribute fees
} else {
this.setStatus('Resolved - Quorum not met');
@ -301,47 +336,57 @@ export class ValidationPool extends ReputationHolder {
return result;
}
async distributeReputation({ votePasses }) {
// For now we assume a tightly binding pool, where all staked reputation is lost
// TODO: Take tokenLossRatio into account
// TODO: revoke staked reputation from losing voters
async distributeReputation({ outcome }) {
// In a binding validation pool, losing voter stakes are transferred to winning voters.
// TODO: Regression tests for different tokenLossRatio values
const tokenLossRatio = this.getTokenLossRatio();
for (const { reputationTypeId, weight } of this.reputationTypes) {
const tokensForWinners = this.getStakedAmount({ outcome: !outcome, reputationTypeId }) * weight * tokenLossRatio;
const winningEntries = this.getStakes({ outcome, reputationTypeId, excludeSystem: true });
const totalValueOfStakesForWin = this.getStakedAmount({ outcome, reputationTypeId });
// In a tightly binding validation pool, losing voter stakes are transferred to winning voters.
const tokensForWinners = this.getTotalStakedOnPost(!votePasses);
const winningEntries = this.getStakes(votePasses, { excludeSystem: true });
const totalValueOfStakesForWin = this.getTotalValueOfStakesForOutcome(votePasses);
// Compute rewards for the winning voters, in proportion to the value of their stakes.
for (const stake of winningEntries) {
const { tokenId, amount } = stake;
const value = stake.getStakeValue({ lockingTimeExponent: params.lockingTimeExponent });
const reward = tokensForWinners * (value / totalValueOfStakesForWin);
// Also return each winning voter their staked amount
const reputationPublicKey = this.dao.reputation.ownerOf(tokenId);
console.log(`reward for winning stake by ${reputationPublicKey}: ${reward}`);
this.dao.reputation.transferValueFrom(this.tokenId, tokenId, reward + amount);
const toActor = this.scene?.findActor((actor) => actor.reputationPublicKey === reputationPublicKey);
this.actions.reward.log(this, toActor, `(${displayNumber(reward)})`);
// Compute rewards for the winning voters, in proportion to the value of their stakes.
for (const stake of winningEntries) {
const { tokenAddress, amount } = stake;
const value = stake.getAmount({ lockingTimeExponent: params.lockingTimeExponent });
const reward = tokensForWinners * (value / totalValueOfStakesForWin);
// Also return each winning voter their staked amount
const reputationPublicKey = this.dao.reputation.ownerOf(tokenAddress);
console.log(`reward of type ${reputationTypeId} for winning stake by ${reputationPublicKey}: ${reward}`);
this.dao.reputation.transferValueFrom(this.tokenAddress, tokenAddress, reputationTypeId, reward + amount);
const toActor = this.scene?.findActor((actor) => actor.reputationPublicKey === reputationPublicKey);
this.actions.reward.log(this, toActor, `(${displayNumber(reward)} type ${reputationTypeId})`);
}
}
if (votePasses) {
if (outcome === true) {
// Distribute awards to author via the forum
// const tokensForAuthor = this.mintedValue * params.stakeForAuthor + rewards.get(this.tokenId);
console.log(`sending reward for author stake to forum: ${this.dao.reputation.valueOf(this.tokenId)}`);
const tokens = this.reputationTypes.reduce((values, { reputationTypeId }) => {
const value = this.dao.reputation.valueOf(this.tokenAddress, reputationTypeId) ?? 0;
values[reputationTypeId] = value;
return values;
}, {});
console.log('sending reward for author stake to forum', tokens);
// Transfer ownership of the minted token, from the pool to the forum
this.dao.reputation.transfer(this.id, this.dao.forum.id, this.tokenId);
// const value = this.dao.reputation.valueOf(this.tokenId);
this.dao.reputation.transfer(this.id, this.dao.forum.id, this.tokenAddress);
// const value = this.dao.reputation.valueOf(this.tokenAddress);
// this.actions.transfer.log(this, this.dao.forum, `(${value})`);
// Recurse through forum to determine reputation effects
await this.dao.forum.onValidate({
const result = {
pool: this,
postId: this.postId,
tokenId: this.tokenId,
tokenAddress: this.tokenAddress,
referenceChainLimit: params.referenceChainLimit,
leachingValue: params.leachingValue,
});
};
// Recurse through forum to determine reputation effects
await this.dao.forum.onValidate({ ...result });
if (this.onValidate) {
await this.onValidate({ ...result });
}
}
console.log('pool complete');

View File

@ -89,6 +89,9 @@ export class Actor {
async computeDisplayValues(cb) {
for (const [label, fn] of this.valueFunctions.entries()) {
const value = fn();
console.log('computeDisplay', {
label, value, fn, cb,
});
await this.setDisplayValue(label, value);
if (cb) {
cb(label, value);

View File

@ -1,5 +1,6 @@
import { DisplayValue } from './display-value.js';
import { randomID } from '../../util/helpers.js';
import { Rectangle } from './geometry.js';
export class Box {
constructor(name, parentEl, options = {}) {
@ -20,6 +21,7 @@ export class Box {
parentEl.appendChild(this.el);
}
}
this.boxes = [];
}
flex({ center = false } = {}) {
@ -35,11 +37,6 @@ export class Box {
return this;
}
hidden() {
this.addClass('hidden');
return this;
}
addClass(className) {
this.el.classList.add(className);
return this;
@ -47,6 +44,7 @@ export class Box {
addBox(name) {
const box = new Box(name, this.el);
this.boxes.push(box);
return box;
}
@ -63,4 +61,16 @@ export class Box {
getId() {
return this.el.id;
}
getGeometry() {
const {
x, y, width, height,
} = this.el.getBoundingClientRect();
return new Rectangle([x, y], [width, height]);
}
move(vector) {
this.el.style.left = `${parseInt(this.el.style.left, 10) + vector[0]}px`;
this.el.style.top = `${parseInt(this.el.style.top, 10) + vector[1]}px`;
}
}

View File

@ -37,7 +37,6 @@ export class Document extends Box {
}
get lastElement() {
if (!this.elements.length) return null;
return this.elements[this.elements.length - 1];
}
}

View File

@ -0,0 +1,111 @@
import {
DEFAULT_TARGET_RADIUS, DISTANCE_FACTOR, MINIMUM_FORCE, OVERLAP_FORCE, VISCOSITY_FACTOR,
} from '../../util/constants.js';
import { Box } from './box.js';
import { Rectangle, Vector } from './geometry.js';
// Render children with absolute css positioning.
// Let there be a force between elements such that the force between
// any two elements is along the line between their centers,
// so that the elements repel when too close but attract when too far.
// The equilibrium distance can be tuned, e.g. can be scaled by an input.
// NOTE: (with optional overlay preferring a grid or some other shape?),
// NOTE: Could also allow user input dragging elements.
// What might be neat here is to implement a force-based resistance effect;
// basically, the mouse pointer drags the element with a spring rather than directly.
// If the shape of the graph resists the transformation,
// the distance between the element and the cursor should increase.
// On an interval, compute forces among the elements.
// Simulate the effects of these forces
// NOTE: Impart random nudges, and resolve their effects to a user-visible resolution
// before rendering.
// NOTE: When mouse is in our box, we could hijack the scroll actions to zoom in/out.
export class ForceDirectedGraph extends Box {
constructor(name, parentEl, options = {}) {
super(name, parentEl, options);
this.addClass('fixed');
}
addBox(name) {
const box = super.addBox(name);
box.addClass('absolute');
box.el.style.left = '0px';
box.el.style.top = '0px';
box.velocity = Vector.from([0, 0]);
return box;
}
static pairwiseForce(boxA, boxB, targetRadius) {
const rectA = boxA instanceof Rectangle ? boxA : boxA.getGeometry();
const centerA = rectA.center;
const rectB = boxB instanceof Rectangle ? boxB : boxB.getGeometry();
const centerB = rectB.center;
const r = centerB.subtract(centerA);
// Apply a stronger force when overlap occurs
if (rectA.doesOverlap(rectB)) {
// if their centers actually coincide we can just randomize the direction.
if (r.magnitudeSquared === 0) {
return Vector.randomUnitVector(rectA.dim).scale(OVERLAP_FORCE);
}
return r.normalize().scale(OVERLAP_FORCE);
}
// repel if closer than targetRadius
// attract if farther than targetRadius
const force = -DISTANCE_FACTOR * (r.magnitude - targetRadius);
return r.normalize().scale(force);
}
computeEulerFrame(tDelta) {
// Compute all net forces
const netForces = Array.from(Array(this.boxes.length), () => Vector.from([0, 0]));
for (const boxA of this.boxes) {
const idxA = this.boxes.indexOf(boxA);
for (const boxB of this.boxes.slice(idxA + 1)) {
const idxB = this.boxes.indexOf(boxB);
const force = ForceDirectedGraph.pairwiseForce(boxA, boxB, DEFAULT_TARGET_RADIUS);
// Ignore forces below a certain threshold
if (force.magnitude >= MINIMUM_FORCE) {
netForces[idxA] = netForces[idxA].subtract(force);
netForces[idxB] = netForces[idxB].add(force);
}
}
}
// Compute motions
for (const box of this.boxes) {
const idx = this.boxes.indexOf(box);
box.velocity = box.velocity.add(netForces[idx].scale(tDelta));
// Apply some drag
box.velocity = box.velocity.scale(1 - VISCOSITY_FACTOR);
}
for (const box of this.boxes) {
box.move(box.velocity);
}
// TODO: translate everything to keep coordinates positive
const translate = Vector.zeros(2);
for (const box of this.boxes) {
const rect = box.getGeometry();
console.log({ box, rect });
for (const vertex of rect.vertices) {
for (let dim = 0; dim < vertex.dim; dim++) {
translate[dim] = Math.max(translate[dim], -vertex[dim]);
console.log(`vertex[${dim}] = ${vertex[dim]}, translate[${dim}] = ${translate[dim]}`);
}
}
}
for (const box of this.boxes) {
box.move(translate);
}
}
}

View File

@ -19,6 +19,20 @@ export class FormElement extends Box {
}
}
export class Select extends FormElement {
constructor(name, form, opts) {
super(name, form, opts);
const { options } = opts;
this.selectEl = document.createElement('select');
for (const { value, label } of options) {
const optionEl = document.createElement('option');
optionEl.setAttribute('value', value);
optionEl.innerHTML = label || value;
}
this.el.appendChild(this.selectEl);
}
}
export class Button extends FormElement {
constructor(name, form, opts) {
super(name, form, { ...opts, cbEventTypes: ['click'] });
@ -43,15 +57,18 @@ export class TextField extends FormElement {
constructor(name, form, opts) {
super(name, form, opts);
this.flex({ center: true });
this.label = document.createElement('label');
this.labelDiv = document.createElement('div');
this.label.appendChild(this.labelDiv);
this.labelDiv.innerHTML = opts.label || name;
this.input = document.createElement('input');
this.input.disabled = !!opts.disabled;
this.input.defaultValue = opts.defaultValue || '';
this.label.appendChild(this.input);
this.el.appendChild(this.label);
// Place label inside a div, for improved styling
const labelDiv = document.createElement('div');
labelDiv.innerHTML = opts.label || name;
const label = document.createElement('label');
label.appendChild(labelDiv);
const input = document.createElement('input');
input.disabled = !!opts.disabled;
input.defaultValue = opts.defaultValue || '';
label.appendChild(input);
this.el.appendChild(label);
this.input = input;
}
get value() {
@ -61,6 +78,30 @@ export class TextField extends FormElement {
export class TextArea extends FormElement { }
export class SubForm extends FormElement {
// Form has:
// this.document = document;
// this.items = [];
// this.id = opts.id ?? `form_${randomID()}`;
// FormElement has
constructor(name, form, opts) {
if (!name) {
name = `subform${randomID()}`;
}
const parentEl = opts.subFormArray ? opts.subFormArray.el : form.el;
const subForm = form.document.form({ name, parentEl, tagName: 'div' }).lastElement;
super(name, form, { ...opts, parentEl });
this.subForm = subForm;
if (opts.subFormArray) {
opts.subFormArray.subForms.push(this);
}
}
get value() {
return this.subForm.value;
}
}
export class SubFormArray extends FormElement {
constructor(name, form, opts) {
super(name, form, opts);
@ -76,36 +117,30 @@ export class SubFormArray extends FormElement {
this.subForms.splice(idx, 1);
subForm.el.remove();
}
}
export class SubForm extends FormElement {
constructor(name, form, opts) {
const parentEl = opts.subFormArray ? opts.subFormArray.el : form.el;
const subForm = form.document.form({ name, parentEl, tagName: 'div' }).lastElement;
super(name, form, { ...opts, parentEl });
this.subForm = subForm;
if (opts.subFormArray) {
opts.subFormArray.subForms.push(this);
}
subForm(opts = {}) {
const subForm = new SubForm(opts.name, this.form, { ...opts, subFormArray: this });
this.subForms.push(subForm);
return this;
}
get value() {
return this.subForm.value;
get lastSubForm() {
return this.subForms[this.subForms.length - 1];
}
}
export class FileInput extends FormElement {
constructor(name, form, opts) {
super(name, form, opts);
this.input = document.createElement('input');
this.input.type = 'file';
this.input.accept = 'application/json';
this.input.classList.add('visually-hidden');
this.label = document.createElement('label');
this.button = form.button({ name, cb: () => this.input.click() }).lastItem;
this.label.appendChild(this.button.el);
this.label.appendChild(this.input);
this.el.appendChild(this.label);
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/json';
input.classList.add('visually-hidden');
const label = document.createElement('label');
const button = form.button({ name, cb: () => input.click() }).lastItem;
label.appendChild(button.el);
label.appendChild(input);
this.el.appendChild(label);
}
}
@ -119,6 +154,11 @@ export class Form extends Box {
this.el.onsubmit = () => false;
}
select(opts) {
this.items.push(new Select(opts.name, this, opts));
return this;
}
button(opts) {
this.items.push(new Button(opts.name, this, opts));
return this;
@ -154,6 +194,10 @@ export class Form extends Box {
return this;
}
remark(text, opts) {
this.document.remark(text, { ...opts, parentEl: this.el });
}
get lastItem() {
return this.items[this.items.length - 1];
}

View File

@ -0,0 +1,100 @@
export class Vector extends Array {
get dim() {
return this.length ?? 0;
}
add(vector) {
if (vector.dim !== this.dim) {
throw new Error('Can only add vectors of the same dimensions');
}
return Vector.from(this.map((q, idx) => q + vector[idx]));
}
subtract(vector) {
if (vector.dim !== this.dim) {
throw new Error('Can only subtract vectors of the same dimensions');
}
return Vector.from(this.map((q, idx) => q - vector[idx]));
}
static unitVector(dim, totalDim) {
return Vector.from(Array(totalDim).map((_, idx) => (idx === dim ? 1 : 0)));
}
get magnitudeSquared() {
return this.reduce((total, q) => total += q ** 2, 0);
}
get magnitude() {
return Math.sqrt(this.magnitudeSquared);
}
scale(factor) {
return Vector.from(this.map((q) => q * factor));
}
normalize() {
return this.scale(1 / this.magnitude);
}
static randomUnitVector(totalDim) {
return Vector.from(Array(totalDim), () => Math.random()).normalize();
}
static zeros(totalDim) {
return Vector.from(Array(totalDim), () => 0);
}
}
export class Polygon {
constructor() {
this.vertices = [];
this.dim = 0;
}
addVertex(point) {
point = point instanceof Vector ? point : Vector.from(point);
if (!this.dim) {
this.dim = point.dim;
} else if (this.dim !== point.dim) {
throw new Error('All vertices of a polygon must have the same dimensionality');
}
this.vertices.push(point);
}
}
export class Rectangle extends Polygon {
constructor(startPoint, dimensions) {
super();
this.startPoint = Vector.from(startPoint);
this.dimensions = Vector.from(dimensions);
// Next point is obtained by moving the specified length along each dimension
// one at a time, then reversing these movements in the same order.
let point = this.startPoint;
for (let dim = 0; dim < dimensions.length; dim++) {
this.addVertex(point);
const increment = Vector.unitVector(dim, dimensions.length);
point = point.add(increment);
}
for (let dim = 0; dim < dimensions.length; dim++) {
this.addVertex(point);
const increment = Vector.unitVector(dim, dimensions.length);
point = point.subtract(increment);
}
}
get center() {
return Vector.from(this.dimensions.map((Q, idx) => this.startPoint[idx] + Q / 2));
}
doesOverlap(rect) {
return this.dimensions.every((_, idx) => {
const thisMin = this.startPoint[idx];
const thisMax = this.startPoint[idx] + this.dimensions[idx];
const thatMin = rect.startPoint[idx];
const thatMax = rect.startPoint[idx] + rect.dimensions[idx];
return (thisMin <= thatMin && thisMax >= thatMin)
|| (thisMin >= thatMin && thisMin <= thatMax);
});
}
}

View File

@ -12,7 +12,7 @@ export class Scene {
constructor(name, rootBox) {
this.name = name;
this.box = rootBox.addBox(name);
this.titleBox = this.box.addBox('Title').setInnerHTML(name);
// this.titleBox = this.box.addBox('Title').setInnerHTML(name);
this.box.addBox('Spacer').setInnerHTML('&nbsp;');
this.topSection = this.box.addBox('Top section').flex();
this.displayValuesBox = this.topSection.addBox('Values');

View File

@ -1,56 +1,64 @@
import { ERC721 } from '../supporting/erc721.js';
import { ERC1155 } from '../supporting/erc1155.js';
import { randomID } from '../../util/helpers.js';
import { EPSILON } from '../../util/constants.js';
class Lock {
constructor(tokenId, amount, duration) {
constructor(tokenAddress, tokenTypeId, amount, duration) {
this.dateCreated = new Date();
this.tokenId = tokenId;
this.tokenAddress = tokenAddress;
this.amount = amount;
this.duration = duration;
this.tokenTypeId = tokenTypeId;
}
}
export class ReputationTokenContract extends ERC721 {
export class ReputationTokenContract extends ERC1155 {
constructor() {
super('Reputation', 'REP');
this.histories = new Map(); // token id --> {increment, context (i.e. validation pool id)}
this.values = new Map(); // token id --> current value
this.locks = new Set(); // {tokenId, amount, start, duration}
this.histories = new Map(); // token address --> {tokenTypeId, increment, context (i.e. validation pool id)}
this.values = new Map(); // token address --> token type id --> current value
this.locks = new Set(); // {tokenAddress, tokenTypeId, amount, start, duration}
}
/**
*
* @param to
* @param value
* @param context
* @param to Recipient address
* @param values Object with reputation type id as key, and amount of reputation as value
* @returns {string}
*/
mint(to, value, context = {}) {
const tokenId = `token_${randomID()}`;
super.mint(to, tokenId);
this.values.set(tokenId, value);
this.histories.set(tokenId, [{ increment: value, context }]);
return tokenId;
mintBatch(to, tokenTypeIds, values) {
const tokenAddress = `token_${randomID()}`;
super.mintBatch(to, tokenAddress, tokenTypeIds, tokenTypeIds.map(() => 1));
const tokenMap = new Map();
for (let idx = 0; idx < tokenTypeIds.length; idx++) {
const tokenTypeId = tokenTypeIds[idx];
const value = values[idx];
tokenMap.set(tokenTypeId, value);
}
this.values.set(tokenAddress, tokenMap);
this.histories.set(tokenAddress, [{ operation: 'mintBatch', args: { to, tokenTypeIds, values } }]);
return tokenAddress;
}
incrementValue(tokenId, increment, context) {
const value = this.values.get(tokenId);
if (value === undefined) {
throw new Error(`Token not found: ${tokenId}`);
incrementValue(tokenAddress, tokenTypeId, increment, context) {
const tokenTypeIds = this.values.get(tokenAddress);
if (tokenTypeIds === undefined) {
throw new Error(`Token not found: ${tokenAddress}`);
}
const value = tokenTypeIds?.get(tokenTypeId);
const newValue = value + increment;
const history = this.histories.get(tokenId) || [];
const history = this.histories.get(tokenAddress) || [];
if (newValue < -EPSILON) {
throw new Error(`Token value can not become negative. Attempted to set value = ${newValue}`);
}
this.values.set(tokenId, newValue);
history.push({ increment, context });
this.histories.set(tokenId, history);
tokenTypeIds.set(tokenAddress, newValue);
this.values.set(tokenAddress, tokenTypeIds);
history.push({ tokenTypeId, increment, context });
this.histories.set(tokenAddress, history);
}
transferValueFrom(fromTokenId, toTokenId, amount) {
transferValueFrom(from, to, tokenTypeId, amount) {
if (amount === undefined) {
throw new Error('Transfer value: amount is undefined!');
}
@ -60,64 +68,83 @@ export class ReputationTokenContract extends ERC721 {
if (amount < 0) {
throw new Error('Transfer value: amount must be positive');
}
const sourceAvailable = this.availableValueOf(fromTokenId);
const sourceAvailable = this.availableValueOf(from, tokenTypeId);
if (sourceAvailable < amount - EPSILON) {
throw new Error('Token value transfer: source has insufficient available value. '
+ `Needs ${amount}; has ${sourceAvailable}.`);
}
this.incrementValue(fromTokenId, -amount);
this.incrementValue(toTokenId, amount);
this.incrementValue(from, tokenTypeId, -amount);
this.incrementValue(to, tokenTypeId, amount);
}
lock(tokenId, amount, duration) {
const lock = new Lock(tokenId, amount, duration);
batchTransferValueFrom(from, to, tokenTypeIds, amounts) {
for (let idx = 0; idx < tokenTypeIds.length; idx++) {
const tokenTypeId = tokenTypeIds[idx];
const amount = amounts[idx];
if (amount === undefined) {
throw new Error('Transfer value: amount is undefined!');
}
if (amount === 0) {
return;
}
if (amount < 0) {
throw new Error('Transfer value: amount must be positive');
}
const sourceAvailable = this.availableValueOf(from, tokenTypeId);
if (sourceAvailable < amount - EPSILON) {
throw new Error('Token value transfer: source has insufficient available value. '
+ `Needs ${amount}; has ${sourceAvailable}.`);
}
this.incrementValue(from, tokenTypeId, -amount);
this.incrementValue(to, tokenTypeId, amount);
}
}
lock(tokenAddress, tokenTypeId, amount, duration) {
const lock = new Lock(tokenAddress, tokenTypeId, amount, duration);
this.locks.add(lock);
}
historyOf(tokenId) {
return this.histories.get(tokenId);
historyOf(tokenAddress) {
return this.histories.get(tokenAddress);
}
valueOf(tokenId) {
const value = this.values.get(tokenId);
if (value === undefined) {
throw new Error(`Token not found: ${tokenId}`);
valueOf(tokenAddress, tokenTypeId) {
const tokenTypeIds = this.values.get(tokenAddress);
if (tokenTypeIds === undefined) {
throw new Error(`Token not found: ${tokenAddress}`);
}
return value;
return tokenTypeIds.get(tokenTypeId);
}
availableValueOf(tokenId) {
availableValueOf(tokenAddress, tokenTypeId) {
const amountLocked = Array.from(this.locks.values())
.filter(({ tokenId: lockTokenId }) => lockTokenId === tokenId)
.filter((lock) => lock.tokenAddress === tokenAddress && lock.tokenTypeId === tokenTypeId)
.filter(({ dateCreated, duration }) => new Date() - dateCreated < duration)
.reduce((total, { amount }) => total += amount, 0);
return this.valueOf(tokenId) - amountLocked;
return this.valueOf(tokenAddress, tokenTypeId) - amountLocked;
}
valueOwnedBy(ownerId) {
valueOwnedBy(ownerAddress, tokenTypeId) {
return Array.from(this.owners.entries())
.filter(([__, owner]) => owner === ownerId)
.map(([tokenId, __]) => this.valueOf(tokenId))
.filter(([__, owner]) => owner === ownerAddress)
.map(([tokenAddress, __]) => this.valueOf(tokenAddress, tokenTypeId))
.reduce((total, value) => total += value, 0);
}
availableValueOwnedBy(ownerId) {
availableValueOwnedBy(ownerAddress, tokenTypeId) {
return Array.from(this.owners.entries())
.filter(([__, owner]) => owner === ownerId)
.map(([tokenId, __]) => this.availableValueOf(tokenId))
.filter(([__, owner]) => owner === ownerAddress)
.map(([tokenAddress, __]) => this.availableValueOf(tokenAddress, tokenTypeId))
.reduce((total, value) => total += value, 0);
}
getTotal() {
return Array.from(this.values.values()).reduce((total, value) => total += value, 0);
getTotal(tokenTypeId) {
return Array.from(this.values.values())
.flatMap((tokens) => tokens.get(tokenTypeId))
.reduce((total, value) => total += value, 0);
}
getTotalAvailable() {
const amountLocked = Array.from(this.locks.values())
.filter(({ dateCreated, duration }) => new Date() - dateCreated < duration)
.reduce((total, { amount }) => total += amount, 0);
return this.getTotal() - amountLocked;
}
// burn(tokenAddress, tokenTypeId, )
}

View File

@ -75,6 +75,12 @@ export class Edge {
static prepareEditorDocument(graph, doc, from, to) {
const form = doc.form({ name: 'editorForm' }).lastElement;
form.button({
name: 'New Vertex',
cb: () => {
graph.resetEditor();
},
});
doc.remark('<h3>Edit Edge</h3>', { parentEl: form.el });
form
.textField({
@ -88,7 +94,7 @@ export class Edge {
const subFormArray = form.subFormArray({ id: 'edges', name: 'edges' }).lastItem;
const addEdgeForm = (edge) => {
const { subForm } = form.subForm({ name: 'subform', subFormArray }).lastItem;
const { subForm } = form.subForm({ subFormArray }).lastItem;
subForm.textField({
id: 'type', name: 'type', defaultValue: edge.type, required: true,
})

View File

@ -0,0 +1,105 @@
/**
* ERC-1155: Multi Token Standard
* See https://eips.ethereum.org/EIPS/eip-1155
* and https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC1155/ERC1155.sol
*
* This implementation is currently incomplete. It lacks the following:
* - Token approvals
* - Operator approvals
* - Emitting events
* - transferFrom
*/
export class ERC1155 {
constructor(name, symbol) {
this.name = name;
this.symbol = symbol;
this.balances = new Map(); // owner address --> token id --> token count
this.owners = new Map(); // token address --> owner address
// this.tokenApprovals = new Map(); // token address --> approved addresses
// this.operatorApprovals = new Map(); // ownerAddress --> operator approvals
this.events = {
// Transfer: (_from, _to, _tokenAddress) => {},
// Approval: (_owner, _approved, _tokenAddress) => {},
// ApprovalForAll: (_owner, _operator, _approved) => {},
};
}
incrementBalance(ownerAddress, tokenTypeId, increment) {
const tokens = this.balances.get(ownerAddress) ?? new Map();
const balance = tokens.get(tokenTypeId) ?? 0;
tokens.set(tokenTypeId, balance + increment);
this.balances.set(ownerAddress, tokens);
}
mintBatch(to, tokenAddress, tokenTypeIds, amounts = null) {
if (!amounts) {
amounts = tokenTypeIds.map(() => 1);
}
console.log('ERC1155.mintBatch', {
to, tokenAddress, tokenTypeIds, amounts,
});
if (this.owners.get(tokenAddress)) {
throw new Error('ERC1155: token already minted');
}
for (let idx = 0; idx < tokenTypeIds.length; idx++) {
const tokenTypeId = tokenTypeIds[idx];
const amount = amounts[idx];
this.incrementBalance(to, tokenTypeId, amount);
}
this.owners.set(tokenAddress, to);
}
burn(tokenAddress, tokenTypeId, amount = 1) {
const ownerAddress = this.owners.get(tokenAddress);
this.incrementBalance(ownerAddress, tokenTypeId, -amount);
}
balanceOf(ownerAddress, tokenTypeId) {
if (!ownerAddress) {
throw new Error('ERC1155: address zero is not a valid owner');
}
const tokens = this.balances.get(ownerAddress) ?? new Map();
return tokens.get(tokenTypeId) ?? 0;
}
ownerOf(tokenAddress) {
const ownerAddress = this.owners.get(tokenAddress);
if (!ownerAddress) {
throw new Error(`ERC1155: invalid token address: ${tokenAddress}`);
}
return ownerAddress;
}
transfer(from, to, tokenAddress) {
console.log('ERC1155.transfer', { from, to, tokenAddress });
const ownerAddress = this.owners.get(tokenAddress);
if (ownerAddress !== from) {
throw new Error(`ERC1155: transfer from incorrect owner ${from}; should be ${ownerAddress}`);
}
this.incrementBalance(from, -1);
this.incrementBalance(to, 1);
this.owners.set(tokenAddress, to);
}
/// @notice Enable or disable approval for a third party ("operator") to manage
/// all of `msg.sender`'s assets
/// @dev Emits the ApprovalForAll event. The contract MUST allow
/// multiple operators per ownerAddress.
/// @param _operator Address to add to the set of authorized operators
/// @param _approved True if the operator is approved, false to revoke approval
// setApprovalForAll(_operator, _approved) {}
/// @notice Get the approved address for a single NFT
/// @dev Throws if `_tokenAddress` is not a valid NFT.
/// @param _tokenAddress The NFT to find the approved address for
/// @return The approved address for this NFT, or the zero address if there is none
// getApproved(_tokenAddress) {}
/// @notice Query if an address is an authorized operator for another address
/// @param _owner The address that owns the NFTs
/// @param _operator The address that acts on behalf of the ownerAddress
/// @return True if `_operator` is an approved operator for `_owner`, false otherwise
// isApprovedForAll(_owner, _operator) {}
}

View File

@ -16,78 +16,78 @@ export class ERC721 {
this.balances = new Map(); // owner address --> token count
this.owners = new Map(); // token id --> owner address
// this.tokenApprovals = new Map(); // token id --> approved addresses
// this.operatorApprovals = new Map(); // owner --> operator approvals
// this.operatorApprovals = new Map(); // ownerAddress --> operator approvals
this.events = {
// Transfer: (_from, _to, _tokenId) => {},
// Approval: (_owner, _approved, _tokenId) => {},
// Transfer: (_from, _to, _tokenAddress) => {},
// Approval: (_owner, _approved, _tokenAddress) => {},
// ApprovalForAll: (_owner, _operator, _approved) => {},
};
}
incrementBalance(owner, increment) {
const balance = this.balances.get(owner) ?? 0;
this.balances.set(owner, balance + increment);
incrementBalance(ownerAddress, increment) {
const balance = this.balances.get(ownerAddress) ?? 0;
this.balances.set(ownerAddress, balance + increment);
}
mint(to, tokenId) {
console.log('ERC721.mint', { to, tokenId });
if (this.owners.get(tokenId)) {
mint(to, tokenAddress) {
console.log('ERC721.mint', { to, tokenAddress });
if (this.owners.get(tokenAddress)) {
throw new Error('ERC721: token already minted');
}
this.incrementBalance(to, 1);
this.owners.set(tokenId, to);
this.owners.set(tokenAddress, to);
}
burn(tokenId) {
const owner = this.owners.get(tokenId);
this.incrementBalance(owner, -1);
this.owners.delete(tokenId);
burn(tokenAddress) {
const ownerAddress = this.owners.get(tokenAddress);
this.incrementBalance(ownerAddress, -1);
this.owners.delete(tokenAddress);
}
balanceOf(owner) {
if (!owner) {
balanceOf(ownerAddress) {
if (!ownerAddress) {
throw new Error('ERC721: address zero is not a valid owner');
}
return this.balances.get(owner) ?? 0;
return this.balances.get(ownerAddress) ?? 0;
}
ownerOf(tokenId) {
const owner = this.owners.get(tokenId);
if (!owner) {
throw new Error(`ERC721: invalid token ID: ${tokenId}`);
ownerOf(tokenAddress) {
const ownerAddress = this.owners.get(tokenAddress);
if (!ownerAddress) {
throw new Error(`ERC721: invalid token ID: ${tokenAddress}`);
}
return owner;
return ownerAddress;
}
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 ${from}; should be ${owner}`);
transfer(from, to, tokenAddress) {
console.log('ERC721.transfer', { from, to, tokenAddress });
const ownerAddress = this.owners.get(tokenAddress);
if (ownerAddress !== from) {
throw new Error(`ERC721: transfer from incorrect owner ${from}; should be ${ownerAddress}`);
}
this.incrementBalance(from, -1);
this.incrementBalance(to, 1);
this.owners.set(tokenId, to);
this.owners.set(tokenAddress, to);
}
/// @notice Enable or disable approval for a third party ("operator") to manage
/// all of `msg.sender`'s assets
/// @dev Emits the ApprovalForAll event. The contract MUST allow
/// multiple operators per owner.
/// multiple operators per ownerAddress.
/// @param _operator Address to add to the set of authorized operators
/// @param _approved True if the operator is approved, false to revoke approval
// setApprovalForAll(_operator, _approved) {}
/// @notice Get the approved address for a single NFT
/// @dev Throws if `_tokenId` is not a valid NFT.
/// @param _tokenId The NFT to find the approved address for
/// @dev Throws if `_tokenAddress` is not a valid NFT.
/// @param _tokenAddress The NFT to find the approved address for
/// @return The approved address for this NFT, or the zero address if there is none
// getApproved(_tokenId) {}
// getApproved(_tokenAddress) {}
/// @notice Query if an address is an authorized operator for another address
/// @param _owner The address that owns the NFTs
/// @param _operator The address that acts on behalf of the owner
/// @param _operator The address that acts on behalf of the ownerAddress
/// @return True if `_operator` is an approved operator for `_owner`, false otherwise
// isApprovedForAll(_owner, _operator) {}
}

View File

@ -0,0 +1,123 @@
import { Document } from '../display/document.js';
export const PropertyTypes = {
string: 'string',
number: 'number',
};
class Property {
constructor(name, type) {
this.name = name;
this.type = type;
}
}
class NodeType {
constructor(name) {
this.name = name;
this.properties = new Set();
}
addProperty(name, type) {
this.properties.add(new Property(name, type));
}
}
class EdgeType {
constructor(name, fromNodeTypes, toNodeTypes) {
this.name = name;
this.fromNodeTypes = fromNodeTypes;
this.toNodeTypes = toNodeTypes;
}
}
export class Schema {
constructor() {
this.nodeTypes = new Set();
this.edgeTypes = new Set();
}
addNodeType(name) {
const nodeType = new NodeType(name);
this.nodeTypes.add(nodeType);
}
addEdgeType(name) {
const edgeType = new EdgeType(name);
this.nodeTypes.add(edgeType);
}
}
export class SchemaEditor {
constructor(name, scene, options = {}) {
this.name = name;
this.scene = scene;
this.options = options;
this.schemaEditorBox = scene.middleSection.addBox('Schema Editor').flex();
this.nodeEditorDoc = new Document('NodeSchemaEditor', this.schemaEditorBox.el);
this.edgeEditorDoc = new Document('EdgeSchemaEditor', this.schemaEditorBox.el);
this.prepareNodeEditorDocument();
this.prepareEdgeEditorDocument();
}
prepareNodeEditorDocument(schema = new Schema()) {
const doc = this.nodeEditorDoc;
doc.clear();
doc.remark('<h2>Node Types</h2>');
const form = doc.form({ name: 'Node Types Editor' }).lastElement;
const nodeTypesSubFormArray = form.subFormArray({ name: 'nodeTypes' }).lastItem;
const addNodeForm = (name, properties) => {
const nodeTypeForm = nodeTypesSubFormArray.subForm().lastSubForm;
if (name) {
nodeTypeForm.remark(`<h3>Node Type: ${name}</h3>`);
} else {
nodeTypeForm.remark('<h3>New Node Type</h3>');
}
nodeTypeForm.textField({ name: 'name', defaultValue: name });
const propertiesSubFormArray = nodeTypeForm.subFormArray({ name: 'properties' }).lastItem;
for (const property of properties.values()) {
const propertyForm = propertiesSubFormArray.subForm().lastSubForm;
propertyForm.textField({ name: 'name', defaultValue: property.name });
propertyForm.textField({ name: 'type', defaultValue: property.type });
}
};
for (const { name, properties } of schema.nodeTypes.values()) {
addNodeForm(name, properties);
}
form.button({
name: 'Add Node Type',
cb: () => {
addNodeForm('', new Set());
},
});
form.submit({
name: 'Save',
cb: ({ form: { value: formValue } }) => {
console.log('save', { formValue });
},
});
}
prepareEdgeEditorDocument(schema = new Schema()) {
const doc = this.edgeEditorDoc;
doc.clear();
doc.remark('<h2>Edge Types</h2>');
const form = doc.form('Edge Types Editor').lastElement;
for (const { name } of schema.edgeTypes.values()) {
form.remark(`<h3>Edge Type: ${name}</h3>`);
form.textField({ name: 'name', defaultValue: name });
}
form.submit({
name: 'Save',
});
}
}
// Properties
// Data types
// Relationships

View File

@ -1,14 +1,15 @@
export class Stake {
constructor({
tokenId, position, amount, lockingTime,
tokenAddress, tokenTypeId, position, amount, lockingTime,
}) {
this.tokenId = tokenId;
this.tokenAddress = tokenAddress;
this.position = position;
this.amount = amount;
this.lockingTime = lockingTime;
this.tokenTypeId = tokenTypeId;
}
getStakeValue({ lockingTimeExponent } = {}) {
getAmount({ lockingTimeExponent } = {}) {
return this.amount * this.lockingTime ** lockingTimeExponent;
}
}

View File

@ -15,7 +15,7 @@ export class Vertex {
to: [],
};
this.installedClickCallback = false;
this.properties = new Map();
this.properties = options.properties ?? new Map();
}
toJSON() {
@ -51,14 +51,18 @@ export class Vertex {
}
let html = '';
html += `${this.label}`;
if (this.type) {
html += `<span class='small'>${this.type}</span>`;
}
html += `${this.label || this.id}`;
html += '<table>';
console.log('displayVertex', { properties: this.properties });
for (const [key, value] of this.properties.entries()) {
const displayValue = typeof value === 'number' ? displayNumber(value) : value;
html += `<tr><td>${key}</td><td>${displayValue}</td></tr>`;
}
html += '</table>';
if (this.id !== this.label) {
if (this.label && this.id !== this.label) {
html += `<span class=small>${this.id}</span><br>`;
}
html = html.replaceAll(/\n\s*/g, '');
@ -73,26 +77,34 @@ export class Vertex {
static prepareEditorDocument(graph, doc, vertexId) {
const vertex = vertexId ? graph.getVertex(vertexId) : undefined;
const form = doc.form().lastElement;
if (vertex) {
form.button({
name: 'New Vertex',
cb: () => {
graph.resetEditor();
},
});
}
doc.remark(`<h3>${vertex ? 'Edit' : 'Add'} Vertex</h3>`, { parentEl: form.el });
form
.textField({
id: 'id', name: 'id', defaultValue: vertex?.id,
name: 'id', defaultValue: vertex?.id,
})
.textField({ id: 'type', name: 'type', defaultValue: vertex?.type })
.textField({ id: 'label', name: 'label', defaultValue: vertex?.label });
.textField({ name: 'type', defaultValue: vertex?.type })
.textField({ name: 'label', defaultValue: vertex?.label });
doc.remark('<h4>Properties</h4>', { parentEl: form.el });
const subFormArray = form.subFormArray({ id: 'properties', name: 'properties' }).lastItem;
const subFormArray = form.subFormArray({ name: 'properties' }).lastItem;
const addPropertyForm = (key, value) => {
const { subForm } = form.subForm({ name: 'subform', subFormArray }).lastItem;
subForm.textField({ id: 'key', name: 'key', defaultValue: key })
.textField({ id: 'value', name: 'value', defaultValue: value })
const { subForm } = form.subForm({ subFormArray }).lastItem;
subForm.textField({ name: 'key', defaultValue: key })
.textField({ name: 'value', defaultValue: value })
.button({
id: 'remove',
name: 'Remove Property',
cb: () => subFormArray.remove(subForm),
});
doc.remark('<br>', { parentEl: subForm.el });
})
.remark('<br>');
};
if (vertex) {
@ -102,13 +114,11 @@ export class Vertex {
}
form.button({
id: 'add',
name: 'Add Property',
cb: () => addPropertyForm('', ''),
});
form.submit({
id: 'save',
name: 'Save',
cb: ({ form: { value: formValue } }) => {
let fullRedraw = false;
@ -121,7 +131,10 @@ export class Vertex {
Object.assign(vertex, formValue);
vertex.displayVertex();
} else {
const newVertex = graph.addVertex(formValue.type, formValue.id, null, formValue.label);
const {
type, id, label, properties,
} = formValue;
const newVertex = graph.addVertex(type, id, null, label, { properties });
Object.assign(newVertex, formValue);
doc.clear();
Vertex.prepareEditorDocument(graph, doc, newVertex.id);
@ -134,7 +147,6 @@ export class Vertex {
if (vertex) {
form.button({
id: 'delete',
name: 'Delete Vertex',
cb: () => {
graph.deleteVertex(vertex.id);
@ -159,7 +171,6 @@ export class Vertex {
}
form.button({
id: 'cancel',
name: 'Cancel',
cb: () => graph.resetEditor(),
parentEl: doc.el,

View File

@ -131,9 +131,10 @@ export class WeightedDirectedGraph {
const form = this.controlDoc.form({ name: 'controlForm' }).lastElement;
const { subForm: graphPropertiesForm } = form.subForm({ name: 'graphPropsForm' }).lastItem;
graphPropertiesForm.flex()
.textField({ name: 'name', label: 'Graph name', defaultValue: this.name })
.submit({
name: 'Save',
.textField({
name: 'name',
label: 'Graph name',
defaultValue: this.name,
cb: (({ form: { value: { name } } }) => {
this.name = name;
}),

View File

@ -50,6 +50,12 @@ a:visited {
left: 12em;
top: -0.5em;
}
.fixed {
position: fixed;
}
.absolute {
position: absolute;
}
svg {
width: 800px;
}

View File

@ -15,18 +15,19 @@
For more information please see the <a href="https://daogovernanceframework.com/wiki/DAO_Governance_Framework">DGF
Wiki</a>.
</p>
<p>
The code for this site is available in <a
href="https://gitlab.com/dao-governance-framework/science-publishing-dao/-/tree/main/forum-network/src">GitLab</a>.
</p>
<h2>Tools</h2>
<ul>
<li><a href="./wdg-editor">Weighted Directed Graph Editor</a></li>
<li><a href="./schema-editor">Schema Editor</a></li>
</ul>
<h2>Example Scenarios</h2>
<p>
Below are example scenarios with various assertions covering features of our reputation system.
</p>
<p>
The code for this site is available in <a
href="https://gitlab.com/dao-governance-framework/science-publishing-dao/-/tree/main/forum-network/src">GitLab</a>.
</p>
<ul>
<li><a href="./tests/validation-pool.test.html">Validation Pool</a></li>
<li><a href="./tests/availability.test.html">Availability + Business</a></li>
@ -66,6 +67,7 @@
<li><a href="./tests/mocha.test.html">Mocha</a></li>
<li><a href="./tests/input.test.html">Input</a></li>
<li><a href="./tests/document.test.html">Document</a></li>
<li><a href="./tests/force-directed.test.html">Force-Directed Graph</a></li>
</ul>
<ul>
<h4><a href="./tests/all.test.html">All</a></h4>

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<head>
<title>Schema Editor</title>
<link type="text/css" rel="stylesheet" href="../index.css" />
<script type="module" src="./index.js"></script>
<script defer data-domain="dgov.io" src="https://plausible.io/js/script.js"></script>
</head>
<body>
<h2><a href="../">DGF Prototype</a></h2>
<h1>Schema Editor</h1>
<div id="scene"></div>
</body>

View File

@ -0,0 +1,9 @@
import { Box } from '../classes/display/box.js';
import { Scene } from '../classes/display/scene.js';
import { SchemaEditor } from '../classes/supporting/schema.js';
const rootElement = document.getElementById('scene');
const rootBox = new Box('rootBox', rootElement).flex();
window.disableSceneControls = true;
window.scene = new Scene('Schema Editor', rootBox);
window.schemaEditor = new SchemaEditor('new', window.scene);

View File

@ -40,6 +40,7 @@
<script type="module" src="./scripts/forum/forum11.test.js"></script>
<script type="module" src="./scripts/input.test.js"></script>
<script type="module" src="./scripts/document.test.js"></script>
<script type="module" src="./scripts/force-directed.test.js"></script>
<script defer class="mocha-init">
mocha.setup({
ui: 'bdd',

View File

@ -27,6 +27,6 @@
<script defer class="mocha-init">
mocha.setup({
ui: 'bdd',
});
});
window.should = chai.should();
</script>

View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<head>
<title>Force-Directed Graph test</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" />
<script defer data-domain="dgov.io" src="https://plausible.io/js/script.js"></script>
</head>
<body>
<h2><a href="../">DGF Prototype</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/chai/chai.js"></script>
<script src="https://unpkg.com/mocha/mocha.js"></script>
<script defer class="mocha-init">
mocha.setup({
ui: 'bdd',
});
chai.should();
</script>
<script type="module" src="./scripts/force-directed.test.js"></script>

View File

@ -6,6 +6,7 @@ import { Public } from '../../classes/actors/public.js';
import { PostContent } from '../../classes/supporting/post-content.js';
import { delayOrWait } from '../../classes/display/scene-controls.js';
import { mochaRun } from '../../util/helpers.js';
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
const DELAY_INTERVAL = 100;
const POOL_DURATION = 200;
@ -32,7 +33,7 @@ const setup = async () => {
scene.withTable();
dao = new DAO('DGF', scene);
await dao.setDisplayValue('total rep', () => dao.reputation.getTotal());
await dao.setDisplayValue('total rep', () => dao.reputation.getTotal(DEFAULT_REP_TOKEN_TYPE_ID));
experts = [];
@ -58,7 +59,7 @@ const setup = async () => {
await pool1.evaluateWinningConditions();
await delayOrWait(DELAY_INTERVAL);
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(10);
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(10);
const { pool: pool2 } = await experts[1].submitPostWithFee(
new PostContent({ hello: 'to you as well' })
@ -77,8 +78,8 @@ const setup = async () => {
await pool2.evaluateWinningConditions();
await delayOrWait(DELAY_INTERVAL);
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(15);
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(5);
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(15);
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
};
const getActiveWorker = async () => {

View File

@ -0,0 +1,73 @@
import { Box } from '../../classes/display/box.js';
import { ForceDirectedGraph } from '../../classes/display/force-directed.js';
import { Rectangle, Vector } from '../../classes/display/geometry.js';
import { delayOrWait } from '../../classes/display/scene-controls.js';
import { Scene } from '../../classes/display/scene.js';
import { EPSILON } from '../../util/constants.js';
import { mochaRun } from '../../util/helpers.js';
const rootElement = document.getElementById('scene');
const rootBox = new Box('rootBox', rootElement).flex();
window.scene = new Scene('WDG test', rootBox);
describe('Weighted Directed Graph', function tests() {
this.timeout(0);
let graph;
before(() => {
graph = (window.graph = new ForceDirectedGraph('test1', window.scene.middleSection.el));
graph.addBox('box1').setInnerHTML('Box 1');
});
it('rectangle should be a polygon with 4 vertices', () => {
const rect = new Rectangle([0, 0], [1, 1]);
rect.vertices[0].should.eql([0, 0]);
rect.vertices[1].should.eql([0, 1]);
rect.vertices[2].should.eql([1, 1]);
rect.vertices[3].should.eql([1, 0]);
});
it('overlapping boxes should repel with default force', () => {
const rect1 = new Rectangle([0, 0], [1, 1]);
const rect2 = new Rectangle([0, 0], [1, 2]);
rect1.center.should.eql([0.5, 0.5]);
rect2.center.should.eql([0.5, 1]);
const force1 = ForceDirectedGraph.pairwiseForce(rect1, rect2, 10);
force1.should.eql([0, 100]);
});
it('boxes at target radius should have no net force', () => {
const rect1 = new Rectangle([0, 0], [1, 1]);
const rect2 = new Rectangle([10, 0], [1, 1]);
rect1.center.should.eql([0.5, 0.5]);
rect2.center.should.eql([10.5, 0.5]);
const force = ForceDirectedGraph.pairwiseForce(rect1, rect2, 10);
force[0].should.be.within(-EPSILON, EPSILON);
force[1].should.be.within(-EPSILON, EPSILON);
});
it('normalized vector should have length = 1', () => {
const v = Vector.from([2, 0]);
const u = v.normalize();
u.magnitude.should.be.within(1 - EPSILON, 1 + EPSILON);
});
it('random unit vector should have length = 1', () => {
const u = Vector.randomUnitVector(2);
console.log('unit vector:', u);
u.magnitude.should.be.within(1 - EPSILON, 1 + EPSILON);
});
it('can add a second box to the graph', async () => {
await delayOrWait(1000);
graph.addBox('box2').setInnerHTML('Box 2');
for (let i = 1; i < 50; i++) {
await delayOrWait(100);
graph.computeEulerFrame(0.1);
}
});
});
mochaRun();

View File

@ -4,6 +4,7 @@ import { Expert } from '../../classes/actors/expert.js';
import { PostContent } from '../../classes/supporting/post-content.js';
import { DAO } from '../../classes/dao/dao.js';
import { delayOrWait } from '../../classes/display/scene-controls.js';
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
export class ForumTest {
constructor(options) {
@ -86,6 +87,6 @@ export class ForumTest {
await this.newExpert();
await this.dao.addComputedValue('total value', () => this.dao.reputation.getTotal());
await this.dao.addComputedValue('total rep', () => this.dao.reputation.getTotal(DEFAULT_REP_TOKEN_TYPE_ID));
}
}

View File

@ -1,3 +1,4 @@
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../../util/constants.js';
import { mochaRun } from '../../../util/helpers.js';
import { ForumTest } from '../forum.test-util.js';
@ -34,9 +35,9 @@ describe('Forum', function tests() {
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);
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(2.5);
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(2.5);
});
});
});

View File

@ -1,3 +1,4 @@
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../../util/constants.js';
import { mochaRun } from '../../../util/helpers.js';
import { ForumTest } from '../forum.test-util.js';
@ -33,9 +34,9 @@ describe('Forum', function tests() {
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);
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(0);
});
it('Post2 with two authors, one shared with Post1', async () => {
@ -46,9 +47,9 @@ describe('Forum', function tests() {
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);
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(10);
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
});
});
});

View File

@ -1,14 +1,17 @@
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',
};
export const DEFAULT_REP_TOKEN_TYPE_ID = 0;
export const DEFAULT_TARGET_RADIUS = 100;
export const EPSILON = 2.23e-16;
export const INCINERATOR_ADDRESS = '0';
export const OVERLAP_FORCE = 100;
export const DISTANCE_FACTOR = 0.25;
export const MINIMUM_FORCE = 2;
export const VISCOSITY_FACTOR = 0.4;