WIP
This commit is contained in:
parent
629274476c
commit
435633a893
|
@ -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.
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,6 @@ export class Document extends Box {
|
|||
}
|
||||
|
||||
get lastElement() {
|
||||
if (!this.elements.length) return null;
|
||||
return this.elements[this.elements.length - 1];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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(' ');
|
||||
this.topSection = this.box.addBox('Top section').flex();
|
||||
this.displayValuesBox = this.topSection.addBox('Values');
|
||||
|
|
|
@ -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, )
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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) {}
|
||||
}
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}),
|
||||
|
|
|
@ -50,6 +50,12 @@ a:visited {
|
|||
left: 12em;
|
||||
top: -0.5em;
|
||||
}
|
||||
.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
.absolute {
|
||||
position: absolute;
|
||||
}
|
||||
svg {
|
||||
width: 800px;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
|
@ -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',
|
||||
|
|
|
@ -27,6 +27,6 @@
|
|||
<script defer class="mocha-init">
|
||||
mocha.setup({
|
||||
ui: 'bdd',
|
||||
});
|
||||
});
|
||||
window.should = chai.should();
|
||||
</script>
|
||||
|
|
|
@ -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>
|
|
@ -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 () => {
|
||||
|
|
|
@ -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();
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue