From 435633a893341f897d2f02744d532e848100b180 Mon Sep 17 00:00:00 2001
From: Ladd Hoffman
Date: Wed, 2 Aug 2023 14:54:31 -0500
Subject: [PATCH] WIP
---
notes/reputation-types.md | 17 ++
src/classes/actors/expert.js | 10 +-
src/classes/dao/availability.js | 10 +-
src/classes/dao/business.js | 2 +-
src/classes/dao/dao.js | 15 +-
src/classes/dao/forum.js | 177 ++++++++++---------
src/classes/dao/validation-pool.js | 193 +++++++++++++--------
src/classes/display/actor.js | 3 +
src/classes/display/box.js | 20 ++-
src/classes/display/document.js | 1 -
src/classes/display/force-directed.js | 111 ++++++++++++
src/classes/display/form.js | 104 +++++++----
src/classes/display/geometry.js | 100 +++++++++++
src/classes/display/scene.js | 2 +-
src/classes/reputation/reputation-token.js | 137 +++++++++------
src/classes/supporting/edge.js | 8 +-
src/classes/supporting/erc1155.js | 105 +++++++++++
src/classes/supporting/erc721.js | 66 +++----
src/classes/supporting/schema.js | 123 +++++++++++++
src/classes/supporting/stake.js | 7 +-
src/classes/supporting/vertex.js | 47 +++--
src/classes/supporting/wdg.js | 7 +-
src/index.css | 6 +
src/index.html | 10 +-
src/schema-editor/index.html | 14 ++
src/schema-editor/index.js | 9 +
src/tests/all.test.html | 1 +
src/tests/availability.test.html | 2 +-
src/tests/force-directed.test.html | 28 +++
src/tests/scripts/availability.test.js | 9 +-
src/tests/scripts/force-directed.test.js | 73 ++++++++
src/tests/scripts/forum.test-util.js | 3 +-
src/tests/scripts/forum/forum10.test.js | 7 +-
src/tests/scripts/forum/forum11.test.js | 13 +-
src/util/constants.js | 13 +-
35 files changed, 1106 insertions(+), 347 deletions(-)
create mode 100644 notes/reputation-types.md
create mode 100644 src/classes/display/force-directed.js
create mode 100644 src/classes/display/geometry.js
create mode 100644 src/classes/supporting/erc1155.js
create mode 100644 src/classes/supporting/schema.js
create mode 100644 src/schema-editor/index.html
create mode 100644 src/schema-editor/index.js
create mode 100644 src/tests/force-directed.test.html
create mode 100644 src/tests/scripts/force-directed.test.js
diff --git a/notes/reputation-types.md b/notes/reputation-types.md
new file mode 100644
index 0000000..df283fb
--- /dev/null
+++ b/notes/reputation-types.md
@@ -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.
\ No newline at end of file
diff --git a/src/classes/actors/expert.js b/src/classes/actors/expert.js
index eb297e2..1ea3bf3 100644
--- a/src/classes/actors/expert.js
+++ b/src/classes/actors/expert.js
@@ -25,7 +25,7 @@ export class Expert extends ReputationHolder {
return 0;
}
const authorEdges = authorVertex.getEdges(EdgeTypes.AUTHOR, false);
- const tokenValues = authorEdges.map(({ data: { tokenId } }) => this.dao.reputation.valueOf(tokenId));
+ const tokenValues = authorEdges.map(({ data: { tokenAddress } }) => this.dao.reputation.valueOf(tokenAddress));
return tokenValues.reduce((value, total) => total += value, 0);
}
@@ -42,7 +42,7 @@ export class Expert extends ReputationHolder {
await this.actions.submitPost.log(this, post);
const postId = post.id;
const pool = await this.initiateValidationPool({ fee, postId }, params);
- this.tokens.push(pool.tokenId);
+ this.tokens.push(pool.tokenAddress);
return { postId, pool };
}
@@ -53,7 +53,7 @@ export class Expert extends ReputationHolder {
postId,
fee,
}, params);
- this.tokens.push(pool.tokenId);
+ this.tokens.push(pool.tokenAddress);
return pool;
}
@@ -68,7 +68,7 @@ export class Expert extends ReputationHolder {
`(${position ? 'for' : 'against'}, stake: ${amount})`,
);
return validationPool.stake(this.reputationPublicKey, {
- position, amount, lockingTime, tokenId: this.tokens[0],
+ position, amount, lockingTime, tokenAddress: this.tokens[0],
});
}
@@ -80,7 +80,7 @@ export class Expert extends ReputationHolder {
);
this.workerId = await this.dao.availability.register(this.reputationPublicKey, {
stakeAmount,
- tokenId: this.tokens[0],
+ tokenAddress: this.tokens[0],
duration,
});
}
diff --git a/src/classes/dao/availability.js b/src/classes/dao/availability.js
index 8cdb638..c2d6b06 100644
--- a/src/classes/dao/availability.js
+++ b/src/classes/dao/availability.js
@@ -3,9 +3,9 @@ import { Actor } from '../display/actor.js';
import { CryptoUtil } from '../supporting/crypto.js';
class Worker {
- constructor(reputationPublicKey, tokenId, stakeAmount, duration) {
+ constructor(reputationPublicKey, tokenAddress, stakeAmount, duration) {
this.reputationPublicKey = reputationPublicKey;
- this.tokenId = tokenId;
+ this.tokenAddress = tokenAddress;
this.stakeAmount = stakeAmount;
this.duration = duration;
this.available = true;
@@ -28,11 +28,11 @@ export class Availability extends Actor {
this.workers = new Map();
}
- register(reputationPublicKey, { stakeAmount, tokenId, duration }) {
+ register(reputationPublicKey, { stakeAmount, tokenAddress, duration }) {
// TODO: Should be signed by token owner
- this.dao.reputation.lock(tokenId, stakeAmount, duration);
+ this.dao.reputation.lock(tokenAddress, stakeAmount, duration);
const workerId = CryptoUtil.randomUUID();
- this.workers.set(workerId, new Worker(reputationPublicKey, tokenId, stakeAmount, duration));
+ this.workers.set(workerId, new Worker(reputationPublicKey, tokenAddress, stakeAmount, duration));
return workerId;
}
diff --git a/src/classes/dao/business.js b/src/classes/dao/business.js
index 7dcf53d..a82387d 100644
--- a/src/classes/dao/business.js
+++ b/src/classes/dao/business.js
@@ -87,7 +87,7 @@ export class Business extends Actor {
});
await pool.stake(reputationPublicKey, {
- tokenId: request.worker.tokenId,
+ tokenAddress: request.worker.tokenAddress,
amount: request.worker.stakeAmount,
position: true,
});
diff --git a/src/classes/dao/dao.js b/src/classes/dao/dao.js
index 4e31fa8..1cc2552 100644
--- a/src/classes/dao/dao.js
+++ b/src/classes/dao/dao.js
@@ -5,6 +5,7 @@ import { Availability } from './availability.js';
import { Business } from './business.js';
import { Voter } from '../supporting/voter.js';
import { Actor } from '../display/actor.js';
+import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
/**
* Purpose:
@@ -49,15 +50,13 @@ export class DAO extends Actor {
});
}
- getActiveReputation() {
+ /**
+ * @param {number} param0.reputationTypeId
+ * @returns {number}
+ */
+ getActiveAvailableReputation({ reputationTypeId = DEFAULT_REP_TOKEN_TYPE_ID }) {
return this.listActiveVoters()
- .map(({ reputationPublicKey }) => this.reputation.valueOwnedBy(reputationPublicKey))
- .reduce((acc, cur) => (acc += cur), 0);
- }
-
- getActiveAvailableReputation() {
- return this.listActiveVoters()
- .map(({ reputationPublicKey }) => this.reputation.availableValueOwnedBy(reputationPublicKey))
+ .map(({ reputationPublicKey }) => this.reputation.availableValueOwnedBy(reputationPublicKey, reputationTypeId))
.reduce((acc, cur) => (acc += cur), 0);
}
diff --git a/src/classes/dao/forum.js b/src/classes/dao/forum.js
index a5e3fee..5dbba05 100644
--- a/src/classes/dao/forum.js
+++ b/src/classes/dao/forum.js
@@ -15,8 +15,8 @@ class Post extends Actor {
this.forum = forum;
this.id = postContent.id ?? name;
this.senderId = senderId;
- this.value = 0;
- this.initialValue = 0;
+ this.values = new Map();
+ this.initialValues = new Map();
this.authors = postContent.authors;
this.citations = postContent.citations;
this.title = postContent.title;
@@ -83,16 +83,21 @@ export class Forum extends ReputationHolder {
// getContract(type) { }
async onValidate({
- pool, postId, tokenId, referenceChainLimit, leachingValue,
+ pool, postId, tokenAddress, referenceChainLimit, leachingValue,
}) {
- console.log('onValidate', { pool, postId, tokenId });
- const initialValue = this.dao.reputation.valueOf(tokenId);
+ console.log('onValidate', { pool, postId, tokenAddress });
+
+ // What we have here now is an ERC-1155 rep token, which can contain multiple reputation types.
+ // ERC-1155 supports a batch transfer operation, so it makes sense to leverage that.
+
+ const initialValues = pool.reputationTypeIds
+ .map((tokenTypeId) => this.dao.reputation.valueOf(tokenAddress, tokenTypeId));
const postVertex = this.graph.getVertex(postId);
const post = postVertex.data;
post.setStatus('Validated');
- post.initialValue = initialValue;
+ post.initialValues = initialValues;
- const addAuthorToGraph = (publicKey, weight, authorTokenId) => {
+ const addAuthorToGraph = (publicKey, weight, authorTokenAddress) => {
// For graph display purposes, we want to use the existing Expert actors from the current scene.
const author = this.scene.findActor(({ reputationPublicKey }) => reputationPublicKey === publicKey);
author.setDisplayValue('reputation', () => author.getReputation());
@@ -105,7 +110,7 @@ export class Forum extends ReputationHolder {
postVertex,
authorVertex,
weight,
- { tokenId: authorTokenId },
+ { tokenAddress: authorTokenAddress },
{ hide: author.options.hide },
);
};
@@ -114,16 +119,18 @@ export class Forum extends ReputationHolder {
// If no authors are specified, treat the sender as the sole author.
// TODO: Verify that cumulative author weight == 1.
if (!post.authors?.length) {
- addAuthorToGraph(post.senderId, 1, tokenId);
+ addAuthorToGraph(post.senderId, 1, tokenAddress);
} else {
for (const { publicKey, weight } of post.authors) {
// If the sender is also listed among the authors, do not mint them an additional token.
- const authorTokenId = (publicKey === post.senderId) ? tokenId : this.dao.reputation.mint(this.id, 0);
- addAuthorToGraph(publicKey, weight, authorTokenId);
+ const authorTokenAddress = (publicKey === post.senderId)
+ ? tokenAddress
+ : this.dao.reputation.mintBatch(this.id, pool.reputationTypeIds, pool.reputationTypeIds.map(() => 0));
+ addAuthorToGraph(publicKey, weight, authorTokenAddress);
}
// If the sender is not an author, they will end up with the minted token but with zero value.
if (!post.authors.find(({ publicKey }) => publicKey === post.senderId)) {
- addAuthorToGraph(post.senderId, 0, tokenId);
+ addAuthorToGraph(post.senderId, 0, tokenAddress);
}
}
@@ -134,7 +141,7 @@ export class Forum extends ReputationHolder {
{ to: postVertex, from: { data: pool } },
{
rewardsAccumulator,
- increment: initialValue,
+ increments: initialValues,
referenceChainLimit,
leachingValue,
},
@@ -142,16 +149,16 @@ export class Forum extends ReputationHolder {
// Apply computed rewards to update values of tokens
for (const [authorEdge, amount] of rewardsAccumulator) {
- const { to: authorVertex, data: { tokenId: authorTokenId } } = authorEdge;
+ const { to: authorVertex, data: { tokenAddress: authorTokenAddress } } = authorEdge;
const { data: author } = authorVertex;
// The primary author gets the validation pool minted token.
// So we don't need to transfer any reputation to the primary author.
// Their reward will be the remaining balance after all other transfers.
- if (authorTokenId !== tokenId) {
+ if (authorTokenAddress !== tokenAddress) {
if (amount < 0) {
- this.dao.reputation.transferValueFrom(authorTokenId, tokenId, -amount);
+ this.dao.reputation.transferValueFrom(authorTokenAddress, tokenAddress, -amount);
} else {
- this.dao.reputation.transferValueFrom(tokenId, authorTokenId, amount);
+ this.dao.reputation.transferValueFrom(tokenAddress, authorTokenAddress, amount);
}
await author.computeDisplayValues((label, value) => authorVertex.setProperty(label, value));
authorVertex.displayVertex();
@@ -167,8 +174,8 @@ export class Forum extends ReputationHolder {
for (const authorEdge of postVertex.getEdges(EdgeTypes.AUTHOR, true)) {
const authorVertex = authorEdge.to;
const author = authorVertex.data;
- const { tokenId: authorTokenId } = authorEdge.data;
- this.dao.reputation.transfer(this.id, author.reputationPublicKey, authorTokenId);
+ const { tokenAddress: authorTokenAddress } = authorEdge.data;
+ this.dao.reputation.transfer(this.id, author.reputationPublicKey, authorTokenAddress);
}
}
@@ -178,7 +185,7 @@ export class Forum extends ReputationHolder {
*/
async propagateValue(edge, {
rewardsAccumulator,
- increment,
+ increments,
depth = 0,
initialNegative = false,
referenceChainLimit,
@@ -186,7 +193,8 @@ export class Forum extends ReputationHolder {
}) {
const postVertex = edge.to;
const post = postVertex.data;
- this.actions.propagate.log(edge.from.data, post, `(${increment})`);
+ const incrementsStr = `(${increments.join(')(')})`;
+ this.actions.propagate.log(edge.from.data, post, incrementsStr);
if (!!referenceChainLimit && depth > referenceChainLimit) {
this.actions.propagate.log(
@@ -196,7 +204,7 @@ export class Forum extends ReputationHolder {
null,
'-x',
);
- return increment;
+ return increments;
}
console.log('propagateValue start', {
@@ -204,95 +212,104 @@ export class Forum extends ReputationHolder {
to: edge.to.id,
depth,
value: post.value,
- increment,
+ increments,
initialNegative,
});
const propagate = async (positive) => {
- let totalOutboundAmount = 0;
+ const totalOutboundAmounts = increments.map(() => 0);
const citationEdges = postVertex.getEdges(EdgeTypes.CITATION, true)
.filter(({ weight }) => (positive ? weight > 0 : weight < 0));
for (const citationEdge of citationEdges) {
const { weight } = citationEdge;
- let outboundAmount = weight * increment;
- if (Math.abs(outboundAmount) > EPSILON) {
- const balanceToOutbound = this.graph.getEdgeWeight(EdgeTypes.BALANCE, citationEdge.from, citationEdge.to)
+ const outboundAmounts = increments.map((increment) => weight * increment);
+ const refundsFromOutbound = increments.map(() => 0);
+ for (let idx = 0; idx < outboundAmounts.length; idx++) {
+ let outboundAmount = outboundAmounts[idx];
+ if (Math.abs(outboundAmount) > EPSILON) {
+ const balanceToOutbound = this.graph.getEdgeWeight(EdgeTypes.BALANCE, citationEdge.from, citationEdge.to)
?? 0;
- let refundFromOutbound = 0;
+ let refundFromOutbound = 0;
- // Special case: Incineration.
- if (citationEdge.to.id === INCINERATOR_ADDRESS) {
+ // Special case: Incineration.
+ if (citationEdge.to.id === INCINERATOR_ADDRESS) {
// Only a positive amount may be incinerated! Otherwise the sink could be used as a source.
- if (outboundAmount < 0) {
- this.scene?.flowchart?.log(`style ${citationEdge.from.id} fill:#620000`);
- this.actions.propagate.log(
- citationEdge.from.data,
- { name: 'Incinerator' },
- `(${increment})`,
- undefined,
- '-x',
- );
- throw new Error('Incinerator can only receive positive citations!');
- }
- // Reputation sent to the incinerator is burned! This means it is deducted from the sender,
- // without increasing the value of any other token.
- this.actions.propagate.log(citationEdge.from.data, { name: 'Incinerator' }, `(${increment})`);
- } else {
+ if (outboundAmount < 0) {
+ this.scene?.flowchart?.log(`style ${citationEdge.from.id} fill:#620000`);
+ this.actions.propagate.log(
+ citationEdge.from.data,
+ { name: 'Incinerator' },
+ incrementsStr,
+ undefined,
+ '-x',
+ );
+ throw new Error('Incinerator can only receive positive citations!');
+ }
+ // Reputation sent to the incinerator is burned! This means it is deducted from the sender,
+ // without increasing the value of any other token.
+ this.actions.propagate.log(citationEdge.from.data, { name: 'Incinerator' }, incrementsStr);
+ } else {
// We need to ensure that we at most undo the prior effects of this post
- if (initialNegative) {
- outboundAmount = outboundAmount < 0
- ? Math.max(outboundAmount, -balanceToOutbound)
- : Math.min(outboundAmount, -balanceToOutbound);
+ if (initialNegative) {
+ outboundAmount = outboundAmount < 0
+ ? Math.max(outboundAmount, -balanceToOutbound)
+ : Math.min(outboundAmount, -balanceToOutbound);
+ }
+
+ // Recursively propagate reputation effects
+ refundFromOutbound = await this.propagateValue(citationEdge, {
+ rewardsAccumulator,
+ increment: outboundAmount,
+ depth: depth + 1,
+ initialNegative: initialNegative || (depth === 0 && outboundAmount < 0),
+ referenceChainLimit,
+ leachingValue,
+ });
+
+ // Any excess (negative) amount that could not be propagated,
+ // i.e. because a cited post has been reduced to zero value,
+ // is retained by the citing post.
+ outboundAmount -= refundFromOutbound;
}
- // Recursively propagate reputation effects
- refundFromOutbound = await this.propagateValue(citationEdge, {
- rewardsAccumulator,
- increment: outboundAmount,
- depth: depth + 1,
- initialNegative: initialNegative || (depth === 0 && outboundAmount < 0),
- referenceChainLimit,
- leachingValue,
- });
-
- // Any excess (negative) amount that could not be propagated,
- // i.e. because a cited post has been reduced to zero value,
- // is retained by the citing post.
- outboundAmount -= refundFromOutbound;
+ // Keep a record of the effect of the reputation transferred along this edge in the graph,
+ // so that later, negative citations can be constrained to at most undo these effects.
+ this.graph.setEdgeWeight(
+ EdgeTypes.BALANCE,
+ citationEdge.from,
+ citationEdge.to,
+ balanceToOutbound + outboundAmount,
+ );
+ refundsFromOutbound[idx] = refundFromOutbound;
+ totalOutboundAmounts[idx] += outboundAmount;
}
-
- // Keep a record of the effect of the reputation transferred along this edge in the graph,
- // so that later, negative citations can be constrained to at most undo these effects.
- this.graph.setEdgeWeight(
- EdgeTypes.BALANCE,
- citationEdge.from,
- citationEdge.to,
- balanceToOutbound + outboundAmount,
- );
- totalOutboundAmount += outboundAmount;
-
+ const refundStr = refundsFromOutbound.map((refund) => displayNumber(refund)).join('/');
this.actions.confirm.log(
citationEdge.to.data,
citationEdge.from.data,
- `(refund: ${displayNumber(refundFromOutbound)}, leach: ${outboundAmount * leachingValue})`,
+ `(refund: ${refundStr}, leach: ${outboundAmount * leachingValue})`,
undefined,
'-->>',
);
}
}
- return totalOutboundAmount;
+ return totalOutboundAmounts;
};
// First, leach value via negative citations
- const totalLeachingAmount = await propagate(false);
- increment -= totalLeachingAmount * leachingValue;
+ const totalLeachingAmounts = await propagate(false);
+ for (let idx = 0; idx < totalLeachingAmounts.length; idx++) {
+ increments[idx] -= totalLeachingAmounts[idx] * leachingValue;
+ }
// Now propagate value via positive citations
const totalDonationAmount = await propagate(true);
- increment -= totalDonationAmount * leachingValue;
+ for (let idx = 0; idx < totalDonationAmounts.length; idx++) {
+ increments[idx] -= totalDonationAmounts[idx] * leachingValue;
+ }
// Apply the remaining increment to the present post
- const rawNewValue = post.value + increment;
+ const rawNewValues = post.value + increment;
const newValue = Math.max(0, rawNewValue);
const appliedIncrement = newValue - post.value;
const refundToInbound = increment - appliedIncrement;
diff --git a/src/classes/dao/validation-pool.js b/src/classes/dao/validation-pool.js
index 520fc83..f14893b 100644
--- a/src/classes/dao/validation-pool.js
+++ b/src/classes/dao/validation-pool.js
@@ -2,6 +2,7 @@ import { ReputationHolder } from '../reputation/reputation-holder.js';
import { Stake } from '../supporting/stake.js';
import { Action } from '../display/action.js';
import { displayNumber } from '../../util/helpers.js';
+import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
const params = {
/* Validation Pool parameters */
@@ -51,6 +52,7 @@ export class ValidationPool extends ReputationHolder {
duration,
tokenLossRatio,
contentiousDebate = false,
+ reputationTypes,
},
name,
scene,
@@ -69,6 +71,16 @@ export class ValidationPool extends ReputationHolder {
this.actions.initiate.log(fromActor, this, `(fee: ${fee})`);
this.activate();
+ // Supporting a simplified use case, if the reputation type is not specified let's use a default
+ this.reputationTypes = reputationTypes ?? [{ reputationTypeId: DEFAULT_REP_TOKEN_TYPE_ID, weight: 1 }];
+ // Normalize so reputation weights sum to 1
+ {
+ const weightTotal = this.reputationTypes.reduce((total, { weight }) => total += weight, 0);
+ for (const reputationType of this.reputationTypes) {
+ reputationType.weight /= weightTotal;
+ }
+ }
+
// If contentiousDebate = true, we will follow the progression defined by getTokenLossRatio()
if (
!contentiousDebate
@@ -130,22 +142,34 @@ export class ValidationPool extends ReputationHolder {
this.duration = duration;
this.tokenLossRatio = tokenLossRatio;
this.contentiousDebate = contentiousDebate;
- this.mintedValue = fee * params.mintingRatio();
- this.tokenId = this.dao.reputation.mint(this.id, this.mintedValue);
- // Tokens minted "for" the post go toward stake of author voting for their own post.
- // Also, author can provide additional stakes, e.g. availability stakes for work evidence post.
- this.stake(this.id, {
- position: true,
- amount: this.mintedValue * params.stakeForAuthor,
- tokenId: this.tokenId,
- });
- this.stake(this.id, {
- position: false,
- amount: this.mintedValue * (1 - params.stakeForAuthor),
- tokenId: this.tokenId,
- });
- this.actions.mint.log(this, this, `(${this.mintedValue})`);
+ const mintTotal = fee * params.mintingRatio();
+ const reputationTypeIds = this.reputationTypes
+ .map(({ reputationTypeId }) => reputationTypeId);
+ const mintValues = this.reputationTypes
+ .map(({ weight }) => mintTotal * weight);
+ console.log('validation pool constructor', { reputationTypeIds, mintValues });
+ this.tokenAddress = this.dao.reputation.mintBatch(this.id, reputationTypeIds, mintValues);
+ this.reputationTypeIds = reputationTypeIds;
+
+ // Minted tokens are staked for/against the post at configured ratio
+ // Each type of reputation is staked in the proportions specified by the `reputationTypes` parameter
+ for (const { reputationTypeId, weight } of this.reputationTypes) {
+ this.stake(this.id, {
+ position: true,
+ amount: mintTotal * params.stakeForAuthor * weight,
+ tokenAddress: this.tokenAddress,
+ reputationTypeId,
+ });
+ this.stake(this.id, {
+ position: false,
+ amount: this.mintedValue * (1 - params.stakeForAuthor) * weight,
+ tokenAddress: this.tokenAddress,
+ reputationTypeId,
+ });
+ }
+
+ this.actions.mint.log(this, this, `(${mintTotal})`);
// Keep a record of voters and their votes
this.dao.addVoteRecord(reputationPublicKey, this);
@@ -174,39 +198,38 @@ export class ValidationPool extends ReputationHolder {
}
/**
- * @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
+ * @param {boolean} options.outcome: null --> all entries. Otherwise filters to position === outcome.
+ * @param {boolean} options.tokenTypeId: null --> all entries. Otherwise filters to the given token type.
* @param {boolean} options.excludeSystem: Whether to exclude votes cast during pool initialization
* @returns stake[]
*/
- getStakes(outcome, { excludeSystem }) {
+ getStakes({ outcome, tokenTypeId, excludeSystem }) {
return Array.from(this.stakes.values())
- .filter(({ tokenId }) => !excludeSystem || tokenId !== this.tokenId)
+ .filter((stake) => tokenTypeId === null || stake.tokenTypeId === tokenTypeId)
+ .filter(({ tokenAddress }) => !excludeSystem || tokenAddress !== this.tokenAddress)
.filter(({ position }) => outcome === null || position === outcome);
}
/**
- * @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
+ * @param {boolean} options.outcome: null --> all entries. Otherwise filters to position === outcome.
* @returns number
*/
- getTotalStakedOnPost(outcome) {
- return this.getStakes(outcome, { excludeSystem: false })
- .map((stake) => stake.getStakeValue({ lockingTimeExponent: params.lockingTimeExponent }))
- .reduce((acc, cur) => (acc += cur), 0);
+ getStakedAmount({ outcome, tokenTypeId }) {
+ return this.getStakes({ outcome, tokenTypeId, excludeSystem: false })
+ .map((stake) => stake.getAmount({ lockingTimeExponent: params.lockingTimeExponent }))
+ .reduce((total, amount) => (total += amount), 0);
}
/**
- * @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
- * @returns number
+ * Stake reputation in favor of a given outcome for this validation pool.
+ *
+ * @param {*} reputationPublicKey
+ * @param {object} opts
*/
- getTotalValueOfStakesForOutcome(outcome) {
- return this.getStakes(outcome, { excludeSystem: false })
- .reduce((total, { amount }) => (total += amount), 0);
- }
-
- // TODO: This can be handled as a hook on receipt of reputation token transfer
async stake(reputationPublicKey, {
- tokenId, position, amount, lockingTime = 0,
+ tokenAddress, tokenTypeId = DEFAULT_REP_TOKEN_TYPE_ID, position, amount, lockingTime = 0,
}) {
+ // TODO: This can be handled as a hook on receipt of reputation token transfer
if (this.state === ValidationPoolStates.CLOSED) {
throw new Error(`Validation pool ${this.id} is closed.`);
}
@@ -217,17 +240,17 @@ export class ValidationPool extends ReputationHolder {
);
}
- if (reputationPublicKey !== this.dao.reputation.ownerOf(tokenId)) {
+ if (reputationPublicKey !== this.dao.reputation.ownerOf(tokenAddress)) {
throw new Error('Reputation may only be staked by its owner!');
}
const stake = new Stake({
- tokenId, position, amount, lockingTime,
+ tokenAddress, tokenTypeId, position, amount, lockingTime,
});
this.stakes.add(stake);
// Transfer staked amount from the sender to the validation pool
- this.dao.reputation.transferValueFrom(tokenId, this.tokenId, amount);
+ this.dao.reputation.transferValueFrom(tokenAddress, this.tokenAddress, tokenTypeId, amount);
// Keep a record of voters and their votes
if (reputationPublicKey !== this.id) {
@@ -243,8 +266,10 @@ export class ValidationPool extends ReputationHolder {
// Before evaluating the winning conditions,
// we need to make sure any staked tokens are locked for the
// specified amounts of time.
- for (const { tokenId, amount, lockingTime } of this.stakes.values()) {
- this.dao.reputation.lock(tokenId, amount, lockingTime);
+ for (const {
+ tokenAddress, tokenTypeId, amount, lockingTime,
+ } of this.stakes.values()) {
+ this.dao.reputation.lock(tokenAddress, tokenTypeId, amount, lockingTime);
// TODO: If there is an exception here, the voter may have voted incorrectly. Consider penalties.
}
}
@@ -261,23 +286,33 @@ export class ValidationPool extends ReputationHolder {
this.state = ValidationPoolStates.CLOSED;
this.setStatus('Closed');
- const upvoteValue = this.getTotalValueOfStakesForOutcome(true);
- const downvoteValue = this.getTotalValueOfStakesForOutcome(false);
- const activeAvailableReputation = this.dao.getActiveAvailableReputation();
- const votePasses = upvoteValue >= params.winningRatio * downvoteValue;
+ // Votes should be scaled by weights of this.reputationTypes
+ const upvoteValue = this.reputationTypes.reduce((value, { reputationTypeId, weight }) => {
+ value += this.getStakedAmount({ outcome: true, reputationTypeId }) * weight;
+ return value;
+ }, 0);
+ const downvoteValue = this.reputationTypes.reduce((value, { reputationTypeId, weight }) => {
+ value += this.getStakedAmount({ outcome: false, reputationTypeId }) * weight;
+ return value;
+ }, 0);
+ const activeAvailableReputation = this.reputationTypes.reduce((value, { reputationTypeId, weight }) => {
+ value += this.dao.getActiveAvailableReputation({ reputationTypeId }) * weight;
+ return value;
+ }, 0);
+ const outcome = upvoteValue >= params.winningRatio * downvoteValue;
const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation;
const result = {
- votePasses,
+ outcome,
upvoteValue,
downvoteValue,
};
if (quorumMet) {
- this.setStatus(`Resolved - ${votePasses ? 'Won' : 'Lost'}`);
- this.scene?.sequence.log(`note over ${this.name} : ${votePasses ? 'Win' : 'Lose'}`);
+ this.setStatus(`Resolved - ${outcome ? 'Won' : 'Lost'}`);
+ this.scene?.sequence.log(`note over ${this.name} : ${outcome ? 'Win' : 'Lose'}`);
this.applyTokenLocking();
- await this.distributeReputation({ votePasses });
+ await this.distributeReputation({ outcome });
// TODO: distribute fees
} else {
this.setStatus('Resolved - Quorum not met');
@@ -301,47 +336,57 @@ export class ValidationPool extends ReputationHolder {
return result;
}
- async distributeReputation({ votePasses }) {
- // For now we assume a tightly binding pool, where all staked reputation is lost
- // TODO: Take tokenLossRatio into account
- // TODO: revoke staked reputation from losing voters
+ async distributeReputation({ outcome }) {
+ // In a binding validation pool, losing voter stakes are transferred to winning voters.
+ // TODO: Regression tests for different tokenLossRatio values
+ const tokenLossRatio = this.getTokenLossRatio();
+ for (const { reputationTypeId, weight } of this.reputationTypes) {
+ const tokensForWinners = this.getStakedAmount({ outcome: !outcome, reputationTypeId }) * weight * tokenLossRatio;
+ const winningEntries = this.getStakes({ outcome, reputationTypeId, excludeSystem: true });
+ const totalValueOfStakesForWin = this.getStakedAmount({ outcome, reputationTypeId });
- // In a tightly binding validation pool, losing voter stakes are transferred to winning voters.
- const tokensForWinners = this.getTotalStakedOnPost(!votePasses);
- const winningEntries = this.getStakes(votePasses, { excludeSystem: true });
- const totalValueOfStakesForWin = this.getTotalValueOfStakesForOutcome(votePasses);
-
- // Compute rewards for the winning voters, in proportion to the value of their stakes.
- for (const stake of winningEntries) {
- const { tokenId, amount } = stake;
- const value = stake.getStakeValue({ lockingTimeExponent: params.lockingTimeExponent });
- const reward = tokensForWinners * (value / totalValueOfStakesForWin);
- // Also return each winning voter their staked amount
- const reputationPublicKey = this.dao.reputation.ownerOf(tokenId);
- console.log(`reward for winning stake by ${reputationPublicKey}: ${reward}`);
- this.dao.reputation.transferValueFrom(this.tokenId, tokenId, reward + amount);
- const toActor = this.scene?.findActor((actor) => actor.reputationPublicKey === reputationPublicKey);
- this.actions.reward.log(this, toActor, `(${displayNumber(reward)})`);
+ // Compute rewards for the winning voters, in proportion to the value of their stakes.
+ for (const stake of winningEntries) {
+ const { tokenAddress, amount } = stake;
+ const value = stake.getAmount({ lockingTimeExponent: params.lockingTimeExponent });
+ const reward = tokensForWinners * (value / totalValueOfStakesForWin);
+ // Also return each winning voter their staked amount
+ const reputationPublicKey = this.dao.reputation.ownerOf(tokenAddress);
+ console.log(`reward of type ${reputationTypeId} for winning stake by ${reputationPublicKey}: ${reward}`);
+ this.dao.reputation.transferValueFrom(this.tokenAddress, tokenAddress, reputationTypeId, reward + amount);
+ const toActor = this.scene?.findActor((actor) => actor.reputationPublicKey === reputationPublicKey);
+ this.actions.reward.log(this, toActor, `(${displayNumber(reward)} type ${reputationTypeId})`);
+ }
}
- if (votePasses) {
+ if (outcome === true) {
// Distribute awards to author via the forum
- // const tokensForAuthor = this.mintedValue * params.stakeForAuthor + rewards.get(this.tokenId);
- console.log(`sending reward for author stake to forum: ${this.dao.reputation.valueOf(this.tokenId)}`);
+ const tokens = this.reputationTypes.reduce((values, { reputationTypeId }) => {
+ const value = this.dao.reputation.valueOf(this.tokenAddress, reputationTypeId) ?? 0;
+ values[reputationTypeId] = value;
+ return values;
+ }, {});
+ console.log('sending reward for author stake to forum', tokens);
// Transfer ownership of the minted token, from the pool to the forum
- this.dao.reputation.transfer(this.id, this.dao.forum.id, this.tokenId);
- // const value = this.dao.reputation.valueOf(this.tokenId);
+ this.dao.reputation.transfer(this.id, this.dao.forum.id, this.tokenAddress);
+ // const value = this.dao.reputation.valueOf(this.tokenAddress);
// this.actions.transfer.log(this, this.dao.forum, `(${value})`);
- // Recurse through forum to determine reputation effects
- await this.dao.forum.onValidate({
+ const result = {
pool: this,
postId: this.postId,
- tokenId: this.tokenId,
+ tokenAddress: this.tokenAddress,
referenceChainLimit: params.referenceChainLimit,
leachingValue: params.leachingValue,
- });
+ };
+
+ // Recurse through forum to determine reputation effects
+ await this.dao.forum.onValidate({ ...result });
+
+ if (this.onValidate) {
+ await this.onValidate({ ...result });
+ }
}
console.log('pool complete');
diff --git a/src/classes/display/actor.js b/src/classes/display/actor.js
index 08ef1c6..2d54b42 100644
--- a/src/classes/display/actor.js
+++ b/src/classes/display/actor.js
@@ -89,6 +89,9 @@ export class Actor {
async computeDisplayValues(cb) {
for (const [label, fn] of this.valueFunctions.entries()) {
const value = fn();
+ console.log('computeDisplay', {
+ label, value, fn, cb,
+ });
await this.setDisplayValue(label, value);
if (cb) {
cb(label, value);
diff --git a/src/classes/display/box.js b/src/classes/display/box.js
index b25746c..7c5078d 100644
--- a/src/classes/display/box.js
+++ b/src/classes/display/box.js
@@ -1,5 +1,6 @@
import { DisplayValue } from './display-value.js';
import { randomID } from '../../util/helpers.js';
+import { Rectangle } from './geometry.js';
export class Box {
constructor(name, parentEl, options = {}) {
@@ -20,6 +21,7 @@ export class Box {
parentEl.appendChild(this.el);
}
}
+ this.boxes = [];
}
flex({ center = false } = {}) {
@@ -35,11 +37,6 @@ export class Box {
return this;
}
- hidden() {
- this.addClass('hidden');
- return this;
- }
-
addClass(className) {
this.el.classList.add(className);
return this;
@@ -47,6 +44,7 @@ export class Box {
addBox(name) {
const box = new Box(name, this.el);
+ this.boxes.push(box);
return box;
}
@@ -63,4 +61,16 @@ export class Box {
getId() {
return this.el.id;
}
+
+ getGeometry() {
+ const {
+ x, y, width, height,
+ } = this.el.getBoundingClientRect();
+ return new Rectangle([x, y], [width, height]);
+ }
+
+ move(vector) {
+ this.el.style.left = `${parseInt(this.el.style.left, 10) + vector[0]}px`;
+ this.el.style.top = `${parseInt(this.el.style.top, 10) + vector[1]}px`;
+ }
}
diff --git a/src/classes/display/document.js b/src/classes/display/document.js
index 2726e18..74b1974 100644
--- a/src/classes/display/document.js
+++ b/src/classes/display/document.js
@@ -37,7 +37,6 @@ export class Document extends Box {
}
get lastElement() {
- if (!this.elements.length) return null;
return this.elements[this.elements.length - 1];
}
}
diff --git a/src/classes/display/force-directed.js b/src/classes/display/force-directed.js
new file mode 100644
index 0000000..e1d1adf
--- /dev/null
+++ b/src/classes/display/force-directed.js
@@ -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);
+ }
+ }
+}
diff --git a/src/classes/display/form.js b/src/classes/display/form.js
index 1b20f6e..eff9fa5 100644
--- a/src/classes/display/form.js
+++ b/src/classes/display/form.js
@@ -19,6 +19,20 @@ export class FormElement extends Box {
}
}
+export class Select extends FormElement {
+ constructor(name, form, opts) {
+ super(name, form, opts);
+ const { options } = opts;
+ this.selectEl = document.createElement('select');
+ for (const { value, label } of options) {
+ const optionEl = document.createElement('option');
+ optionEl.setAttribute('value', value);
+ optionEl.innerHTML = label || value;
+ }
+ this.el.appendChild(this.selectEl);
+ }
+}
+
export class Button extends FormElement {
constructor(name, form, opts) {
super(name, form, { ...opts, cbEventTypes: ['click'] });
@@ -43,15 +57,18 @@ export class TextField extends FormElement {
constructor(name, form, opts) {
super(name, form, opts);
this.flex({ center: true });
- this.label = document.createElement('label');
- this.labelDiv = document.createElement('div');
- this.label.appendChild(this.labelDiv);
- this.labelDiv.innerHTML = opts.label || name;
- this.input = document.createElement('input');
- this.input.disabled = !!opts.disabled;
- this.input.defaultValue = opts.defaultValue || '';
- this.label.appendChild(this.input);
- this.el.appendChild(this.label);
+
+ // Place label inside a div, for improved styling
+ const labelDiv = document.createElement('div');
+ labelDiv.innerHTML = opts.label || name;
+ const label = document.createElement('label');
+ label.appendChild(labelDiv);
+ const input = document.createElement('input');
+ input.disabled = !!opts.disabled;
+ input.defaultValue = opts.defaultValue || '';
+ label.appendChild(input);
+ this.el.appendChild(label);
+ this.input = input;
}
get value() {
@@ -61,6 +78,30 @@ export class TextField extends FormElement {
export class TextArea extends FormElement { }
+export class SubForm extends FormElement {
+ // Form has:
+ // this.document = document;
+ // this.items = [];
+ // this.id = opts.id ?? `form_${randomID()}`;
+ // FormElement has
+ constructor(name, form, opts) {
+ if (!name) {
+ name = `subform${randomID()}`;
+ }
+ const parentEl = opts.subFormArray ? opts.subFormArray.el : form.el;
+ const subForm = form.document.form({ name, parentEl, tagName: 'div' }).lastElement;
+ super(name, form, { ...opts, parentEl });
+ this.subForm = subForm;
+ if (opts.subFormArray) {
+ opts.subFormArray.subForms.push(this);
+ }
+ }
+
+ get value() {
+ return this.subForm.value;
+ }
+}
+
export class SubFormArray extends FormElement {
constructor(name, form, opts) {
super(name, form, opts);
@@ -76,36 +117,30 @@ export class SubFormArray extends FormElement {
this.subForms.splice(idx, 1);
subForm.el.remove();
}
-}
-export class SubForm extends FormElement {
- constructor(name, form, opts) {
- const parentEl = opts.subFormArray ? opts.subFormArray.el : form.el;
- const subForm = form.document.form({ name, parentEl, tagName: 'div' }).lastElement;
- super(name, form, { ...opts, parentEl });
- this.subForm = subForm;
- if (opts.subFormArray) {
- opts.subFormArray.subForms.push(this);
- }
+ subForm(opts = {}) {
+ const subForm = new SubForm(opts.name, this.form, { ...opts, subFormArray: this });
+ this.subForms.push(subForm);
+ return this;
}
- get value() {
- return this.subForm.value;
+ get lastSubForm() {
+ return this.subForms[this.subForms.length - 1];
}
}
export class FileInput extends FormElement {
constructor(name, form, opts) {
super(name, form, opts);
- this.input = document.createElement('input');
- this.input.type = 'file';
- this.input.accept = 'application/json';
- this.input.classList.add('visually-hidden');
- this.label = document.createElement('label');
- this.button = form.button({ name, cb: () => this.input.click() }).lastItem;
- this.label.appendChild(this.button.el);
- this.label.appendChild(this.input);
- this.el.appendChild(this.label);
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = 'application/json';
+ input.classList.add('visually-hidden');
+ const label = document.createElement('label');
+ const button = form.button({ name, cb: () => input.click() }).lastItem;
+ label.appendChild(button.el);
+ label.appendChild(input);
+ this.el.appendChild(label);
}
}
@@ -119,6 +154,11 @@ export class Form extends Box {
this.el.onsubmit = () => false;
}
+ select(opts) {
+ this.items.push(new Select(opts.name, this, opts));
+ return this;
+ }
+
button(opts) {
this.items.push(new Button(opts.name, this, opts));
return this;
@@ -154,6 +194,10 @@ export class Form extends Box {
return this;
}
+ remark(text, opts) {
+ this.document.remark(text, { ...opts, parentEl: this.el });
+ }
+
get lastItem() {
return this.items[this.items.length - 1];
}
diff --git a/src/classes/display/geometry.js b/src/classes/display/geometry.js
new file mode 100644
index 0000000..f8b21eb
--- /dev/null
+++ b/src/classes/display/geometry.js
@@ -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);
+ });
+ }
+}
diff --git a/src/classes/display/scene.js b/src/classes/display/scene.js
index 43103e5..b650c2e 100644
--- a/src/classes/display/scene.js
+++ b/src/classes/display/scene.js
@@ -12,7 +12,7 @@ export class Scene {
constructor(name, rootBox) {
this.name = name;
this.box = rootBox.addBox(name);
- this.titleBox = this.box.addBox('Title').setInnerHTML(name);
+ // this.titleBox = this.box.addBox('Title').setInnerHTML(name);
this.box.addBox('Spacer').setInnerHTML(' ');
this.topSection = this.box.addBox('Top section').flex();
this.displayValuesBox = this.topSection.addBox('Values');
diff --git a/src/classes/reputation/reputation-token.js b/src/classes/reputation/reputation-token.js
index 73ea289..23cc80e 100644
--- a/src/classes/reputation/reputation-token.js
+++ b/src/classes/reputation/reputation-token.js
@@ -1,56 +1,64 @@
-import { ERC721 } from '../supporting/erc721.js';
+import { ERC1155 } from '../supporting/erc1155.js';
import { randomID } from '../../util/helpers.js';
import { EPSILON } from '../../util/constants.js';
class Lock {
- constructor(tokenId, amount, duration) {
+ constructor(tokenAddress, tokenTypeId, amount, duration) {
this.dateCreated = new Date();
- this.tokenId = tokenId;
+ this.tokenAddress = tokenAddress;
this.amount = amount;
this.duration = duration;
+ this.tokenTypeId = tokenTypeId;
}
}
-export class ReputationTokenContract extends ERC721 {
+export class ReputationTokenContract extends ERC1155 {
constructor() {
super('Reputation', 'REP');
- this.histories = new Map(); // token id --> {increment, context (i.e. validation pool id)}
- this.values = new Map(); // token id --> current value
- this.locks = new Set(); // {tokenId, amount, start, duration}
+ this.histories = new Map(); // token address --> {tokenTypeId, increment, context (i.e. validation pool id)}
+ this.values = new Map(); // token address --> token type id --> current value
+ this.locks = new Set(); // {tokenAddress, tokenTypeId, amount, start, duration}
}
/**
*
- * @param to
- * @param value
- * @param context
+ * @param to Recipient address
+ * @param values Object with reputation type id as key, and amount of reputation as value
* @returns {string}
*/
- mint(to, value, context = {}) {
- const tokenId = `token_${randomID()}`;
- super.mint(to, tokenId);
- this.values.set(tokenId, value);
- this.histories.set(tokenId, [{ increment: value, context }]);
- return tokenId;
+ mintBatch(to, tokenTypeIds, values) {
+ const tokenAddress = `token_${randomID()}`;
+ super.mintBatch(to, tokenAddress, tokenTypeIds, tokenTypeIds.map(() => 1));
+ const tokenMap = new Map();
+ for (let idx = 0; idx < tokenTypeIds.length; idx++) {
+ const tokenTypeId = tokenTypeIds[idx];
+ const value = values[idx];
+ tokenMap.set(tokenTypeId, value);
+ }
+ this.values.set(tokenAddress, tokenMap);
+ this.histories.set(tokenAddress, [{ operation: 'mintBatch', args: { to, tokenTypeIds, values } }]);
+ return tokenAddress;
}
- incrementValue(tokenId, increment, context) {
- const value = this.values.get(tokenId);
- if (value === undefined) {
- throw new Error(`Token not found: ${tokenId}`);
+ incrementValue(tokenAddress, tokenTypeId, increment, context) {
+ const tokenTypeIds = this.values.get(tokenAddress);
+ if (tokenTypeIds === undefined) {
+ throw new Error(`Token not found: ${tokenAddress}`);
}
+ const value = tokenTypeIds?.get(tokenTypeId);
const newValue = value + increment;
- const history = this.histories.get(tokenId) || [];
+ const history = this.histories.get(tokenAddress) || [];
if (newValue < -EPSILON) {
throw new Error(`Token value can not become negative. Attempted to set value = ${newValue}`);
}
- this.values.set(tokenId, newValue);
- history.push({ increment, context });
- this.histories.set(tokenId, history);
+ tokenTypeIds.set(tokenAddress, newValue);
+ this.values.set(tokenAddress, tokenTypeIds);
+ history.push({ tokenTypeId, increment, context });
+ this.histories.set(tokenAddress, history);
}
- transferValueFrom(fromTokenId, toTokenId, amount) {
+ transferValueFrom(from, to, tokenTypeId, amount) {
if (amount === undefined) {
throw new Error('Transfer value: amount is undefined!');
}
@@ -60,64 +68,83 @@ export class ReputationTokenContract extends ERC721 {
if (amount < 0) {
throw new Error('Transfer value: amount must be positive');
}
- const sourceAvailable = this.availableValueOf(fromTokenId);
+ const sourceAvailable = this.availableValueOf(from, tokenTypeId);
if (sourceAvailable < amount - EPSILON) {
throw new Error('Token value transfer: source has insufficient available value. '
+ `Needs ${amount}; has ${sourceAvailable}.`);
}
- this.incrementValue(fromTokenId, -amount);
- this.incrementValue(toTokenId, amount);
+ this.incrementValue(from, tokenTypeId, -amount);
+ this.incrementValue(to, tokenTypeId, amount);
}
- lock(tokenId, amount, duration) {
- const lock = new Lock(tokenId, amount, duration);
+ batchTransferValueFrom(from, to, tokenTypeIds, amounts) {
+ for (let idx = 0; idx < tokenTypeIds.length; idx++) {
+ const tokenTypeId = tokenTypeIds[idx];
+ const amount = amounts[idx];
+ if (amount === undefined) {
+ throw new Error('Transfer value: amount is undefined!');
+ }
+ if (amount === 0) {
+ return;
+ }
+ if (amount < 0) {
+ throw new Error('Transfer value: amount must be positive');
+ }
+ const sourceAvailable = this.availableValueOf(from, tokenTypeId);
+ if (sourceAvailable < amount - EPSILON) {
+ throw new Error('Token value transfer: source has insufficient available value. '
+ + `Needs ${amount}; has ${sourceAvailable}.`);
+ }
+ this.incrementValue(from, tokenTypeId, -amount);
+ this.incrementValue(to, tokenTypeId, amount);
+ }
+ }
+
+ lock(tokenAddress, tokenTypeId, amount, duration) {
+ const lock = new Lock(tokenAddress, tokenTypeId, amount, duration);
this.locks.add(lock);
}
- historyOf(tokenId) {
- return this.histories.get(tokenId);
+ historyOf(tokenAddress) {
+ return this.histories.get(tokenAddress);
}
- valueOf(tokenId) {
- const value = this.values.get(tokenId);
- if (value === undefined) {
- throw new Error(`Token not found: ${tokenId}`);
+ valueOf(tokenAddress, tokenTypeId) {
+ const tokenTypeIds = this.values.get(tokenAddress);
+ if (tokenTypeIds === undefined) {
+ throw new Error(`Token not found: ${tokenAddress}`);
}
- return value;
+ return tokenTypeIds.get(tokenTypeId);
}
- availableValueOf(tokenId) {
+ availableValueOf(tokenAddress, tokenTypeId) {
const amountLocked = Array.from(this.locks.values())
- .filter(({ tokenId: lockTokenId }) => lockTokenId === tokenId)
+ .filter((lock) => lock.tokenAddress === tokenAddress && lock.tokenTypeId === tokenTypeId)
.filter(({ dateCreated, duration }) => new Date() - dateCreated < duration)
.reduce((total, { amount }) => total += amount, 0);
- return this.valueOf(tokenId) - amountLocked;
+ return this.valueOf(tokenAddress, tokenTypeId) - amountLocked;
}
- valueOwnedBy(ownerId) {
+ valueOwnedBy(ownerAddress, tokenTypeId) {
return Array.from(this.owners.entries())
- .filter(([__, owner]) => owner === ownerId)
- .map(([tokenId, __]) => this.valueOf(tokenId))
+ .filter(([__, owner]) => owner === ownerAddress)
+ .map(([tokenAddress, __]) => this.valueOf(tokenAddress, tokenTypeId))
.reduce((total, value) => total += value, 0);
}
- availableValueOwnedBy(ownerId) {
+ availableValueOwnedBy(ownerAddress, tokenTypeId) {
return Array.from(this.owners.entries())
- .filter(([__, owner]) => owner === ownerId)
- .map(([tokenId, __]) => this.availableValueOf(tokenId))
+ .filter(([__, owner]) => owner === ownerAddress)
+ .map(([tokenAddress, __]) => this.availableValueOf(tokenAddress, tokenTypeId))
.reduce((total, value) => total += value, 0);
}
- getTotal() {
- return Array.from(this.values.values()).reduce((total, value) => total += value, 0);
+ getTotal(tokenTypeId) {
+ return Array.from(this.values.values())
+ .flatMap((tokens) => tokens.get(tokenTypeId))
+ .reduce((total, value) => total += value, 0);
}
- getTotalAvailable() {
- const amountLocked = Array.from(this.locks.values())
- .filter(({ dateCreated, duration }) => new Date() - dateCreated < duration)
- .reduce((total, { amount }) => total += amount, 0);
-
- return this.getTotal() - amountLocked;
- }
+ // burn(tokenAddress, tokenTypeId, )
}
diff --git a/src/classes/supporting/edge.js b/src/classes/supporting/edge.js
index a05e48f..1b9cc64 100644
--- a/src/classes/supporting/edge.js
+++ b/src/classes/supporting/edge.js
@@ -75,6 +75,12 @@ export class Edge {
static prepareEditorDocument(graph, doc, from, to) {
const form = doc.form({ name: 'editorForm' }).lastElement;
+ form.button({
+ name: 'New Vertex',
+ cb: () => {
+ graph.resetEditor();
+ },
+ });
doc.remark('Edit Edge
', { parentEl: form.el });
form
.textField({
@@ -88,7 +94,7 @@ export class Edge {
const subFormArray = form.subFormArray({ id: 'edges', name: 'edges' }).lastItem;
const addEdgeForm = (edge) => {
- const { subForm } = form.subForm({ name: 'subform', subFormArray }).lastItem;
+ const { subForm } = form.subForm({ subFormArray }).lastItem;
subForm.textField({
id: 'type', name: 'type', defaultValue: edge.type, required: true,
})
diff --git a/src/classes/supporting/erc1155.js b/src/classes/supporting/erc1155.js
new file mode 100644
index 0000000..945a5b5
--- /dev/null
+++ b/src/classes/supporting/erc1155.js
@@ -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) {}
+}
diff --git a/src/classes/supporting/erc721.js b/src/classes/supporting/erc721.js
index 9a68166..c69bb26 100644
--- a/src/classes/supporting/erc721.js
+++ b/src/classes/supporting/erc721.js
@@ -16,78 +16,78 @@ export class ERC721 {
this.balances = new Map(); // owner address --> token count
this.owners = new Map(); // token id --> owner address
// this.tokenApprovals = new Map(); // token id --> approved addresses
- // this.operatorApprovals = new Map(); // owner --> operator approvals
+ // this.operatorApprovals = new Map(); // ownerAddress --> operator approvals
this.events = {
- // Transfer: (_from, _to, _tokenId) => {},
- // Approval: (_owner, _approved, _tokenId) => {},
+ // Transfer: (_from, _to, _tokenAddress) => {},
+ // Approval: (_owner, _approved, _tokenAddress) => {},
// ApprovalForAll: (_owner, _operator, _approved) => {},
};
}
- incrementBalance(owner, increment) {
- const balance = this.balances.get(owner) ?? 0;
- this.balances.set(owner, balance + increment);
+ incrementBalance(ownerAddress, increment) {
+ const balance = this.balances.get(ownerAddress) ?? 0;
+ this.balances.set(ownerAddress, balance + increment);
}
- mint(to, tokenId) {
- console.log('ERC721.mint', { to, tokenId });
- if (this.owners.get(tokenId)) {
+ mint(to, tokenAddress) {
+ console.log('ERC721.mint', { to, tokenAddress });
+ if (this.owners.get(tokenAddress)) {
throw new Error('ERC721: token already minted');
}
this.incrementBalance(to, 1);
- this.owners.set(tokenId, to);
+ this.owners.set(tokenAddress, to);
}
- burn(tokenId) {
- const owner = this.owners.get(tokenId);
- this.incrementBalance(owner, -1);
- this.owners.delete(tokenId);
+ burn(tokenAddress) {
+ const ownerAddress = this.owners.get(tokenAddress);
+ this.incrementBalance(ownerAddress, -1);
+ this.owners.delete(tokenAddress);
}
- balanceOf(owner) {
- if (!owner) {
+ balanceOf(ownerAddress) {
+ if (!ownerAddress) {
throw new Error('ERC721: address zero is not a valid owner');
}
- return this.balances.get(owner) ?? 0;
+ return this.balances.get(ownerAddress) ?? 0;
}
- ownerOf(tokenId) {
- const owner = this.owners.get(tokenId);
- if (!owner) {
- throw new Error(`ERC721: invalid token ID: ${tokenId}`);
+ ownerOf(tokenAddress) {
+ const ownerAddress = this.owners.get(tokenAddress);
+ if (!ownerAddress) {
+ throw new Error(`ERC721: invalid token ID: ${tokenAddress}`);
}
- return owner;
+ return ownerAddress;
}
- transfer(from, to, tokenId) {
- console.log('ERC721.transfer', { from, to, tokenId });
- const owner = this.owners.get(tokenId);
- if (owner !== from) {
- throw new Error(`ERC721: transfer from incorrect owner ${from}; should be ${owner}`);
+ transfer(from, to, tokenAddress) {
+ console.log('ERC721.transfer', { from, to, tokenAddress });
+ const ownerAddress = this.owners.get(tokenAddress);
+ if (ownerAddress !== from) {
+ throw new Error(`ERC721: transfer from incorrect owner ${from}; should be ${ownerAddress}`);
}
this.incrementBalance(from, -1);
this.incrementBalance(to, 1);
- this.owners.set(tokenId, to);
+ this.owners.set(tokenAddress, to);
}
/// @notice Enable or disable approval for a third party ("operator") to manage
/// all of `msg.sender`'s assets
/// @dev Emits the ApprovalForAll event. The contract MUST allow
- /// multiple operators per owner.
+ /// multiple operators per ownerAddress.
/// @param _operator Address to add to the set of authorized operators
/// @param _approved True if the operator is approved, false to revoke approval
// setApprovalForAll(_operator, _approved) {}
/// @notice Get the approved address for a single NFT
- /// @dev Throws if `_tokenId` is not a valid NFT.
- /// @param _tokenId The NFT to find the approved address for
+ /// @dev Throws if `_tokenAddress` is not a valid NFT.
+ /// @param _tokenAddress The NFT to find the approved address for
/// @return The approved address for this NFT, or the zero address if there is none
- // getApproved(_tokenId) {}
+ // getApproved(_tokenAddress) {}
/// @notice Query if an address is an authorized operator for another address
/// @param _owner The address that owns the NFTs
- /// @param _operator The address that acts on behalf of the owner
+ /// @param _operator The address that acts on behalf of the ownerAddress
/// @return True if `_operator` is an approved operator for `_owner`, false otherwise
// isApprovedForAll(_owner, _operator) {}
}
diff --git a/src/classes/supporting/schema.js b/src/classes/supporting/schema.js
new file mode 100644
index 0000000..5644ffd
--- /dev/null
+++ b/src/classes/supporting/schema.js
@@ -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('Node Types
');
+ 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(`Node Type: ${name}
`);
+ } else {
+ nodeTypeForm.remark('New Node Type
');
+ }
+ 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('Edge Types
');
+ const form = doc.form('Edge Types Editor').lastElement;
+ for (const { name } of schema.edgeTypes.values()) {
+ form.remark(`Edge Type: ${name}
`);
+ form.textField({ name: 'name', defaultValue: name });
+ }
+ form.submit({
+ name: 'Save',
+ });
+ }
+}
+
+// Properties
+// Data types
+// Relationships
diff --git a/src/classes/supporting/stake.js b/src/classes/supporting/stake.js
index 8b6ac7e..6fad7e4 100644
--- a/src/classes/supporting/stake.js
+++ b/src/classes/supporting/stake.js
@@ -1,14 +1,15 @@
export class Stake {
constructor({
- tokenId, position, amount, lockingTime,
+ tokenAddress, tokenTypeId, position, amount, lockingTime,
}) {
- this.tokenId = tokenId;
+ this.tokenAddress = tokenAddress;
this.position = position;
this.amount = amount;
this.lockingTime = lockingTime;
+ this.tokenTypeId = tokenTypeId;
}
- getStakeValue({ lockingTimeExponent } = {}) {
+ getAmount({ lockingTimeExponent } = {}) {
return this.amount * this.lockingTime ** lockingTimeExponent;
}
}
diff --git a/src/classes/supporting/vertex.js b/src/classes/supporting/vertex.js
index 1137bf6..92671c0 100644
--- a/src/classes/supporting/vertex.js
+++ b/src/classes/supporting/vertex.js
@@ -15,7 +15,7 @@ export class Vertex {
to: [],
};
this.installedClickCallback = false;
- this.properties = new Map();
+ this.properties = options.properties ?? new Map();
}
toJSON() {
@@ -51,14 +51,18 @@ export class Vertex {
}
let html = '';
- html += `${this.label}`;
+ if (this.type) {
+ html += `${this.type}`;
+ }
+ html += `${this.label || this.id}`;
html += '';
+ console.log('displayVertex', { properties: this.properties });
for (const [key, value] of this.properties.entries()) {
const displayValue = typeof value === 'number' ? displayNumber(value) : value;
html += `${key} | ${displayValue} |
`;
}
html += '
';
- if (this.id !== this.label) {
+ if (this.label && this.id !== this.label) {
html += `${this.id}
`;
}
html = html.replaceAll(/\n\s*/g, '');
@@ -73,26 +77,34 @@ export class Vertex {
static prepareEditorDocument(graph, doc, vertexId) {
const vertex = vertexId ? graph.getVertex(vertexId) : undefined;
const form = doc.form().lastElement;
+
+ if (vertex) {
+ form.button({
+ name: 'New Vertex',
+ cb: () => {
+ graph.resetEditor();
+ },
+ });
+ }
doc.remark(`${vertex ? 'Edit' : 'Add'} Vertex
`, { parentEl: form.el });
form
.textField({
- id: 'id', name: 'id', defaultValue: vertex?.id,
+ name: 'id', defaultValue: vertex?.id,
})
- .textField({ id: 'type', name: 'type', defaultValue: vertex?.type })
- .textField({ id: 'label', name: 'label', defaultValue: vertex?.label });
+ .textField({ name: 'type', defaultValue: vertex?.type })
+ .textField({ name: 'label', defaultValue: vertex?.label });
doc.remark('Properties
', { parentEl: form.el });
- const subFormArray = form.subFormArray({ id: 'properties', name: 'properties' }).lastItem;
+ const subFormArray = form.subFormArray({ name: 'properties' }).lastItem;
const addPropertyForm = (key, value) => {
- const { subForm } = form.subForm({ name: 'subform', subFormArray }).lastItem;
- subForm.textField({ id: 'key', name: 'key', defaultValue: key })
- .textField({ id: 'value', name: 'value', defaultValue: value })
+ const { subForm } = form.subForm({ subFormArray }).lastItem;
+ subForm.textField({ name: 'key', defaultValue: key })
+ .textField({ name: 'value', defaultValue: value })
.button({
- id: 'remove',
name: 'Remove Property',
cb: () => subFormArray.remove(subForm),
- });
- doc.remark('
', { parentEl: subForm.el });
+ })
+ .remark('
');
};
if (vertex) {
@@ -102,13 +114,11 @@ export class Vertex {
}
form.button({
- id: 'add',
name: 'Add Property',
cb: () => addPropertyForm('', ''),
});
form.submit({
- id: 'save',
name: 'Save',
cb: ({ form: { value: formValue } }) => {
let fullRedraw = false;
@@ -121,7 +131,10 @@ export class Vertex {
Object.assign(vertex, formValue);
vertex.displayVertex();
} else {
- const newVertex = graph.addVertex(formValue.type, formValue.id, null, formValue.label);
+ const {
+ type, id, label, properties,
+ } = formValue;
+ const newVertex = graph.addVertex(type, id, null, label, { properties });
Object.assign(newVertex, formValue);
doc.clear();
Vertex.prepareEditorDocument(graph, doc, newVertex.id);
@@ -134,7 +147,6 @@ export class Vertex {
if (vertex) {
form.button({
- id: 'delete',
name: 'Delete Vertex',
cb: () => {
graph.deleteVertex(vertex.id);
@@ -159,7 +171,6 @@ export class Vertex {
}
form.button({
- id: 'cancel',
name: 'Cancel',
cb: () => graph.resetEditor(),
parentEl: doc.el,
diff --git a/src/classes/supporting/wdg.js b/src/classes/supporting/wdg.js
index c454279..8636c3c 100644
--- a/src/classes/supporting/wdg.js
+++ b/src/classes/supporting/wdg.js
@@ -131,9 +131,10 @@ export class WeightedDirectedGraph {
const form = this.controlDoc.form({ name: 'controlForm' }).lastElement;
const { subForm: graphPropertiesForm } = form.subForm({ name: 'graphPropsForm' }).lastItem;
graphPropertiesForm.flex()
- .textField({ name: 'name', label: 'Graph name', defaultValue: this.name })
- .submit({
- name: 'Save',
+ .textField({
+ name: 'name',
+ label: 'Graph name',
+ defaultValue: this.name,
cb: (({ form: { value: { name } } }) => {
this.name = name;
}),
diff --git a/src/index.css b/src/index.css
index cfa67b0..72c4aaf 100644
--- a/src/index.css
+++ b/src/index.css
@@ -50,6 +50,12 @@ a:visited {
left: 12em;
top: -0.5em;
}
+.fixed {
+ position: fixed;
+}
+.absolute {
+ position: absolute;
+}
svg {
width: 800px;
}
diff --git a/src/index.html b/src/index.html
index decbfdb..8735cfd 100644
--- a/src/index.html
+++ b/src/index.html
@@ -15,18 +15,19 @@
For more information please see the DGF
Wiki.
+
+ The code for this site is available in GitLab.
+
Tools
Example Scenarios
Below are example scenarios with various assertions covering features of our reputation system.
-
- The code for this site is available in GitLab.
-
diff --git a/src/schema-editor/index.html b/src/schema-editor/index.html
new file mode 100644
index 0000000..d7d987d
--- /dev/null
+++ b/src/schema-editor/index.html
@@ -0,0 +1,14 @@
+
+
+
+ Schema Editor
+
+
+
+
+
+
+
+ Schema Editor
+
+
\ No newline at end of file
diff --git a/src/schema-editor/index.js b/src/schema-editor/index.js
new file mode 100644
index 0000000..6e75443
--- /dev/null
+++ b/src/schema-editor/index.js
@@ -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);
diff --git a/src/tests/all.test.html b/src/tests/all.test.html
index 9b3b7c1..ebc61ab 100644
--- a/src/tests/all.test.html
+++ b/src/tests/all.test.html
@@ -40,6 +40,7 @@
+
diff --git a/src/tests/force-directed.test.html b/src/tests/force-directed.test.html
new file mode 100644
index 0000000..f77ac16
--- /dev/null
+++ b/src/tests/force-directed.test.html
@@ -0,0 +1,28 @@
+
+
+
+ Force-Directed Graph test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/tests/scripts/availability.test.js b/src/tests/scripts/availability.test.js
index 5ac1619..cd209bf 100644
--- a/src/tests/scripts/availability.test.js
+++ b/src/tests/scripts/availability.test.js
@@ -6,6 +6,7 @@ import { Public } from '../../classes/actors/public.js';
import { PostContent } from '../../classes/supporting/post-content.js';
import { delayOrWait } from '../../classes/display/scene-controls.js';
import { mochaRun } from '../../util/helpers.js';
+import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
const DELAY_INTERVAL = 100;
const POOL_DURATION = 200;
@@ -32,7 +33,7 @@ const setup = async () => {
scene.withTable();
dao = new DAO('DGF', scene);
- await dao.setDisplayValue('total rep', () => dao.reputation.getTotal());
+ await dao.setDisplayValue('total rep', () => dao.reputation.getTotal(DEFAULT_REP_TOKEN_TYPE_ID));
experts = [];
@@ -58,7 +59,7 @@ const setup = async () => {
await pool1.evaluateWinningConditions();
await delayOrWait(DELAY_INTERVAL);
- dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(10);
+ dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(10);
const { pool: pool2 } = await experts[1].submitPostWithFee(
new PostContent({ hello: 'to you as well' })
@@ -77,8 +78,8 @@ const setup = async () => {
await pool2.evaluateWinningConditions();
await delayOrWait(DELAY_INTERVAL);
- dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(15);
- dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(5);
+ dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(15);
+ dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
};
const getActiveWorker = async () => {
diff --git a/src/tests/scripts/force-directed.test.js b/src/tests/scripts/force-directed.test.js
new file mode 100644
index 0000000..7754d4d
--- /dev/null
+++ b/src/tests/scripts/force-directed.test.js
@@ -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();
diff --git a/src/tests/scripts/forum.test-util.js b/src/tests/scripts/forum.test-util.js
index 36cb47a..5314f39 100644
--- a/src/tests/scripts/forum.test-util.js
+++ b/src/tests/scripts/forum.test-util.js
@@ -4,6 +4,7 @@ import { Expert } from '../../classes/actors/expert.js';
import { PostContent } from '../../classes/supporting/post-content.js';
import { DAO } from '../../classes/dao/dao.js';
import { delayOrWait } from '../../classes/display/scene-controls.js';
+import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
export class ForumTest {
constructor(options) {
@@ -86,6 +87,6 @@ export class ForumTest {
await this.newExpert();
- await this.dao.addComputedValue('total value', () => this.dao.reputation.getTotal());
+ await this.dao.addComputedValue('total rep', () => this.dao.reputation.getTotal(DEFAULT_REP_TOKEN_TYPE_ID));
}
}
diff --git a/src/tests/scripts/forum/forum10.test.js b/src/tests/scripts/forum/forum10.test.js
index 21430a3..dcd43ed 100644
--- a/src/tests/scripts/forum/forum10.test.js
+++ b/src/tests/scripts/forum/forum10.test.js
@@ -1,3 +1,4 @@
+import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../../util/constants.js';
import { mochaRun } from '../../../util/helpers.js';
import { ForumTest } from '../forum.test-util.js';
@@ -34,9 +35,9 @@ describe('Forum', function tests() {
await forumTest.addPost(authors, 10);
forum.getPost(posts[0]).value.should.equal(10);
- dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(5);
- dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(2.5);
- dao.reputation.valueOwnedBy(experts[2].reputationPublicKey).should.equal(2.5);
+ dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
+ dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(2.5);
+ dao.reputation.valueOwnedBy(experts[2].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(2.5);
});
});
});
diff --git a/src/tests/scripts/forum/forum11.test.js b/src/tests/scripts/forum/forum11.test.js
index 04dcc92..909eebe 100644
--- a/src/tests/scripts/forum/forum11.test.js
+++ b/src/tests/scripts/forum/forum11.test.js
@@ -1,3 +1,4 @@
+import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../../util/constants.js';
import { mochaRun } from '../../../util/helpers.js';
import { ForumTest } from '../forum.test-util.js';
@@ -33,9 +34,9 @@ describe('Forum', function tests() {
await forumTest.addPost(authors, 10);
forum.getPost(posts[0]).value.should.equal(10);
- dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(5);
- dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(5);
- dao.reputation.valueOwnedBy(experts[2].reputationPublicKey).should.equal(0);
+ dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
+ dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
+ dao.reputation.valueOwnedBy(experts[2].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(0);
});
it('Post2 with two authors, one shared with Post1', async () => {
@@ -46,9 +47,9 @@ describe('Forum', function tests() {
await forumTest.addPost(authors, 10);
forum.getPost(posts[0]).value.should.equal(10);
- dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(5);
- dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(10);
- dao.reputation.valueOwnedBy(experts[2].reputationPublicKey).should.equal(5);
+ dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
+ dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(10);
+ dao.reputation.valueOwnedBy(experts[2].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
});
});
});
diff --git a/src/util/constants.js b/src/util/constants.js
index fbfdb79..a3303c7 100644
--- a/src/util/constants.js
+++ b/src/util/constants.js
@@ -1,14 +1,17 @@
-export const EPSILON = 2.23e-16;
-
-export const INCINERATOR_ADDRESS = '0';
-
export const EdgeTypes = {
CITATION: 'citation',
BALANCE: 'balance',
AUTHOR: 'author',
};
-
export const VertexTypes = {
POST: 'post',
AUTHOR: 'author',
};
+export const DEFAULT_REP_TOKEN_TYPE_ID = 0;
+export const DEFAULT_TARGET_RADIUS = 100;
+export const EPSILON = 2.23e-16;
+export const INCINERATOR_ADDRESS = '0';
+export const OVERLAP_FORCE = 100;
+export const DISTANCE_FACTOR = 0.25;
+export const MINIMUM_FORCE = 2;
+export const VISCOSITY_FACTOR = 0.4;