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;
|
return 0;
|
||||||
}
|
}
|
||||||
const authorEdges = authorVertex.getEdges(EdgeTypes.AUTHOR, false);
|
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);
|
return tokenValues.reduce((value, total) => total += value, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ export class Expert extends ReputationHolder {
|
||||||
await this.actions.submitPost.log(this, post);
|
await this.actions.submitPost.log(this, post);
|
||||||
const postId = post.id;
|
const postId = post.id;
|
||||||
const pool = await this.initiateValidationPool({ fee, postId }, params);
|
const pool = await this.initiateValidationPool({ fee, postId }, params);
|
||||||
this.tokens.push(pool.tokenId);
|
this.tokens.push(pool.tokenAddress);
|
||||||
return { postId, pool };
|
return { postId, pool };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ export class Expert extends ReputationHolder {
|
||||||
postId,
|
postId,
|
||||||
fee,
|
fee,
|
||||||
}, params);
|
}, params);
|
||||||
this.tokens.push(pool.tokenId);
|
this.tokens.push(pool.tokenAddress);
|
||||||
return pool;
|
return pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ export class Expert extends ReputationHolder {
|
||||||
`(${position ? 'for' : 'against'}, stake: ${amount})`,
|
`(${position ? 'for' : 'against'}, stake: ${amount})`,
|
||||||
);
|
);
|
||||||
return validationPool.stake(this.reputationPublicKey, {
|
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, {
|
this.workerId = await this.dao.availability.register(this.reputationPublicKey, {
|
||||||
stakeAmount,
|
stakeAmount,
|
||||||
tokenId: this.tokens[0],
|
tokenAddress: this.tokens[0],
|
||||||
duration,
|
duration,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@ import { Actor } from '../display/actor.js';
|
||||||
import { CryptoUtil } from '../supporting/crypto.js';
|
import { CryptoUtil } from '../supporting/crypto.js';
|
||||||
|
|
||||||
class Worker {
|
class Worker {
|
||||||
constructor(reputationPublicKey, tokenId, stakeAmount, duration) {
|
constructor(reputationPublicKey, tokenAddress, stakeAmount, duration) {
|
||||||
this.reputationPublicKey = reputationPublicKey;
|
this.reputationPublicKey = reputationPublicKey;
|
||||||
this.tokenId = tokenId;
|
this.tokenAddress = tokenAddress;
|
||||||
this.stakeAmount = stakeAmount;
|
this.stakeAmount = stakeAmount;
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
this.available = true;
|
this.available = true;
|
||||||
|
@ -28,11 +28,11 @@ export class Availability extends Actor {
|
||||||
this.workers = new Map();
|
this.workers = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
register(reputationPublicKey, { stakeAmount, tokenId, duration }) {
|
register(reputationPublicKey, { stakeAmount, tokenAddress, duration }) {
|
||||||
// TODO: Should be signed by token owner
|
// 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();
|
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;
|
return workerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,7 +87,7 @@ export class Business extends Actor {
|
||||||
});
|
});
|
||||||
|
|
||||||
await pool.stake(reputationPublicKey, {
|
await pool.stake(reputationPublicKey, {
|
||||||
tokenId: request.worker.tokenId,
|
tokenAddress: request.worker.tokenAddress,
|
||||||
amount: request.worker.stakeAmount,
|
amount: request.worker.stakeAmount,
|
||||||
position: true,
|
position: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { Availability } from './availability.js';
|
||||||
import { Business } from './business.js';
|
import { Business } from './business.js';
|
||||||
import { Voter } from '../supporting/voter.js';
|
import { Voter } from '../supporting/voter.js';
|
||||||
import { Actor } from '../display/actor.js';
|
import { Actor } from '../display/actor.js';
|
||||||
|
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Purpose:
|
* 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()
|
return this.listActiveVoters()
|
||||||
.map(({ reputationPublicKey }) => this.reputation.valueOwnedBy(reputationPublicKey))
|
.map(({ reputationPublicKey }) => this.reputation.availableValueOwnedBy(reputationPublicKey, reputationTypeId))
|
||||||
.reduce((acc, cur) => (acc += cur), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
getActiveAvailableReputation() {
|
|
||||||
return this.listActiveVoters()
|
|
||||||
.map(({ reputationPublicKey }) => this.reputation.availableValueOwnedBy(reputationPublicKey))
|
|
||||||
.reduce((acc, cur) => (acc += cur), 0);
|
.reduce((acc, cur) => (acc += cur), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,8 @@ class Post extends Actor {
|
||||||
this.forum = forum;
|
this.forum = forum;
|
||||||
this.id = postContent.id ?? name;
|
this.id = postContent.id ?? name;
|
||||||
this.senderId = senderId;
|
this.senderId = senderId;
|
||||||
this.value = 0;
|
this.values = new Map();
|
||||||
this.initialValue = 0;
|
this.initialValues = new Map();
|
||||||
this.authors = postContent.authors;
|
this.authors = postContent.authors;
|
||||||
this.citations = postContent.citations;
|
this.citations = postContent.citations;
|
||||||
this.title = postContent.title;
|
this.title = postContent.title;
|
||||||
|
@ -83,16 +83,21 @@ export class Forum extends ReputationHolder {
|
||||||
// getContract(type) { }
|
// getContract(type) { }
|
||||||
|
|
||||||
async onValidate({
|
async onValidate({
|
||||||
pool, postId, tokenId, referenceChainLimit, leachingValue,
|
pool, postId, tokenAddress, referenceChainLimit, leachingValue,
|
||||||
}) {
|
}) {
|
||||||
console.log('onValidate', { pool, postId, tokenId });
|
console.log('onValidate', { pool, postId, tokenAddress });
|
||||||
const initialValue = this.dao.reputation.valueOf(tokenId);
|
|
||||||
|
// 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 postVertex = this.graph.getVertex(postId);
|
||||||
const post = postVertex.data;
|
const post = postVertex.data;
|
||||||
post.setStatus('Validated');
|
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.
|
// For graph display purposes, we want to use the existing Expert actors from the current scene.
|
||||||
const author = this.scene.findActor(({ reputationPublicKey }) => reputationPublicKey === publicKey);
|
const author = this.scene.findActor(({ reputationPublicKey }) => reputationPublicKey === publicKey);
|
||||||
author.setDisplayValue('reputation', () => author.getReputation());
|
author.setDisplayValue('reputation', () => author.getReputation());
|
||||||
|
@ -105,7 +110,7 @@ export class Forum extends ReputationHolder {
|
||||||
postVertex,
|
postVertex,
|
||||||
authorVertex,
|
authorVertex,
|
||||||
weight,
|
weight,
|
||||||
{ tokenId: authorTokenId },
|
{ tokenAddress: authorTokenAddress },
|
||||||
{ hide: author.options.hide },
|
{ 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.
|
// If no authors are specified, treat the sender as the sole author.
|
||||||
// TODO: Verify that cumulative author weight == 1.
|
// TODO: Verify that cumulative author weight == 1.
|
||||||
if (!post.authors?.length) {
|
if (!post.authors?.length) {
|
||||||
addAuthorToGraph(post.senderId, 1, tokenId);
|
addAuthorToGraph(post.senderId, 1, tokenAddress);
|
||||||
} else {
|
} else {
|
||||||
for (const { publicKey, weight } of post.authors) {
|
for (const { publicKey, weight } of post.authors) {
|
||||||
// If the sender is also listed among the authors, do not mint them an additional token.
|
// 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);
|
const authorTokenAddress = (publicKey === post.senderId)
|
||||||
addAuthorToGraph(publicKey, weight, authorTokenId);
|
? 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 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)) {
|
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 } },
|
{ to: postVertex, from: { data: pool } },
|
||||||
{
|
{
|
||||||
rewardsAccumulator,
|
rewardsAccumulator,
|
||||||
increment: initialValue,
|
increments: initialValues,
|
||||||
referenceChainLimit,
|
referenceChainLimit,
|
||||||
leachingValue,
|
leachingValue,
|
||||||
},
|
},
|
||||||
|
@ -142,16 +149,16 @@ export class Forum extends ReputationHolder {
|
||||||
|
|
||||||
// Apply computed rewards to update values of tokens
|
// Apply computed rewards to update values of tokens
|
||||||
for (const [authorEdge, amount] of rewardsAccumulator) {
|
for (const [authorEdge, amount] of rewardsAccumulator) {
|
||||||
const { to: authorVertex, data: { tokenId: authorTokenId } } = authorEdge;
|
const { to: authorVertex, data: { tokenAddress: authorTokenAddress } } = authorEdge;
|
||||||
const { data: author } = authorVertex;
|
const { data: author } = authorVertex;
|
||||||
// The primary author gets the validation pool minted token.
|
// The primary author gets the validation pool minted token.
|
||||||
// So we don't need to transfer any reputation to the primary author.
|
// So we don't need to transfer any reputation to the primary author.
|
||||||
// Their reward will be the remaining balance after all other transfers.
|
// Their reward will be the remaining balance after all other transfers.
|
||||||
if (authorTokenId !== tokenId) {
|
if (authorTokenAddress !== tokenAddress) {
|
||||||
if (amount < 0) {
|
if (amount < 0) {
|
||||||
this.dao.reputation.transferValueFrom(authorTokenId, tokenId, -amount);
|
this.dao.reputation.transferValueFrom(authorTokenAddress, tokenAddress, -amount);
|
||||||
} else {
|
} else {
|
||||||
this.dao.reputation.transferValueFrom(tokenId, authorTokenId, amount);
|
this.dao.reputation.transferValueFrom(tokenAddress, authorTokenAddress, amount);
|
||||||
}
|
}
|
||||||
await author.computeDisplayValues((label, value) => authorVertex.setProperty(label, value));
|
await author.computeDisplayValues((label, value) => authorVertex.setProperty(label, value));
|
||||||
authorVertex.displayVertex();
|
authorVertex.displayVertex();
|
||||||
|
@ -167,8 +174,8 @@ export class Forum extends ReputationHolder {
|
||||||
for (const authorEdge of postVertex.getEdges(EdgeTypes.AUTHOR, true)) {
|
for (const authorEdge of postVertex.getEdges(EdgeTypes.AUTHOR, true)) {
|
||||||
const authorVertex = authorEdge.to;
|
const authorVertex = authorEdge.to;
|
||||||
const author = authorVertex.data;
|
const author = authorVertex.data;
|
||||||
const { tokenId: authorTokenId } = authorEdge.data;
|
const { tokenAddress: authorTokenAddress } = authorEdge.data;
|
||||||
this.dao.reputation.transfer(this.id, author.reputationPublicKey, authorTokenId);
|
this.dao.reputation.transfer(this.id, author.reputationPublicKey, authorTokenAddress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,7 +185,7 @@ export class Forum extends ReputationHolder {
|
||||||
*/
|
*/
|
||||||
async propagateValue(edge, {
|
async propagateValue(edge, {
|
||||||
rewardsAccumulator,
|
rewardsAccumulator,
|
||||||
increment,
|
increments,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
initialNegative = false,
|
initialNegative = false,
|
||||||
referenceChainLimit,
|
referenceChainLimit,
|
||||||
|
@ -186,7 +193,8 @@ export class Forum extends ReputationHolder {
|
||||||
}) {
|
}) {
|
||||||
const postVertex = edge.to;
|
const postVertex = edge.to;
|
||||||
const post = postVertex.data;
|
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) {
|
if (!!referenceChainLimit && depth > referenceChainLimit) {
|
||||||
this.actions.propagate.log(
|
this.actions.propagate.log(
|
||||||
|
@ -196,7 +204,7 @@ export class Forum extends ReputationHolder {
|
||||||
null,
|
null,
|
||||||
'-x',
|
'-x',
|
||||||
);
|
);
|
||||||
return increment;
|
return increments;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('propagateValue start', {
|
console.log('propagateValue start', {
|
||||||
|
@ -204,17 +212,20 @@ export class Forum extends ReputationHolder {
|
||||||
to: edge.to.id,
|
to: edge.to.id,
|
||||||
depth,
|
depth,
|
||||||
value: post.value,
|
value: post.value,
|
||||||
increment,
|
increments,
|
||||||
initialNegative,
|
initialNegative,
|
||||||
});
|
});
|
||||||
|
|
||||||
const propagate = async (positive) => {
|
const propagate = async (positive) => {
|
||||||
let totalOutboundAmount = 0;
|
const totalOutboundAmounts = increments.map(() => 0);
|
||||||
const citationEdges = postVertex.getEdges(EdgeTypes.CITATION, true)
|
const citationEdges = postVertex.getEdges(EdgeTypes.CITATION, true)
|
||||||
.filter(({ weight }) => (positive ? weight > 0 : weight < 0));
|
.filter(({ weight }) => (positive ? weight > 0 : weight < 0));
|
||||||
for (const citationEdge of citationEdges) {
|
for (const citationEdge of citationEdges) {
|
||||||
const { weight } = citationEdge;
|
const { weight } = citationEdge;
|
||||||
let outboundAmount = weight * increment;
|
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) {
|
if (Math.abs(outboundAmount) > EPSILON) {
|
||||||
const balanceToOutbound = this.graph.getEdgeWeight(EdgeTypes.BALANCE, citationEdge.from, citationEdge.to)
|
const balanceToOutbound = this.graph.getEdgeWeight(EdgeTypes.BALANCE, citationEdge.from, citationEdge.to)
|
||||||
?? 0;
|
?? 0;
|
||||||
|
@ -228,7 +239,7 @@ export class Forum extends ReputationHolder {
|
||||||
this.actions.propagate.log(
|
this.actions.propagate.log(
|
||||||
citationEdge.from.data,
|
citationEdge.from.data,
|
||||||
{ name: 'Incinerator' },
|
{ name: 'Incinerator' },
|
||||||
`(${increment})`,
|
incrementsStr,
|
||||||
undefined,
|
undefined,
|
||||||
'-x',
|
'-x',
|
||||||
);
|
);
|
||||||
|
@ -236,7 +247,7 @@ export class Forum extends ReputationHolder {
|
||||||
}
|
}
|
||||||
// Reputation sent to the incinerator is burned! This means it is deducted from the sender,
|
// Reputation sent to the incinerator is burned! This means it is deducted from the sender,
|
||||||
// without increasing the value of any other token.
|
// without increasing the value of any other token.
|
||||||
this.actions.propagate.log(citationEdge.from.data, { name: 'Incinerator' }, `(${increment})`);
|
this.actions.propagate.log(citationEdge.from.data, { name: 'Incinerator' }, incrementsStr);
|
||||||
} else {
|
} else {
|
||||||
// We need to ensure that we at most undo the prior effects of this post
|
// We need to ensure that we at most undo the prior effects of this post
|
||||||
if (initialNegative) {
|
if (initialNegative) {
|
||||||
|
@ -269,30 +280,36 @@ export class Forum extends ReputationHolder {
|
||||||
citationEdge.to,
|
citationEdge.to,
|
||||||
balanceToOutbound + outboundAmount,
|
balanceToOutbound + outboundAmount,
|
||||||
);
|
);
|
||||||
totalOutboundAmount += outboundAmount;
|
refundsFromOutbound[idx] = refundFromOutbound;
|
||||||
|
totalOutboundAmounts[idx] += outboundAmount;
|
||||||
|
}
|
||||||
|
const refundStr = refundsFromOutbound.map((refund) => displayNumber(refund)).join('/');
|
||||||
this.actions.confirm.log(
|
this.actions.confirm.log(
|
||||||
citationEdge.to.data,
|
citationEdge.to.data,
|
||||||
citationEdge.from.data,
|
citationEdge.from.data,
|
||||||
`(refund: ${displayNumber(refundFromOutbound)}, leach: ${outboundAmount * leachingValue})`,
|
`(refund: ${refundStr}, leach: ${outboundAmount * leachingValue})`,
|
||||||
undefined,
|
undefined,
|
||||||
'-->>',
|
'-->>',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return totalOutboundAmount;
|
return totalOutboundAmounts;
|
||||||
};
|
};
|
||||||
|
|
||||||
// First, leach value via negative citations
|
// First, leach value via negative citations
|
||||||
const totalLeachingAmount = await propagate(false);
|
const totalLeachingAmounts = await propagate(false);
|
||||||
increment -= totalLeachingAmount * leachingValue;
|
for (let idx = 0; idx < totalLeachingAmounts.length; idx++) {
|
||||||
|
increments[idx] -= totalLeachingAmounts[idx] * leachingValue;
|
||||||
|
}
|
||||||
|
|
||||||
// Now propagate value via positive citations
|
// Now propagate value via positive citations
|
||||||
const totalDonationAmount = await propagate(true);
|
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
|
// 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 newValue = Math.max(0, rawNewValue);
|
||||||
const appliedIncrement = newValue - post.value;
|
const appliedIncrement = newValue - post.value;
|
||||||
const refundToInbound = increment - appliedIncrement;
|
const refundToInbound = increment - appliedIncrement;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { ReputationHolder } from '../reputation/reputation-holder.js';
|
||||||
import { Stake } from '../supporting/stake.js';
|
import { Stake } from '../supporting/stake.js';
|
||||||
import { Action } from '../display/action.js';
|
import { Action } from '../display/action.js';
|
||||||
import { displayNumber } from '../../util/helpers.js';
|
import { displayNumber } from '../../util/helpers.js';
|
||||||
|
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
/* Validation Pool parameters */
|
/* Validation Pool parameters */
|
||||||
|
@ -51,6 +52,7 @@ export class ValidationPool extends ReputationHolder {
|
||||||
duration,
|
duration,
|
||||||
tokenLossRatio,
|
tokenLossRatio,
|
||||||
contentiousDebate = false,
|
contentiousDebate = false,
|
||||||
|
reputationTypes,
|
||||||
},
|
},
|
||||||
name,
|
name,
|
||||||
scene,
|
scene,
|
||||||
|
@ -69,6 +71,16 @@ export class ValidationPool extends ReputationHolder {
|
||||||
this.actions.initiate.log(fromActor, this, `(fee: ${fee})`);
|
this.actions.initiate.log(fromActor, this, `(fee: ${fee})`);
|
||||||
this.activate();
|
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 = true, we will follow the progression defined by getTokenLossRatio()
|
||||||
if (
|
if (
|
||||||
!contentiousDebate
|
!contentiousDebate
|
||||||
|
@ -130,22 +142,34 @@ export class ValidationPool extends ReputationHolder {
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
this.tokenLossRatio = tokenLossRatio;
|
this.tokenLossRatio = tokenLossRatio;
|
||||||
this.contentiousDebate = contentiousDebate;
|
this.contentiousDebate = contentiousDebate;
|
||||||
this.mintedValue = fee * params.mintingRatio();
|
|
||||||
this.tokenId = this.dao.reputation.mint(this.id, this.mintedValue);
|
const mintTotal = fee * params.mintingRatio();
|
||||||
// Tokens minted "for" the post go toward stake of author voting for their own post.
|
const reputationTypeIds = this.reputationTypes
|
||||||
// Also, author can provide additional stakes, e.g. availability stakes for work evidence post.
|
.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, {
|
this.stake(this.id, {
|
||||||
position: true,
|
position: true,
|
||||||
amount: this.mintedValue * params.stakeForAuthor,
|
amount: mintTotal * params.stakeForAuthor * weight,
|
||||||
tokenId: this.tokenId,
|
tokenAddress: this.tokenAddress,
|
||||||
|
reputationTypeId,
|
||||||
});
|
});
|
||||||
this.stake(this.id, {
|
this.stake(this.id, {
|
||||||
position: false,
|
position: false,
|
||||||
amount: this.mintedValue * (1 - params.stakeForAuthor),
|
amount: this.mintedValue * (1 - params.stakeForAuthor) * weight,
|
||||||
tokenId: this.tokenId,
|
tokenAddress: this.tokenAddress,
|
||||||
|
reputationTypeId,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.actions.mint.log(this, this, `(${this.mintedValue})`);
|
this.actions.mint.log(this, this, `(${mintTotal})`);
|
||||||
|
|
||||||
// Keep a record of voters and their votes
|
// Keep a record of voters and their votes
|
||||||
this.dao.addVoteRecord(reputationPublicKey, this);
|
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
|
* @param {boolean} options.excludeSystem: Whether to exclude votes cast during pool initialization
|
||||||
* @returns stake[]
|
* @returns stake[]
|
||||||
*/
|
*/
|
||||||
getStakes(outcome, { excludeSystem }) {
|
getStakes({ outcome, tokenTypeId, excludeSystem }) {
|
||||||
return Array.from(this.stakes.values())
|
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);
|
.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
|
* @returns number
|
||||||
*/
|
*/
|
||||||
getTotalStakedOnPost(outcome) {
|
getStakedAmount({ outcome, tokenTypeId }) {
|
||||||
return this.getStakes(outcome, { excludeSystem: false })
|
return this.getStakes({ outcome, tokenTypeId, excludeSystem: false })
|
||||||
.map((stake) => stake.getStakeValue({ lockingTimeExponent: params.lockingTimeExponent }))
|
.map((stake) => stake.getAmount({ lockingTimeExponent: params.lockingTimeExponent }))
|
||||||
.reduce((acc, cur) => (acc += cur), 0);
|
.reduce((total, amount) => (total += amount), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
|
* Stake reputation in favor of a given outcome for this validation pool.
|
||||||
* @returns number
|
*
|
||||||
|
* @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, {
|
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) {
|
if (this.state === ValidationPoolStates.CLOSED) {
|
||||||
throw new Error(`Validation pool ${this.id} is 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!');
|
throw new Error('Reputation may only be staked by its owner!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const stake = new Stake({
|
const stake = new Stake({
|
||||||
tokenId, position, amount, lockingTime,
|
tokenAddress, tokenTypeId, position, amount, lockingTime,
|
||||||
});
|
});
|
||||||
this.stakes.add(stake);
|
this.stakes.add(stake);
|
||||||
|
|
||||||
// Transfer staked amount from the sender to the validation pool
|
// 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
|
// Keep a record of voters and their votes
|
||||||
if (reputationPublicKey !== this.id) {
|
if (reputationPublicKey !== this.id) {
|
||||||
|
@ -243,8 +266,10 @@ export class ValidationPool extends ReputationHolder {
|
||||||
// Before evaluating the winning conditions,
|
// Before evaluating the winning conditions,
|
||||||
// we need to make sure any staked tokens are locked for the
|
// we need to make sure any staked tokens are locked for the
|
||||||
// specified amounts of time.
|
// specified amounts of time.
|
||||||
for (const { tokenId, amount, lockingTime } of this.stakes.values()) {
|
for (const {
|
||||||
this.dao.reputation.lock(tokenId, amount, lockingTime);
|
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.
|
// 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.state = ValidationPoolStates.CLOSED;
|
||||||
this.setStatus('Closed');
|
this.setStatus('Closed');
|
||||||
|
|
||||||
const upvoteValue = this.getTotalValueOfStakesForOutcome(true);
|
// Votes should be scaled by weights of this.reputationTypes
|
||||||
const downvoteValue = this.getTotalValueOfStakesForOutcome(false);
|
const upvoteValue = this.reputationTypes.reduce((value, { reputationTypeId, weight }) => {
|
||||||
const activeAvailableReputation = this.dao.getActiveAvailableReputation();
|
value += this.getStakedAmount({ outcome: true, reputationTypeId }) * weight;
|
||||||
const votePasses = upvoteValue >= params.winningRatio * downvoteValue;
|
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 quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation;
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
votePasses,
|
outcome,
|
||||||
upvoteValue,
|
upvoteValue,
|
||||||
downvoteValue,
|
downvoteValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (quorumMet) {
|
if (quorumMet) {
|
||||||
this.setStatus(`Resolved - ${votePasses ? 'Won' : 'Lost'}`);
|
this.setStatus(`Resolved - ${outcome ? 'Won' : 'Lost'}`);
|
||||||
this.scene?.sequence.log(`note over ${this.name} : ${votePasses ? 'Win' : 'Lose'}`);
|
this.scene?.sequence.log(`note over ${this.name} : ${outcome ? 'Win' : 'Lose'}`);
|
||||||
this.applyTokenLocking();
|
this.applyTokenLocking();
|
||||||
await this.distributeReputation({ votePasses });
|
await this.distributeReputation({ outcome });
|
||||||
// TODO: distribute fees
|
// TODO: distribute fees
|
||||||
} else {
|
} else {
|
||||||
this.setStatus('Resolved - Quorum not met');
|
this.setStatus('Resolved - Quorum not met');
|
||||||
|
@ -301,47 +336,57 @@ export class ValidationPool extends ReputationHolder {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async distributeReputation({ votePasses }) {
|
async distributeReputation({ outcome }) {
|
||||||
// For now we assume a tightly binding pool, where all staked reputation is lost
|
// In a binding validation pool, losing voter stakes are transferred to winning voters.
|
||||||
// TODO: Take tokenLossRatio into account
|
// TODO: Regression tests for different tokenLossRatio values
|
||||||
// TODO: revoke staked reputation from losing voters
|
const tokenLossRatio = this.getTokenLossRatio();
|
||||||
|
for (const { reputationTypeId, weight } of this.reputationTypes) {
|
||||||
// In a tightly binding validation pool, losing voter stakes are transferred to winning voters.
|
const tokensForWinners = this.getStakedAmount({ outcome: !outcome, reputationTypeId }) * weight * tokenLossRatio;
|
||||||
const tokensForWinners = this.getTotalStakedOnPost(!votePasses);
|
const winningEntries = this.getStakes({ outcome, reputationTypeId, excludeSystem: true });
|
||||||
const winningEntries = this.getStakes(votePasses, { excludeSystem: true });
|
const totalValueOfStakesForWin = this.getStakedAmount({ outcome, reputationTypeId });
|
||||||
const totalValueOfStakesForWin = this.getTotalValueOfStakesForOutcome(votePasses);
|
|
||||||
|
|
||||||
// Compute rewards for the winning voters, in proportion to the value of their stakes.
|
// Compute rewards for the winning voters, in proportion to the value of their stakes.
|
||||||
for (const stake of winningEntries) {
|
for (const stake of winningEntries) {
|
||||||
const { tokenId, amount } = stake;
|
const { tokenAddress, amount } = stake;
|
||||||
const value = stake.getStakeValue({ lockingTimeExponent: params.lockingTimeExponent });
|
const value = stake.getAmount({ lockingTimeExponent: params.lockingTimeExponent });
|
||||||
const reward = tokensForWinners * (value / totalValueOfStakesForWin);
|
const reward = tokensForWinners * (value / totalValueOfStakesForWin);
|
||||||
// Also return each winning voter their staked amount
|
// Also return each winning voter their staked amount
|
||||||
const reputationPublicKey = this.dao.reputation.ownerOf(tokenId);
|
const reputationPublicKey = this.dao.reputation.ownerOf(tokenAddress);
|
||||||
console.log(`reward for winning stake by ${reputationPublicKey}: ${reward}`);
|
console.log(`reward of type ${reputationTypeId} for winning stake by ${reputationPublicKey}: ${reward}`);
|
||||||
this.dao.reputation.transferValueFrom(this.tokenId, tokenId, reward + amount);
|
this.dao.reputation.transferValueFrom(this.tokenAddress, tokenAddress, reputationTypeId, reward + amount);
|
||||||
const toActor = this.scene?.findActor((actor) => actor.reputationPublicKey === reputationPublicKey);
|
const toActor = this.scene?.findActor((actor) => actor.reputationPublicKey === reputationPublicKey);
|
||||||
this.actions.reward.log(this, toActor, `(${displayNumber(reward)})`);
|
this.actions.reward.log(this, toActor, `(${displayNumber(reward)} type ${reputationTypeId})`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (votePasses) {
|
if (outcome === true) {
|
||||||
// Distribute awards to author via the forum
|
// Distribute awards to author via the forum
|
||||||
// const tokensForAuthor = this.mintedValue * params.stakeForAuthor + rewards.get(this.tokenId);
|
const tokens = this.reputationTypes.reduce((values, { reputationTypeId }) => {
|
||||||
console.log(`sending reward for author stake to forum: ${this.dao.reputation.valueOf(this.tokenId)}`);
|
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
|
// Transfer ownership of the minted token, from the pool to the forum
|
||||||
this.dao.reputation.transfer(this.id, this.dao.forum.id, this.tokenId);
|
this.dao.reputation.transfer(this.id, this.dao.forum.id, this.tokenAddress);
|
||||||
// const value = this.dao.reputation.valueOf(this.tokenId);
|
// const value = this.dao.reputation.valueOf(this.tokenAddress);
|
||||||
// this.actions.transfer.log(this, this.dao.forum, `(${value})`);
|
// this.actions.transfer.log(this, this.dao.forum, `(${value})`);
|
||||||
|
|
||||||
// Recurse through forum to determine reputation effects
|
const result = {
|
||||||
await this.dao.forum.onValidate({
|
|
||||||
pool: this,
|
pool: this,
|
||||||
postId: this.postId,
|
postId: this.postId,
|
||||||
tokenId: this.tokenId,
|
tokenAddress: this.tokenAddress,
|
||||||
referenceChainLimit: params.referenceChainLimit,
|
referenceChainLimit: params.referenceChainLimit,
|
||||||
leachingValue: params.leachingValue,
|
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');
|
console.log('pool complete');
|
||||||
|
|
|
@ -89,6 +89,9 @@ export class Actor {
|
||||||
async computeDisplayValues(cb) {
|
async computeDisplayValues(cb) {
|
||||||
for (const [label, fn] of this.valueFunctions.entries()) {
|
for (const [label, fn] of this.valueFunctions.entries()) {
|
||||||
const value = fn();
|
const value = fn();
|
||||||
|
console.log('computeDisplay', {
|
||||||
|
label, value, fn, cb,
|
||||||
|
});
|
||||||
await this.setDisplayValue(label, value);
|
await this.setDisplayValue(label, value);
|
||||||
if (cb) {
|
if (cb) {
|
||||||
cb(label, value);
|
cb(label, value);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { DisplayValue } from './display-value.js';
|
import { DisplayValue } from './display-value.js';
|
||||||
import { randomID } from '../../util/helpers.js';
|
import { randomID } from '../../util/helpers.js';
|
||||||
|
import { Rectangle } from './geometry.js';
|
||||||
|
|
||||||
export class Box {
|
export class Box {
|
||||||
constructor(name, parentEl, options = {}) {
|
constructor(name, parentEl, options = {}) {
|
||||||
|
@ -20,6 +21,7 @@ export class Box {
|
||||||
parentEl.appendChild(this.el);
|
parentEl.appendChild(this.el);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.boxes = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
flex({ center = false } = {}) {
|
flex({ center = false } = {}) {
|
||||||
|
@ -35,11 +37,6 @@ export class Box {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
hidden() {
|
|
||||||
this.addClass('hidden');
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
addClass(className) {
|
addClass(className) {
|
||||||
this.el.classList.add(className);
|
this.el.classList.add(className);
|
||||||
return this;
|
return this;
|
||||||
|
@ -47,6 +44,7 @@ export class Box {
|
||||||
|
|
||||||
addBox(name) {
|
addBox(name) {
|
||||||
const box = new Box(name, this.el);
|
const box = new Box(name, this.el);
|
||||||
|
this.boxes.push(box);
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,4 +61,16 @@ export class Box {
|
||||||
getId() {
|
getId() {
|
||||||
return this.el.id;
|
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() {
|
get lastElement() {
|
||||||
if (!this.elements.length) return null;
|
|
||||||
return this.elements[this.elements.length - 1];
|
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 {
|
export class Button extends FormElement {
|
||||||
constructor(name, form, opts) {
|
constructor(name, form, opts) {
|
||||||
super(name, form, { ...opts, cbEventTypes: ['click'] });
|
super(name, form, { ...opts, cbEventTypes: ['click'] });
|
||||||
|
@ -43,15 +57,18 @@ export class TextField extends FormElement {
|
||||||
constructor(name, form, opts) {
|
constructor(name, form, opts) {
|
||||||
super(name, form, opts);
|
super(name, form, opts);
|
||||||
this.flex({ center: true });
|
this.flex({ center: true });
|
||||||
this.label = document.createElement('label');
|
|
||||||
this.labelDiv = document.createElement('div');
|
// Place label inside a div, for improved styling
|
||||||
this.label.appendChild(this.labelDiv);
|
const labelDiv = document.createElement('div');
|
||||||
this.labelDiv.innerHTML = opts.label || name;
|
labelDiv.innerHTML = opts.label || name;
|
||||||
this.input = document.createElement('input');
|
const label = document.createElement('label');
|
||||||
this.input.disabled = !!opts.disabled;
|
label.appendChild(labelDiv);
|
||||||
this.input.defaultValue = opts.defaultValue || '';
|
const input = document.createElement('input');
|
||||||
this.label.appendChild(this.input);
|
input.disabled = !!opts.disabled;
|
||||||
this.el.appendChild(this.label);
|
input.defaultValue = opts.defaultValue || '';
|
||||||
|
label.appendChild(input);
|
||||||
|
this.el.appendChild(label);
|
||||||
|
this.input = input;
|
||||||
}
|
}
|
||||||
|
|
||||||
get value() {
|
get value() {
|
||||||
|
@ -61,6 +78,30 @@ export class TextField extends FormElement {
|
||||||
|
|
||||||
export class TextArea 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 {
|
export class SubFormArray extends FormElement {
|
||||||
constructor(name, form, opts) {
|
constructor(name, form, opts) {
|
||||||
super(name, form, opts);
|
super(name, form, opts);
|
||||||
|
@ -76,36 +117,30 @@ export class SubFormArray extends FormElement {
|
||||||
this.subForms.splice(idx, 1);
|
this.subForms.splice(idx, 1);
|
||||||
subForm.el.remove();
|
subForm.el.remove();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export class SubForm extends FormElement {
|
subForm(opts = {}) {
|
||||||
constructor(name, form, opts) {
|
const subForm = new SubForm(opts.name, this.form, { ...opts, subFormArray: this });
|
||||||
const parentEl = opts.subFormArray ? opts.subFormArray.el : form.el;
|
this.subForms.push(subForm);
|
||||||
const subForm = form.document.form({ name, parentEl, tagName: 'div' }).lastElement;
|
return this;
|
||||||
super(name, form, { ...opts, parentEl });
|
|
||||||
this.subForm = subForm;
|
|
||||||
if (opts.subFormArray) {
|
|
||||||
opts.subFormArray.subForms.push(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get value() {
|
get lastSubForm() {
|
||||||
return this.subForm.value;
|
return this.subForms[this.subForms.length - 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FileInput extends FormElement {
|
export class FileInput extends FormElement {
|
||||||
constructor(name, form, opts) {
|
constructor(name, form, opts) {
|
||||||
super(name, form, opts);
|
super(name, form, opts);
|
||||||
this.input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
this.input.type = 'file';
|
input.type = 'file';
|
||||||
this.input.accept = 'application/json';
|
input.accept = 'application/json';
|
||||||
this.input.classList.add('visually-hidden');
|
input.classList.add('visually-hidden');
|
||||||
this.label = document.createElement('label');
|
const label = document.createElement('label');
|
||||||
this.button = form.button({ name, cb: () => this.input.click() }).lastItem;
|
const button = form.button({ name, cb: () => input.click() }).lastItem;
|
||||||
this.label.appendChild(this.button.el);
|
label.appendChild(button.el);
|
||||||
this.label.appendChild(this.input);
|
label.appendChild(input);
|
||||||
this.el.appendChild(this.label);
|
this.el.appendChild(label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,6 +154,11 @@ export class Form extends Box {
|
||||||
this.el.onsubmit = () => false;
|
this.el.onsubmit = () => false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select(opts) {
|
||||||
|
this.items.push(new Select(opts.name, this, opts));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
button(opts) {
|
button(opts) {
|
||||||
this.items.push(new Button(opts.name, this, opts));
|
this.items.push(new Button(opts.name, this, opts));
|
||||||
return this;
|
return this;
|
||||||
|
@ -154,6 +194,10 @@ export class Form extends Box {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remark(text, opts) {
|
||||||
|
this.document.remark(text, { ...opts, parentEl: this.el });
|
||||||
|
}
|
||||||
|
|
||||||
get lastItem() {
|
get lastItem() {
|
||||||
return this.items[this.items.length - 1];
|
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) {
|
constructor(name, rootBox) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.box = rootBox.addBox(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.box.addBox('Spacer').setInnerHTML(' ');
|
||||||
this.topSection = this.box.addBox('Top section').flex();
|
this.topSection = this.box.addBox('Top section').flex();
|
||||||
this.displayValuesBox = this.topSection.addBox('Values');
|
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 { randomID } from '../../util/helpers.js';
|
||||||
import { EPSILON } from '../../util/constants.js';
|
import { EPSILON } from '../../util/constants.js';
|
||||||
|
|
||||||
class Lock {
|
class Lock {
|
||||||
constructor(tokenId, amount, duration) {
|
constructor(tokenAddress, tokenTypeId, amount, duration) {
|
||||||
this.dateCreated = new Date();
|
this.dateCreated = new Date();
|
||||||
this.tokenId = tokenId;
|
this.tokenAddress = tokenAddress;
|
||||||
this.amount = amount;
|
this.amount = amount;
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
|
this.tokenTypeId = tokenTypeId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ReputationTokenContract extends ERC721 {
|
export class ReputationTokenContract extends ERC1155 {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('Reputation', 'REP');
|
super('Reputation', 'REP');
|
||||||
this.histories = new Map(); // token id --> {increment, context (i.e. validation pool id)}
|
this.histories = new Map(); // token address --> {tokenTypeId, increment, context (i.e. validation pool id)}
|
||||||
this.values = new Map(); // token id --> current value
|
this.values = new Map(); // token address --> token type id --> current value
|
||||||
this.locks = new Set(); // {tokenId, amount, start, duration}
|
this.locks = new Set(); // {tokenAddress, tokenTypeId, amount, start, duration}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param to
|
* @param to Recipient address
|
||||||
* @param value
|
* @param values Object with reputation type id as key, and amount of reputation as value
|
||||||
* @param context
|
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
mint(to, value, context = {}) {
|
mintBatch(to, tokenTypeIds, values) {
|
||||||
const tokenId = `token_${randomID()}`;
|
const tokenAddress = `token_${randomID()}`;
|
||||||
super.mint(to, tokenId);
|
super.mintBatch(to, tokenAddress, tokenTypeIds, tokenTypeIds.map(() => 1));
|
||||||
this.values.set(tokenId, value);
|
const tokenMap = new Map();
|
||||||
this.histories.set(tokenId, [{ increment: value, context }]);
|
for (let idx = 0; idx < tokenTypeIds.length; idx++) {
|
||||||
return tokenId;
|
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) {
|
incrementValue(tokenAddress, tokenTypeId, increment, context) {
|
||||||
const value = this.values.get(tokenId);
|
const tokenTypeIds = this.values.get(tokenAddress);
|
||||||
if (value === undefined) {
|
if (tokenTypeIds === undefined) {
|
||||||
throw new Error(`Token not found: ${tokenId}`);
|
throw new Error(`Token not found: ${tokenAddress}`);
|
||||||
}
|
}
|
||||||
|
const value = tokenTypeIds?.get(tokenTypeId);
|
||||||
const newValue = value + increment;
|
const newValue = value + increment;
|
||||||
const history = this.histories.get(tokenId) || [];
|
const history = this.histories.get(tokenAddress) || [];
|
||||||
|
|
||||||
if (newValue < -EPSILON) {
|
if (newValue < -EPSILON) {
|
||||||
throw new Error(`Token value can not become negative. Attempted to set value = ${newValue}`);
|
throw new Error(`Token value can not become negative. Attempted to set value = ${newValue}`);
|
||||||
}
|
}
|
||||||
this.values.set(tokenId, newValue);
|
tokenTypeIds.set(tokenAddress, newValue);
|
||||||
history.push({ increment, context });
|
this.values.set(tokenAddress, tokenTypeIds);
|
||||||
this.histories.set(tokenId, history);
|
history.push({ tokenTypeId, increment, context });
|
||||||
|
this.histories.set(tokenAddress, history);
|
||||||
}
|
}
|
||||||
|
|
||||||
transferValueFrom(fromTokenId, toTokenId, amount) {
|
transferValueFrom(from, to, tokenTypeId, amount) {
|
||||||
if (amount === undefined) {
|
if (amount === undefined) {
|
||||||
throw new Error('Transfer value: amount is undefined!');
|
throw new Error('Transfer value: amount is undefined!');
|
||||||
}
|
}
|
||||||
|
@ -60,64 +68,83 @@ export class ReputationTokenContract extends ERC721 {
|
||||||
if (amount < 0) {
|
if (amount < 0) {
|
||||||
throw new Error('Transfer value: amount must be positive');
|
throw new Error('Transfer value: amount must be positive');
|
||||||
}
|
}
|
||||||
const sourceAvailable = this.availableValueOf(fromTokenId);
|
const sourceAvailable = this.availableValueOf(from, tokenTypeId);
|
||||||
if (sourceAvailable < amount - EPSILON) {
|
if (sourceAvailable < amount - EPSILON) {
|
||||||
throw new Error('Token value transfer: source has insufficient available value. '
|
throw new Error('Token value transfer: source has insufficient available value. '
|
||||||
+ `Needs ${amount}; has ${sourceAvailable}.`);
|
+ `Needs ${amount}; has ${sourceAvailable}.`);
|
||||||
}
|
}
|
||||||
this.incrementValue(fromTokenId, -amount);
|
this.incrementValue(from, tokenTypeId, -amount);
|
||||||
this.incrementValue(toTokenId, amount);
|
this.incrementValue(to, tokenTypeId, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock(tokenId, amount, duration) {
|
batchTransferValueFrom(from, to, tokenTypeIds, amounts) {
|
||||||
const lock = new Lock(tokenId, amount, duration);
|
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);
|
this.locks.add(lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
historyOf(tokenId) {
|
historyOf(tokenAddress) {
|
||||||
return this.histories.get(tokenId);
|
return this.histories.get(tokenAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
valueOf(tokenId) {
|
valueOf(tokenAddress, tokenTypeId) {
|
||||||
const value = this.values.get(tokenId);
|
const tokenTypeIds = this.values.get(tokenAddress);
|
||||||
if (value === undefined) {
|
if (tokenTypeIds === undefined) {
|
||||||
throw new Error(`Token not found: ${tokenId}`);
|
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())
|
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)
|
.filter(({ dateCreated, duration }) => new Date() - dateCreated < duration)
|
||||||
.reduce((total, { amount }) => total += amount, 0);
|
.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())
|
return Array.from(this.owners.entries())
|
||||||
.filter(([__, owner]) => owner === ownerId)
|
.filter(([__, owner]) => owner === ownerAddress)
|
||||||
.map(([tokenId, __]) => this.valueOf(tokenId))
|
.map(([tokenAddress, __]) => this.valueOf(tokenAddress, tokenTypeId))
|
||||||
.reduce((total, value) => total += value, 0);
|
.reduce((total, value) => total += value, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
availableValueOwnedBy(ownerId) {
|
availableValueOwnedBy(ownerAddress, tokenTypeId) {
|
||||||
return Array.from(this.owners.entries())
|
return Array.from(this.owners.entries())
|
||||||
.filter(([__, owner]) => owner === ownerId)
|
.filter(([__, owner]) => owner === ownerAddress)
|
||||||
.map(([tokenId, __]) => this.availableValueOf(tokenId))
|
.map(([tokenAddress, __]) => this.availableValueOf(tokenAddress, tokenTypeId))
|
||||||
.reduce((total, value) => total += value, 0);
|
.reduce((total, value) => total += value, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTotal() {
|
getTotal(tokenTypeId) {
|
||||||
return Array.from(this.values.values()).reduce((total, value) => total += value, 0);
|
return Array.from(this.values.values())
|
||||||
|
.flatMap((tokens) => tokens.get(tokenTypeId))
|
||||||
|
.reduce((total, value) => total += value, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTotalAvailable() {
|
// burn(tokenAddress, tokenTypeId, )
|
||||||
const amountLocked = Array.from(this.locks.values())
|
|
||||||
.filter(({ dateCreated, duration }) => new Date() - dateCreated < duration)
|
|
||||||
.reduce((total, { amount }) => total += amount, 0);
|
|
||||||
|
|
||||||
return this.getTotal() - amountLocked;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,12 @@ export class Edge {
|
||||||
|
|
||||||
static prepareEditorDocument(graph, doc, from, to) {
|
static prepareEditorDocument(graph, doc, from, to) {
|
||||||
const form = doc.form({ name: 'editorForm' }).lastElement;
|
const form = doc.form({ name: 'editorForm' }).lastElement;
|
||||||
|
form.button({
|
||||||
|
name: 'New Vertex',
|
||||||
|
cb: () => {
|
||||||
|
graph.resetEditor();
|
||||||
|
},
|
||||||
|
});
|
||||||
doc.remark('<h3>Edit Edge</h3>', { parentEl: form.el });
|
doc.remark('<h3>Edit Edge</h3>', { parentEl: form.el });
|
||||||
form
|
form
|
||||||
.textField({
|
.textField({
|
||||||
|
@ -88,7 +94,7 @@ export class Edge {
|
||||||
const subFormArray = form.subFormArray({ id: 'edges', name: 'edges' }).lastItem;
|
const subFormArray = form.subFormArray({ id: 'edges', name: 'edges' }).lastItem;
|
||||||
|
|
||||||
const addEdgeForm = (edge) => {
|
const addEdgeForm = (edge) => {
|
||||||
const { subForm } = form.subForm({ name: 'subform', subFormArray }).lastItem;
|
const { subForm } = form.subForm({ subFormArray }).lastItem;
|
||||||
subForm.textField({
|
subForm.textField({
|
||||||
id: 'type', name: 'type', defaultValue: edge.type, required: true,
|
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.balances = new Map(); // owner address --> token count
|
||||||
this.owners = new Map(); // token id --> owner address
|
this.owners = new Map(); // token id --> owner address
|
||||||
// this.tokenApprovals = new Map(); // token id --> approved addresses
|
// this.tokenApprovals = new Map(); // token id --> approved addresses
|
||||||
// this.operatorApprovals = new Map(); // owner --> operator approvals
|
// this.operatorApprovals = new Map(); // ownerAddress --> operator approvals
|
||||||
|
|
||||||
this.events = {
|
this.events = {
|
||||||
// Transfer: (_from, _to, _tokenId) => {},
|
// Transfer: (_from, _to, _tokenAddress) => {},
|
||||||
// Approval: (_owner, _approved, _tokenId) => {},
|
// Approval: (_owner, _approved, _tokenAddress) => {},
|
||||||
// ApprovalForAll: (_owner, _operator, _approved) => {},
|
// ApprovalForAll: (_owner, _operator, _approved) => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
incrementBalance(owner, increment) {
|
incrementBalance(ownerAddress, increment) {
|
||||||
const balance = this.balances.get(owner) ?? 0;
|
const balance = this.balances.get(ownerAddress) ?? 0;
|
||||||
this.balances.set(owner, balance + increment);
|
this.balances.set(ownerAddress, balance + increment);
|
||||||
}
|
}
|
||||||
|
|
||||||
mint(to, tokenId) {
|
mint(to, tokenAddress) {
|
||||||
console.log('ERC721.mint', { to, tokenId });
|
console.log('ERC721.mint', { to, tokenAddress });
|
||||||
if (this.owners.get(tokenId)) {
|
if (this.owners.get(tokenAddress)) {
|
||||||
throw new Error('ERC721: token already minted');
|
throw new Error('ERC721: token already minted');
|
||||||
}
|
}
|
||||||
this.incrementBalance(to, 1);
|
this.incrementBalance(to, 1);
|
||||||
this.owners.set(tokenId, to);
|
this.owners.set(tokenAddress, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
burn(tokenId) {
|
burn(tokenAddress) {
|
||||||
const owner = this.owners.get(tokenId);
|
const ownerAddress = this.owners.get(tokenAddress);
|
||||||
this.incrementBalance(owner, -1);
|
this.incrementBalance(ownerAddress, -1);
|
||||||
this.owners.delete(tokenId);
|
this.owners.delete(tokenAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
balanceOf(owner) {
|
balanceOf(ownerAddress) {
|
||||||
if (!owner) {
|
if (!ownerAddress) {
|
||||||
throw new Error('ERC721: address zero is not a valid owner');
|
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) {
|
ownerOf(tokenAddress) {
|
||||||
const owner = this.owners.get(tokenId);
|
const ownerAddress = this.owners.get(tokenAddress);
|
||||||
if (!owner) {
|
if (!ownerAddress) {
|
||||||
throw new Error(`ERC721: invalid token ID: ${tokenId}`);
|
throw new Error(`ERC721: invalid token ID: ${tokenAddress}`);
|
||||||
}
|
}
|
||||||
return owner;
|
return ownerAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
transfer(from, to, tokenId) {
|
transfer(from, to, tokenAddress) {
|
||||||
console.log('ERC721.transfer', { from, to, tokenId });
|
console.log('ERC721.transfer', { from, to, tokenAddress });
|
||||||
const owner = this.owners.get(tokenId);
|
const ownerAddress = this.owners.get(tokenAddress);
|
||||||
if (owner !== from) {
|
if (ownerAddress !== from) {
|
||||||
throw new Error(`ERC721: transfer from incorrect owner ${from}; should be ${owner}`);
|
throw new Error(`ERC721: transfer from incorrect owner ${from}; should be ${ownerAddress}`);
|
||||||
}
|
}
|
||||||
this.incrementBalance(from, -1);
|
this.incrementBalance(from, -1);
|
||||||
this.incrementBalance(to, 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
|
/// @notice Enable or disable approval for a third party ("operator") to manage
|
||||||
/// all of `msg.sender`'s assets
|
/// all of `msg.sender`'s assets
|
||||||
/// @dev Emits the ApprovalForAll event. The contract MUST allow
|
/// @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 _operator Address to add to the set of authorized operators
|
||||||
/// @param _approved True if the operator is approved, false to revoke approval
|
/// @param _approved True if the operator is approved, false to revoke approval
|
||||||
// setApprovalForAll(_operator, _approved) {}
|
// setApprovalForAll(_operator, _approved) {}
|
||||||
|
|
||||||
/// @notice Get the approved address for a single NFT
|
/// @notice Get the approved address for a single NFT
|
||||||
/// @dev Throws if `_tokenId` is not a valid NFT.
|
/// @dev Throws if `_tokenAddress` is not a valid NFT.
|
||||||
/// @param _tokenId The NFT to find the approved address for
|
/// @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
|
/// @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
|
/// @notice Query if an address is an authorized operator for another address
|
||||||
/// @param _owner The address that owns the NFTs
|
/// @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
|
/// @return True if `_operator` is an approved operator for `_owner`, false otherwise
|
||||||
// isApprovedForAll(_owner, _operator) {}
|
// 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 {
|
export class Stake {
|
||||||
constructor({
|
constructor({
|
||||||
tokenId, position, amount, lockingTime,
|
tokenAddress, tokenTypeId, position, amount, lockingTime,
|
||||||
}) {
|
}) {
|
||||||
this.tokenId = tokenId;
|
this.tokenAddress = tokenAddress;
|
||||||
this.position = position;
|
this.position = position;
|
||||||
this.amount = amount;
|
this.amount = amount;
|
||||||
this.lockingTime = lockingTime;
|
this.lockingTime = lockingTime;
|
||||||
|
this.tokenTypeId = tokenTypeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
getStakeValue({ lockingTimeExponent } = {}) {
|
getAmount({ lockingTimeExponent } = {}) {
|
||||||
return this.amount * this.lockingTime ** lockingTimeExponent;
|
return this.amount * this.lockingTime ** lockingTimeExponent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ export class Vertex {
|
||||||
to: [],
|
to: [],
|
||||||
};
|
};
|
||||||
this.installedClickCallback = false;
|
this.installedClickCallback = false;
|
||||||
this.properties = new Map();
|
this.properties = options.properties ?? new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
|
@ -51,14 +51,18 @@ export class Vertex {
|
||||||
}
|
}
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
html += `${this.label}`;
|
if (this.type) {
|
||||||
|
html += `<span class='small'>${this.type}</span>`;
|
||||||
|
}
|
||||||
|
html += `${this.label || this.id}`;
|
||||||
html += '<table>';
|
html += '<table>';
|
||||||
|
console.log('displayVertex', { properties: this.properties });
|
||||||
for (const [key, value] of this.properties.entries()) {
|
for (const [key, value] of this.properties.entries()) {
|
||||||
const displayValue = typeof value === 'number' ? displayNumber(value) : value;
|
const displayValue = typeof value === 'number' ? displayNumber(value) : value;
|
||||||
html += `<tr><td>${key}</td><td>${displayValue}</td></tr>`;
|
html += `<tr><td>${key}</td><td>${displayValue}</td></tr>`;
|
||||||
}
|
}
|
||||||
html += '</table>';
|
html += '</table>';
|
||||||
if (this.id !== this.label) {
|
if (this.label && this.id !== this.label) {
|
||||||
html += `<span class=small>${this.id}</span><br>`;
|
html += `<span class=small>${this.id}</span><br>`;
|
||||||
}
|
}
|
||||||
html = html.replaceAll(/\n\s*/g, '');
|
html = html.replaceAll(/\n\s*/g, '');
|
||||||
|
@ -73,26 +77,34 @@ export class Vertex {
|
||||||
static prepareEditorDocument(graph, doc, vertexId) {
|
static prepareEditorDocument(graph, doc, vertexId) {
|
||||||
const vertex = vertexId ? graph.getVertex(vertexId) : undefined;
|
const vertex = vertexId ? graph.getVertex(vertexId) : undefined;
|
||||||
const form = doc.form().lastElement;
|
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 });
|
doc.remark(`<h3>${vertex ? 'Edit' : 'Add'} Vertex</h3>`, { parentEl: form.el });
|
||||||
form
|
form
|
||||||
.textField({
|
.textField({
|
||||||
id: 'id', name: 'id', defaultValue: vertex?.id,
|
name: 'id', defaultValue: vertex?.id,
|
||||||
})
|
})
|
||||||
.textField({ id: 'type', name: 'type', defaultValue: vertex?.type })
|
.textField({ name: 'type', defaultValue: vertex?.type })
|
||||||
.textField({ id: 'label', name: 'label', defaultValue: vertex?.label });
|
.textField({ name: 'label', defaultValue: vertex?.label });
|
||||||
|
|
||||||
doc.remark('<h4>Properties</h4>', { parentEl: form.el });
|
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 addPropertyForm = (key, value) => {
|
||||||
const { subForm } = form.subForm({ name: 'subform', subFormArray }).lastItem;
|
const { subForm } = form.subForm({ subFormArray }).lastItem;
|
||||||
subForm.textField({ id: 'key', name: 'key', defaultValue: key })
|
subForm.textField({ name: 'key', defaultValue: key })
|
||||||
.textField({ id: 'value', name: 'value', defaultValue: value })
|
.textField({ name: 'value', defaultValue: value })
|
||||||
.button({
|
.button({
|
||||||
id: 'remove',
|
|
||||||
name: 'Remove Property',
|
name: 'Remove Property',
|
||||||
cb: () => subFormArray.remove(subForm),
|
cb: () => subFormArray.remove(subForm),
|
||||||
});
|
})
|
||||||
doc.remark('<br>', { parentEl: subForm.el });
|
.remark('<br>');
|
||||||
};
|
};
|
||||||
|
|
||||||
if (vertex) {
|
if (vertex) {
|
||||||
|
@ -102,13 +114,11 @@ export class Vertex {
|
||||||
}
|
}
|
||||||
|
|
||||||
form.button({
|
form.button({
|
||||||
id: 'add',
|
|
||||||
name: 'Add Property',
|
name: 'Add Property',
|
||||||
cb: () => addPropertyForm('', ''),
|
cb: () => addPropertyForm('', ''),
|
||||||
});
|
});
|
||||||
|
|
||||||
form.submit({
|
form.submit({
|
||||||
id: 'save',
|
|
||||||
name: 'Save',
|
name: 'Save',
|
||||||
cb: ({ form: { value: formValue } }) => {
|
cb: ({ form: { value: formValue } }) => {
|
||||||
let fullRedraw = false;
|
let fullRedraw = false;
|
||||||
|
@ -121,7 +131,10 @@ export class Vertex {
|
||||||
Object.assign(vertex, formValue);
|
Object.assign(vertex, formValue);
|
||||||
vertex.displayVertex();
|
vertex.displayVertex();
|
||||||
} else {
|
} 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);
|
Object.assign(newVertex, formValue);
|
||||||
doc.clear();
|
doc.clear();
|
||||||
Vertex.prepareEditorDocument(graph, doc, newVertex.id);
|
Vertex.prepareEditorDocument(graph, doc, newVertex.id);
|
||||||
|
@ -134,7 +147,6 @@ export class Vertex {
|
||||||
|
|
||||||
if (vertex) {
|
if (vertex) {
|
||||||
form.button({
|
form.button({
|
||||||
id: 'delete',
|
|
||||||
name: 'Delete Vertex',
|
name: 'Delete Vertex',
|
||||||
cb: () => {
|
cb: () => {
|
||||||
graph.deleteVertex(vertex.id);
|
graph.deleteVertex(vertex.id);
|
||||||
|
@ -159,7 +171,6 @@ export class Vertex {
|
||||||
}
|
}
|
||||||
|
|
||||||
form.button({
|
form.button({
|
||||||
id: 'cancel',
|
|
||||||
name: 'Cancel',
|
name: 'Cancel',
|
||||||
cb: () => graph.resetEditor(),
|
cb: () => graph.resetEditor(),
|
||||||
parentEl: doc.el,
|
parentEl: doc.el,
|
||||||
|
|
|
@ -131,9 +131,10 @@ export class WeightedDirectedGraph {
|
||||||
const form = this.controlDoc.form({ name: 'controlForm' }).lastElement;
|
const form = this.controlDoc.form({ name: 'controlForm' }).lastElement;
|
||||||
const { subForm: graphPropertiesForm } = form.subForm({ name: 'graphPropsForm' }).lastItem;
|
const { subForm: graphPropertiesForm } = form.subForm({ name: 'graphPropsForm' }).lastItem;
|
||||||
graphPropertiesForm.flex()
|
graphPropertiesForm.flex()
|
||||||
.textField({ name: 'name', label: 'Graph name', defaultValue: this.name })
|
.textField({
|
||||||
.submit({
|
name: 'name',
|
||||||
name: 'Save',
|
label: 'Graph name',
|
||||||
|
defaultValue: this.name,
|
||||||
cb: (({ form: { value: { name } } }) => {
|
cb: (({ form: { value: { name } } }) => {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -50,6 +50,12 @@ a:visited {
|
||||||
left: 12em;
|
left: 12em;
|
||||||
top: -0.5em;
|
top: -0.5em;
|
||||||
}
|
}
|
||||||
|
.fixed {
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
.absolute {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
svg {
|
svg {
|
||||||
width: 800px;
|
width: 800px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,18 +15,19 @@
|
||||||
For more information please see the <a href="https://daogovernanceframework.com/wiki/DAO_Governance_Framework">DGF
|
For more information please see the <a href="https://daogovernanceframework.com/wiki/DAO_Governance_Framework">DGF
|
||||||
Wiki</a>.
|
Wiki</a>.
|
||||||
</p>
|
</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>
|
<h2>Tools</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="./wdg-editor">Weighted Directed Graph Editor</a></li>
|
<li><a href="./wdg-editor">Weighted Directed Graph Editor</a></li>
|
||||||
|
<li><a href="./schema-editor">Schema Editor</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Example Scenarios</h2>
|
<h2>Example Scenarios</h2>
|
||||||
<p>
|
<p>
|
||||||
Below are example scenarios with various assertions covering features of our reputation system.
|
Below are example scenarios with various assertions covering features of our reputation system.
|
||||||
</p>
|
</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>
|
<ul>
|
||||||
<li><a href="./tests/validation-pool.test.html">Validation Pool</a></li>
|
<li><a href="./tests/validation-pool.test.html">Validation Pool</a></li>
|
||||||
<li><a href="./tests/availability.test.html">Availability + Business</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/mocha.test.html">Mocha</a></li>
|
||||||
<li><a href="./tests/input.test.html">Input</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/document.test.html">Document</a></li>
|
||||||
|
<li><a href="./tests/force-directed.test.html">Force-Directed Graph</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
<h4><a href="./tests/all.test.html">All</a></h4>
|
<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/forum/forum11.test.js"></script>
|
||||||
<script type="module" src="./scripts/input.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/document.test.js"></script>
|
||||||
|
<script type="module" src="./scripts/force-directed.test.js"></script>
|
||||||
<script defer class="mocha-init">
|
<script defer class="mocha-init">
|
||||||
mocha.setup({
|
mocha.setup({
|
||||||
ui: 'bdd',
|
ui: 'bdd',
|
||||||
|
|
|
@ -27,6 +27,6 @@
|
||||||
<script defer class="mocha-init">
|
<script defer class="mocha-init">
|
||||||
mocha.setup({
|
mocha.setup({
|
||||||
ui: 'bdd',
|
ui: 'bdd',
|
||||||
});
|
});
|
||||||
window.should = chai.should();
|
window.should = chai.should();
|
||||||
</script>
|
</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 { PostContent } from '../../classes/supporting/post-content.js';
|
||||||
import { delayOrWait } from '../../classes/display/scene-controls.js';
|
import { delayOrWait } from '../../classes/display/scene-controls.js';
|
||||||
import { mochaRun } from '../../util/helpers.js';
|
import { mochaRun } from '../../util/helpers.js';
|
||||||
|
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
|
||||||
|
|
||||||
const DELAY_INTERVAL = 100;
|
const DELAY_INTERVAL = 100;
|
||||||
const POOL_DURATION = 200;
|
const POOL_DURATION = 200;
|
||||||
|
@ -32,7 +33,7 @@ const setup = async () => {
|
||||||
scene.withTable();
|
scene.withTable();
|
||||||
|
|
||||||
dao = new DAO('DGF', scene);
|
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 = [];
|
experts = [];
|
||||||
|
|
||||||
|
@ -58,7 +59,7 @@ const setup = async () => {
|
||||||
await pool1.evaluateWinningConditions();
|
await pool1.evaluateWinningConditions();
|
||||||
await delayOrWait(DELAY_INTERVAL);
|
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(
|
const { pool: pool2 } = await experts[1].submitPostWithFee(
|
||||||
new PostContent({ hello: 'to you as well' })
|
new PostContent({ hello: 'to you as well' })
|
||||||
|
@ -77,8 +78,8 @@ const setup = async () => {
|
||||||
await pool2.evaluateWinningConditions();
|
await pool2.evaluateWinningConditions();
|
||||||
await delayOrWait(DELAY_INTERVAL);
|
await delayOrWait(DELAY_INTERVAL);
|
||||||
|
|
||||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(15);
|
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(15);
|
||||||
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(5);
|
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getActiveWorker = async () => {
|
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 { PostContent } from '../../classes/supporting/post-content.js';
|
||||||
import { DAO } from '../../classes/dao/dao.js';
|
import { DAO } from '../../classes/dao/dao.js';
|
||||||
import { delayOrWait } from '../../classes/display/scene-controls.js';
|
import { delayOrWait } from '../../classes/display/scene-controls.js';
|
||||||
|
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
|
||||||
|
|
||||||
export class ForumTest {
|
export class ForumTest {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
|
@ -86,6 +87,6 @@ export class ForumTest {
|
||||||
|
|
||||||
await this.newExpert();
|
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 { mochaRun } from '../../../util/helpers.js';
|
||||||
import { ForumTest } from '../forum.test-util.js';
|
import { ForumTest } from '../forum.test-util.js';
|
||||||
|
|
||||||
|
@ -34,9 +35,9 @@ describe('Forum', function tests() {
|
||||||
await forumTest.addPost(authors, 10);
|
await forumTest.addPost(authors, 10);
|
||||||
forum.getPost(posts[0]).value.should.equal(10);
|
forum.getPost(posts[0]).value.should.equal(10);
|
||||||
|
|
||||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(5);
|
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
|
||||||
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(2.5);
|
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(2.5);
|
||||||
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey).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 { mochaRun } from '../../../util/helpers.js';
|
||||||
import { ForumTest } from '../forum.test-util.js';
|
import { ForumTest } from '../forum.test-util.js';
|
||||||
|
|
||||||
|
@ -33,9 +34,9 @@ describe('Forum', function tests() {
|
||||||
await forumTest.addPost(authors, 10);
|
await forumTest.addPost(authors, 10);
|
||||||
forum.getPost(posts[0]).value.should.equal(10);
|
forum.getPost(posts[0]).value.should.equal(10);
|
||||||
|
|
||||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(5);
|
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
|
||||||
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(5);
|
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
|
||||||
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey).should.equal(0);
|
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Post2 with two authors, one shared with Post1', async () => {
|
it('Post2 with two authors, one shared with Post1', async () => {
|
||||||
|
@ -46,9 +47,9 @@ describe('Forum', function tests() {
|
||||||
await forumTest.addPost(authors, 10);
|
await forumTest.addPost(authors, 10);
|
||||||
forum.getPost(posts[0]).value.should.equal(10);
|
forum.getPost(posts[0]).value.should.equal(10);
|
||||||
|
|
||||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(5);
|
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
|
||||||
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(10);
|
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(10);
|
||||||
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey).should.equal(5);
|
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 = {
|
export const EdgeTypes = {
|
||||||
CITATION: 'citation',
|
CITATION: 'citation',
|
||||||
BALANCE: 'balance',
|
BALANCE: 'balance',
|
||||||
AUTHOR: 'author',
|
AUTHOR: 'author',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VertexTypes = {
|
export const VertexTypes = {
|
||||||
POST: 'post',
|
POST: 'post',
|
||||||
AUTHOR: 'author',
|
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