Compare commits
57 Commits
forum-test
...
main
Author | SHA1 | Date |
---|---|---|
Ladd Hoffman | 24c183912a | |
Ladd Hoffman | ad382b5caf | |
Ladd Hoffman | c80f2ee79b | |
Ladd Hoffman | 846eb73cea | |
Ladd Hoffman | 1f3d8a7d1e | |
Ladd Hoffman | 6679b9dedb | |
Ladd Hoffman | 7a8bb0a95e | |
Ladd Hoffman | 4d53f5c70e | |
Ladd Hoffman | 907b99bb65 | |
Ladd Hoffman | 5230f8664b | |
Ladd Hoffman | 9eff884636 | |
Ladd Hoffman | 8d0daf2062 | |
Ladd Hoffman | 72c3bd1663 | |
Ladd Hoffman | ea6e2d4494 | |
Ladd Hoffman | a743a81218 | |
Ladd Hoffman | 07370be4fa | |
Ladd Hoffman | 56132b2fec | |
Ladd Hoffman | f978c20104 | |
Ladd Hoffman | ae5ab09e16 | |
Ladd Hoffman | dd582c4d20 | |
Ladd Hoffman | 77d6698899 | |
Ladd Hoffman | 498b5c106f | |
Ladd Hoffman | 9eb3451451 | |
Ladd Hoffman | 82e026f327 | |
Ladd Hoffman | 1f1f1f0c1d | |
Ladd Hoffman | 8bb188ff13 | |
Ladd Hoffman | 9974712aa9 | |
Ladd Hoffman | a8544dfd39 | |
Ladd Hoffman | 36acc56fa2 | |
Ladd Hoffman | 7e74773242 | |
Ladd Hoffman | e602466800 | |
Ladd Hoffman | 7eddd66385 | |
Ladd Hoffman | 21a0ef6bda | |
Ladd Hoffman | ce4f78aa97 | |
Ladd Hoffman | 81823cd009 | |
Ladd Hoffman | b02efb66ad | |
Ladd Hoffman | 36c827a40f | |
Ladd Hoffman | 92bbab2a5b | |
Ladd Hoffman | d4bdb1c435 | |
Ladd Hoffman | f3037a766d | |
Ladd Hoffman | 5ca884686b | |
Ladd Hoffman | 2ed07b7f5e | |
Ladd Hoffman | 353190fdcd | |
Ladd Hoffman | 0a8b170115 | |
Ladd Hoffman | 5988950857 | |
Ladd Hoffman | 63b43a0f4d | |
Ladd Hoffman | e6a0c22d3f | |
Ladd Hoffman | dddee70365 | |
Ladd Hoffman | b943daf28c | |
Ladd Hoffman | fa7f0620b6 | |
Ladd Hoffman | c4a528283c | |
Ladd Hoffman | 3072dbae28 | |
Ladd Hoffman | aba7cc6870 | |
Ladd Hoffman | 77ae33ce5a | |
Ladd Hoffman | 68d04117c9 | |
Ladd Hoffman | ff7d6134f1 | |
Ladd Hoffman | 43462e84ea |
|
@ -1,30 +0,0 @@
|
|||
workflow:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
|
||||
pages:
|
||||
stage: deploy
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "main"
|
||||
script:
|
||||
- mkdir public
|
||||
- cp -r forum-network/src/* public/
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
|
||||
artifacts:
|
||||
stage: deploy
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH != "main"'
|
||||
script:
|
||||
- mkdir public
|
||||
- cp -r forum-network/src/* public/
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
environment:
|
||||
name: "$CI_COMMIT_BRANCH $CI_JOB_NAME"
|
||||
url: "$CI_SERVER_PROTOCOL://$CI_PROJECT_ROOT_NAMESPACE.$CI_PAGES_DOMAIN/-/$CI_PROJECT_NAME/-/jobs/$CI_JOB_ID/artifacts/public/index.html"
|
||||
variables:
|
||||
PUBLIC_URL: "/-/$CI_PROJECT_NAME/-/jobs/$CI_JOB_ID/artifacts/public/index.html"
|
|
@ -1,7 +1,7 @@
|
|||
# Science Publishing DAO
|
||||
# DAO Governance Framework
|
||||
|
||||
## Subprojects
|
||||
|
||||
| Name | Description |
|
||||
| --- | --- |
|
||||
| [forum-network](./forum-network) | Javascript prototyping forum architecture |
|
||||
| [semantic-scholar-client](./semantic-scholar-client) | Rust utility for reading data from the [Semantic Scholar API](https://api.semanticscholar.org/api-docs) |
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
mocha: true,
|
||||
},
|
||||
extends: ['airbnb-base'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.test.js'],
|
||||
rules: {
|
||||
'no-unused-expressions': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: [
|
||||
'import',
|
||||
'html',
|
||||
],
|
||||
rules: {
|
||||
'import/extensions': ['error', 'always'],
|
||||
'import/prefer-default-export': ['off'],
|
||||
'import/no-unresolved': ['error', { ignore: ['^http'] }],
|
||||
'import/no-absolute-path': ['off'],
|
||||
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
'max-classes-per-file': ['off'],
|
||||
'no-param-reassign': ['off'],
|
||||
'no-plusplus': ['off'],
|
||||
'no-restricted-syntax': ['off'],
|
||||
'max-len': ['warn', 120],
|
||||
'no-console': ['off'],
|
||||
'no-return-assign': ['off'],
|
||||
'no-multi-assign': ['off'],
|
||||
'no-constant-condition': ['off'],
|
||||
'no-await-in-loop': ['off'],
|
||||
},
|
||||
globals: {
|
||||
_: 'readonly',
|
||||
chai: 'readonly',
|
||||
sinon: 'readonly',
|
||||
sinonChai: 'readonly',
|
||||
should: 'readonly',
|
||||
},
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
ssl/
|
||||
node_modules/
|
||||
git/
|
|
@ -1,30 +0,0 @@
|
|||
We've considered implementing this validation pool + forum structure as smart contracts.
|
||||
However, we expect that such contracts would be expensive to run, because the recursive algorithm for distributing reputation via the forum will incur a lot of computation, consuming a lot of gas.
|
||||
|
||||
Can we bake this reputation algorithm into the core protocol of our blockchain?
|
||||
|
||||
The structure seems to be similar to proof-of-stake. A big difference is that what is staked and awarded is reputation rather than currency.
|
||||
The idea with reputation is that it entitles you to a proportional share of revenue earned by the network.
|
||||
So what does that look like in this context?
|
||||
|
||||
Let's say we are extending Ethereum. ETH would continue to be the currency that users must spend in order to execute transactions.
|
||||
So when a user wants to execute a transaction, they must pay a fee.
|
||||
A portion of this fee could then be distributed to reputation holders.
|
||||
|
||||
- https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/
|
||||
- https://ethereum.org/en/developers/docs/nodes-and-clients/
|
||||
|
||||
---
|
||||
|
||||
execution client
|
||||
execution gossip network
|
||||
|
||||
consensus client
|
||||
consensus gossip network
|
||||
|
||||
---
|
||||
|
||||
cardano -- "dynamic availability"?
|
||||
staking pools -- does it make sense with reputation?
|
||||
what about for governance voting --
|
||||
do we want a representative republic or a democracy?
|
|
@ -1,43 +0,0 @@
|
|||
# Primary
|
||||
|
||||
## Forum
|
||||
|
||||
## ValidationPool
|
||||
|
||||
## ReputationToken
|
||||
|
||||
## WDAG
|
||||
|
||||
# Secondary
|
||||
|
||||
## Availability
|
||||
|
||||
## Business
|
||||
|
||||
## ERC721
|
||||
|
||||
## Expert
|
||||
|
||||
## Bench
|
||||
|
||||
# Tertiary
|
||||
|
||||
## Actor
|
||||
|
||||
## Action
|
||||
|
||||
## Scene
|
||||
|
||||
# To Explore
|
||||
|
||||
## Exchange
|
||||
|
||||
## Storage
|
||||
|
||||
## Network
|
||||
|
||||
## Wallet
|
||||
|
||||
## Agent/UI
|
||||
|
||||
## BlockConsensus
|
|
@ -1,35 +0,0 @@
|
|||
A DAO is a group of cooperating entities.
|
||||
|
||||
If we're running our own network, it probably makes sense to consider nodes as the participants.
|
||||
|
||||
If we're running as smart contracts, it probably makes sense to consider individual addresses as the participants.
|
||||
|
||||
These schemes overlap, since both involve asymmetric keys.
|
||||
|
||||
Each node must validate the work of the other nodes
|
||||
|
||||
Our protocol will be a peer protocol, and will rely on signatures.
|
||||
|
||||
Therefore we arrive at a requirement for nodes: they must be physically secured so that private keys are protected.
|
||||
|
||||
We also arrive at a requirement for our network protocol: It must be possible to sign messages and verify message signatures against known public keys.
|
||||
|
||||
The network protocol MAY support asking peers about other peers / telling other peers about peers.
|
||||
|
||||
IF we support this IT SHALL BE linked with each node's reputation.
|
||||
|
||||
CAN WE SAY that each node MUST maintain A VIEW of THE ENTIRE / (THE CURRENT) / (ALL / CURRENT) HASHES / MERKLE TREE / -- World state, History
|
||||
|
||||
CAN WE GET AWAY WITH ONLY SAYING that each node maintains its own view.
|
||||
|
||||
WHAT is our protocol for evaluating the perspectives offered by peers?
|
||||
|
||||
- If one node perceives consensus among many others, that may sway their opinion.
|
||||
|
||||
- There may be opportunity during "informal voting" / non-binding validation pools (low tokenLossRatio) to gather this sort of information.
|
||||
|
||||
- If there is exact agreement, we have a very efficient case.
|
||||
|
||||
- If there is the HOPE of exact agreement, mistakes and attacks can be costly
|
||||
|
||||
- If there is an EXPECTATION of exact agreement, there must be externalities supporting that agreement, i.e. a common protocol and governance of that protocol.
|
|
@ -1,12 +0,0 @@
|
|||
In physics, Energy per unit of time is Power.
|
||||
Energy is in the same units as Work, Potential, Heat, Free Energy,
|
||||
|
||||
We've talked about the "power" of a post regarding the effects it has on other posts.
|
||||
|
||||
The mechanism of a post exerting its effect also includes the validation pool, which has a duration.
|
||||
|
||||
Effective power can be considered as a flow rate of posts; (value per post) / (duration of each post)
|
||||
|
||||
Internal energy is similar to Forum total value / DAO total reputation
|
||||
|
||||
Total available reputation is similar to thermodynamic free energy
|
|
@ -1,99 +0,0 @@
|
|||
# Challenges
|
||||
|
||||
- Receiving payments
|
||||
- Distributing payments to participants
|
||||
- Computing updates to forum graph
|
||||
|
||||
---
|
||||
|
||||
# Receiving payments
|
||||
|
||||
Business SC will need to implement a financial model.
|
||||
|
||||
---
|
||||
|
||||
# Excerpts from DeSciPubDAOArchit22July19PrintCut.pdf
|
||||
|
||||
> With today’s prices, however, we will begin by programming this all off-chain and simplify the reputation tokens to be less dynamic in their evaluation. Next iteration improves the decentralization commensurate with practical realities.
|
||||
|
||||
---
|
||||
|
||||
# Validation pool termination
|
||||
|
||||
How do we want to handle this?
|
||||
The validation pool specifies a duration.
|
||||
We don't want to compute results until the end of this duration.
|
||||
We're currently supporting anonymous voting.
|
||||
With anonymous voting, we need to wait until the end of the vote duration,
|
||||
and then have a separate interval in which voters reveal their identities.
|
||||
For now, we can let anonymous voters reveal their identities at any time
|
||||
|
||||
---
|
||||
|
||||
Bench.totalReputation is a very important quantity, isn't it? Basically determines inflation for reputation.
|
||||
|
||||
---
|
||||
|
||||
Should availability registration encumber reputation?
|
||||
|
||||
---
|
||||
|
||||
- Is a particular availability stake amount required?
|
||||
|
||||
Currently we support updating the staked amount.
|
||||
Seems like a soft protocol thing.
|
||||
A given DAO can have a formula for deciding appropriate amounts.
|
||||
|
||||
---
|
||||
|
||||
The following was a code comment on `Business.submitRequest(fee, ...)`:
|
||||
|
||||
> Fee should be held in escrow.
|
||||
> That means there should be specific conditions under which the fee will be refunded.
|
||||
> That means the submission should include some time value to indicate when it expires.
|
||||
> There could be separate thresholds to indicate the earliest that the job may be cancelled,
|
||||
> and the time at which the job will be automatically cancelled.
|
||||
|
||||
# Implementing forum
|
||||
|
||||
Does the following make sense?
|
||||
We will link the forum to the bench
|
||||
An author of a forum post /_ ? is always? can be? _/ a reputation holder.
|
||||
This is what we call a expert. Let's update that terminology to be `reputationHolder`.
|
||||
That's too long, though. Let's rename it to `expert`.
|
||||
So we want to aim for the situation where the author of a forum post is an expert.
|
||||
For now let's try thinking of them as experts no matter what;
|
||||
The strength of their expertise is meant to be represented by reputation tokens.
|
||||
So each reputation token must be a contract.
|
||||
Minting a reputation token means to construct an instance of such a contract.
|
||||
The reputation contract then has its own lifecycle.
|
||||
We can support dynamic reevaluation if the reputation contract
|
||||
|
||||
- has an interface that allows (securely) updating
|
||||
- Define secure :: passes validation pool
|
||||
- How shall it know the operation is occurring as part of an "official" validation pool?
|
||||
It can verify a signature...
|
||||
|
||||
---
|
||||
|
||||
Tokens staked for and against a post.
|
||||
|
||||
---
|
||||
|
||||
Token loss ratio
|
||||
|
||||
---
|
||||
|
||||
parameter q_4 -- what is c_n?
|
||||
|
||||
---
|
||||
|
||||
what is reputation?
|
||||
valuable evidence that you're going to do what you say you'll do in the future
|
||||
|
||||
---
|
||||
|
||||
for now, combine c2 and c3
|
||||
|
||||
validation pool should compute rewards for author,
|
||||
then send that to the forum to distribute.
|
|
@ -1,37 +0,0 @@
|
|||
The communication protocol(s) among network nodes
|
||||
Each communication protocol among network nodes
|
||||
has its own purpose
|
||||
has its own assumptions, expectations, requirements, constraints
|
||||
|
||||
I think it makes sense to identify the constraints for our protocols.
|
||||
|
||||
We need the general public to be able to reliably
|
||||
|
||||
- Query information about the reputation WDAG
|
||||
- Submit requests and fees for work
|
||||
- Obtain the products of the work submitted by forum experts
|
||||
|
||||
Suppose we want only the requestor to be able to access a given work product.
|
||||
(Why might we want this?)
|
||||
Then the (soft) protocol for reviewing the work product would consist of
|
||||
validating a signature by the requestor, attesting to their acceptance of the work product.
|
||||
|
||||
Alternatively access could be permitted to some group, such as reputation holders (a.k.a. experts).
|
||||
|
||||
Otherwise, for maximum utility, we would want to make the work products available indefinitely, as valuable artifacts.
|
||||
Value here can be equated to the expected fees that the work products will help attract, which can in turn be equated to
|
||||
reputation awarded to authors and reviewers of the work products.
|
||||
|
||||
Thus, the work of making the artifacts available must be funded.
|
||||
|
||||
The work of participating in a gossip / forum node consensus protocol and validating forum chain blocks must also be funded.
|
||||
|
||||
Suppose we have a storage contract.
|
||||
|
||||
- There can be a market for specific pledges of storage.
|
||||
- buy: (amount, duration, price)
|
||||
- sell: (amount, duration, price)
|
||||
- Governance: Management of storage price
|
||||
- may negotiate via loose -> tight binding traversal forum post sequence
|
||||
- reputation in accordance with majority opinions on price parameters
|
||||
- Verification of storage must occur by (randomly) querying the storage nodes and validating their responses.
|
|
@ -1,52 +0,0 @@
|
|||
Reputation Tokens
|
||||
|
||||
Minting
|
||||
|
||||
Suppose it's possible to mint a reputation token.
|
||||
Say we have a contract that keeps track of all the reputation tokens.
|
||||
Suppose the reputation contract implements ERC720 (NFT).
|
||||
Assume a validation pool always corresponds to a post.
|
||||
A single token could be minted for each validation pool.
|
||||
That token could be subdivided so that each winning voter gets some.
|
||||
Perhaps voters get tokens that are specifically identifiable as governance reputation tokens.
|
||||
Then the main token can be awarded to the post author.
|
||||
Each token should carry a specific value.
|
||||
The forum will update the value of the token for the post author as well as posts affected by its chain of references.
|
||||
|
||||
Then, when participants wish to stake reputation (for voting or for availability),
|
||||
they must specify the amount and the token address which carries that reputation.
|
||||
The token should probably then lock that reputation, preventing it from being staked concurrently for another purpose.
|
||||
|
||||
Perhaps our interface can allow staking reputation from multiple tokens at the same time.
|
||||
And/or we can provide a mechanism for consolidating tokens.
|
||||
|
||||
Or maybe, if reputation is staked via a given token, then the reputation awards should go to that same token.
|
||||
In that case, when should new tokens be minted?
|
||||
|
||||
Maybe a token should be minted for each validation pool, but not subdivided.
|
||||
Voter rewards can just add value to the existing tokens from which reputation was staked.
|
||||
|
||||
Maybe a new token should only be minted if the author did not provide a token from which to stake reputation on their own post.
|
||||
This supports the case of a new author earning their first reputation.
|
||||
In that case the author may need to pay a fee to buy in to the DAO.
|
||||
Or perhaps they can be sponsored by one or more existing reputation token holders.
|
||||
Existing reputation holders could grant some reputation to a new member.
|
||||
Perhaps there could be a contract that allows sponsoring a new member, such that whatever fee is given,
|
||||
that fee will automatically be repaid from the new member's earnings, before the new member starts receiving their share of earnings.
|
||||
This could be a multi-party action, or could just be a generic operation that can be performed multiple times.
|
||||
|
||||
However, this effectively allows buying reputation, which goes against the core concept of reputation as evidence of performance.
|
||||
|
||||
It could make more sense for new members to submit some sort of petition, i.e. to make a post.
|
||||
|
||||
Maybe rather than submitting fees, existing members can grant some of their own reputation to a new member, and receive some sort of compensation if the new member does well.
|
||||
|
||||
So far the only workable model seems to be the idea that a new member must submit a post along with a fee, in order to be considered, and if the post is approved, they gain their first reputation.
|
||||
The amount of this fee can be up to the applicant, and/or can be regulated by soft protocols within the DAO.
|
||||
|
||||
If this is the only scenario in which new rep tokens are minted, and from then on their value is updated as a result of each validation pool,
|
||||
then we probably want each token to store information about the history of its value.
|
||||
At a minimum this can be a list where each item includes the identifier of the corresponding validation pool, and the resulting increment/decrement of token value.
|
||||
Each validation pool can then keep a record of the reputation staked by each voter, and the identifier of the corresponding post.
|
||||
|
||||
---
|
|
@ -1,5 +0,0 @@
|
|||
expert Expert1
|
||||
expert Expert2
|
||||
forum Forum
|
||||
|
||||
source -- action --> destination
|
|
@ -1,4 +0,0 @@
|
|||
Possible statements
|
||||
|
||||
- It is what I would have done
|
||||
- It is consistent with what I (would) have done
|
|
@ -1,89 +0,0 @@
|
|||
This system is meant to represent a body of experts receiving fees to perform work.
|
||||
Experts register their availability to receive work via the availability contract.
|
||||
Request and associated fees are sumbitted via the business contract.
|
||||
Evidence of the work performed is submitted as a post to the forum.
|
||||
A successful validation pool ratifies the post.
|
||||
Reputation is minted and distributed.
|
||||
Fees are distributed.
|
||||
|
||||
What if we want the work to be block production for a blockchain?
|
||||
Then to perform this work, an expert will need to participate in a communications network
|
||||
such that they can confidently arrive at a majority view of each block.
|
||||
Or else must at least be able to attest that a proposed block is valid,
|
||||
meaning that it
|
||||
|
||||
- does not conflict with what the node believes to be the majority view
|
||||
- includes what the node believes must be included according to the majority view
|
||||
note that with this scheme there be muliple possible valid proposed blocks.
|
||||
|
||||
In any case, block production will require some form of consensus protocol. (BFT).
|
||||
|
||||
We have to define an algorithm from the perspective of a single node.
|
||||
|
||||
That node will need to make certain assumptions. Let us identify those assumptions.
|
||||
|
||||
- continuity guarantees?
|
||||
- sender identifiability?
|
||||
- it needs to bo possible to verify an asymmetric signature
|
||||
|
||||
This leads us to the storage requirements for a node.
|
||||
|
||||
For a node to exist, it must be capable of at least some temporal continuity.
|
||||
That's what distinguishes a node from a client.
|
||||
|
||||
We want our protocol to involve performing certain kinds of work.
|
||||
|
||||
- Block production
|
||||
- Messaging protocol
|
||||
- In-memory storage
|
||||
- Queryable history
|
||||
- Fast record storage
|
||||
- Archival record storage
|
||||
|
||||
What is common among these?
|
||||
|
||||
- Availability stake represents commitment to perform the specified work
|
||||
- Peers must validate the work product via validation pool
|
||||
|
||||
How can we adapt the following concepts?
|
||||
|
||||
- Business contract interfacing with availability contract
|
||||
- Reputation is minted via validation pools
|
||||
- Reputation is rewarded to validation pool winners
|
||||
- Reputation is awarded to a post in the forum
|
||||
- Reputation is propagated via citations
|
||||
|
||||
In a messaging system, the work is
|
||||
|
||||
- Listening for messages
|
||||
- Receiving messages
|
||||
- Processing messages
|
||||
- Sending messages
|
||||
- Maintaining context related to incoming messages for the duration of some operation
|
||||
- Performing computations
|
||||
|
||||
The work of verifying peers in a messaging system is
|
||||
|
||||
- Detecting invalid messages
|
||||
- Successfully defending against DoS attacks
|
||||
- Initiating validation pools?
|
||||
- Voting in validation pools?
|
||||
|
||||
The work of providing a storage service extends that of participating in a messaging system.
|
||||
- Storing data
|
||||
- Retrieving data
|
||||
|
||||
The work of verifying peers work products in a storage network is
|
||||
|
||||
- Periodically querying peers and verifying their responses
|
||||
- Participating in validation pools to police peers
|
||||
- Initiating validation pools
|
||||
- Voting in validation pools
|
||||
|
||||
Governance of a storage network includes
|
||||
tuning post and validation pool timing and other parameters.
|
||||
This can be served via the forum and validation pool,
|
||||
by having the clients agree on an interpretation of the forum,
|
||||
such that clients can derive from forum posts, at least some operating parameters.
|
||||
It may even be possible to use the forum to provide the client code itself,
|
||||
or tools for generating such code.
|
File diff suppressed because it is too large
Load Diff
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"name": "forum-network",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-plugin-html": "^7.1.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-eslint": "^15.0.1"
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
import { Action } from '../display/action.js';
|
||||
import { Actor } from '../display/actor.js';
|
||||
import { CryptoUtil } from '../util/crypto.js';
|
||||
|
||||
class Worker {
|
||||
constructor(reputationPublicKey, tokenId, stakeAmount, duration) {
|
||||
this.reputationPublicKey = reputationPublicKey;
|
||||
this.tokenId = tokenId;
|
||||
this.stakeAmount = stakeAmount;
|
||||
this.duration = duration;
|
||||
this.available = true;
|
||||
this.assignedRequestId = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Purpose: Enable staking reputation to enter the pool of workers
|
||||
*/
|
||||
export class Availability extends Actor {
|
||||
constructor(dao, name, scene) {
|
||||
super(name, scene);
|
||||
this.dao = dao;
|
||||
|
||||
this.actions = {
|
||||
assignWork: new Action('assign work', scene),
|
||||
};
|
||||
|
||||
this.workers = new Map();
|
||||
}
|
||||
|
||||
register(reputationPublicKey, { stakeAmount, tokenId, duration }) {
|
||||
// TODO: Should be signed by token owner
|
||||
this.dao.reputation.lock(tokenId, stakeAmount, duration);
|
||||
const workerId = CryptoUtil.randomUUID();
|
||||
this.workers.set(workerId, new Worker(reputationPublicKey, tokenId, stakeAmount, duration));
|
||||
return workerId;
|
||||
}
|
||||
|
||||
get availableWorkers() {
|
||||
return Array.from(this.workers.values()).filter(({ available }) => !!available);
|
||||
}
|
||||
|
||||
async assignWork(requestId) {
|
||||
const totalAmountStaked = this.availableWorkers
|
||||
.reduce((total, { stakeAmount }) => total += stakeAmount, 0);
|
||||
// Imagine all these amounts layed end-to-end along a number line.
|
||||
// To weight choice by amount staked, pick a stake by choosing a number at random
|
||||
// from within that line segment.
|
||||
const randomChoice = Math.random() * totalAmountStaked;
|
||||
let index = 0;
|
||||
let acc = 0;
|
||||
for (const { stakeAmount } of this.workers.values()) {
|
||||
acc += stakeAmount;
|
||||
if (acc >= randomChoice) {
|
||||
break;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
const worker = this.availableWorkers[index];
|
||||
worker.available = false;
|
||||
worker.assignedRequestId = requestId;
|
||||
|
||||
// TODO: Notify assignee
|
||||
return worker;
|
||||
}
|
||||
|
||||
async getAssignedWork(workerId) {
|
||||
const worker = this.workers.get(workerId);
|
||||
return worker.assignedRequestId;
|
||||
}
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
import { randomID } from '../../util.js';
|
||||
import { Action } from '../display/action.js';
|
||||
import { Actor } from '../display/actor.js';
|
||||
import { PostContent } from '../util/post-content.js';
|
||||
|
||||
class Request {
|
||||
static nextSeq = 0;
|
||||
|
||||
constructor(fee, content) {
|
||||
this.seq = this.nextSeq;
|
||||
this.nextSeq += 1;
|
||||
this.id = `req_${randomID()}`;
|
||||
this.fee = fee;
|
||||
this.content = content;
|
||||
this.worker = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Purpose: Enable fee-driven work requests, to be completed by workers from the availability pool
|
||||
*/
|
||||
export class Business extends Actor {
|
||||
constructor(dao, name, scene) {
|
||||
super(name, scene);
|
||||
this.dao = dao;
|
||||
|
||||
this.actions = {
|
||||
assignWork: new Action('assign work', scene),
|
||||
submitPost: new Action('submit post', scene),
|
||||
initiateValidationPool: new Action('initiate validation pool', scene),
|
||||
};
|
||||
|
||||
this.requests = new Map();
|
||||
}
|
||||
|
||||
async submitRequest(fee, content) {
|
||||
const request = new Request(fee, content);
|
||||
this.requests.set(request.id, request);
|
||||
await this.actions.assignWork.log(this, this.dao.availability);
|
||||
const worker = await this.dao.availability.assignWork(request.id);
|
||||
request.worker = worker;
|
||||
return request.id;
|
||||
}
|
||||
|
||||
async getRequest(requestId) {
|
||||
const request = this.requests.get(requestId);
|
||||
return request;
|
||||
}
|
||||
|
||||
async getRequests() {
|
||||
return Array.from(this.requests.values());
|
||||
}
|
||||
|
||||
async submitWork(reputationPublicKey, requestId, workEvidence, { tokenLossRatio, duration }) {
|
||||
const request = this.requests.get(requestId);
|
||||
if (!request) {
|
||||
throw new Error(`Request not found! id: ${requestId}`);
|
||||
}
|
||||
|
||||
if (reputationPublicKey !== request.worker.reputationPublicKey) {
|
||||
throw new Error('Work evidence must be submitted by the assigned worker!');
|
||||
}
|
||||
|
||||
// Create a post representing this submission.
|
||||
const post = new PostContent({
|
||||
requestId,
|
||||
workEvidence,
|
||||
});
|
||||
|
||||
const requestIndex = Array.from(this.requests.values())
|
||||
.findIndex(({ id }) => id === request.id);
|
||||
|
||||
post.setTitle(`Work Evidence ${requestIndex + 1}`);
|
||||
|
||||
await this.actions.submitPost.log(this, this.dao);
|
||||
const { id: postId } = await this.dao.forum.addPost(reputationPublicKey, post);
|
||||
|
||||
// Initiate a validation pool for this work evidence.
|
||||
await this.actions.initiateValidationPool.log(this, this.dao);
|
||||
const pool = await this.dao.initiateValidationPool({
|
||||
postId,
|
||||
fee: request.fee,
|
||||
duration,
|
||||
tokenLossRatio,
|
||||
}, {
|
||||
reputationPublicKey,
|
||||
authorStakeAmount: request.worker.stakeAmount,
|
||||
tokenId: request.worker.tokenId,
|
||||
});
|
||||
|
||||
// When the validation pool concludes,
|
||||
// reputation should be awarded and fees should be distributed.
|
||||
return pool;
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
import params from '../../params.js';
|
||||
import { Forum } from './forum.js';
|
||||
import { ReputationTokenContract } from '../contracts/reputation-token.js';
|
||||
import { ValidationPool } from './validation-pool.js';
|
||||
import { Availability } from './availability.js';
|
||||
import { Business } from './business.js';
|
||||
import { Actor } from '../display/actor.js';
|
||||
|
||||
/**
|
||||
* Purpose:
|
||||
* - Forum: Maintain a directed, acyclic, graph of positively and negatively weighted citations.
|
||||
* and the value accrued via each post and citation.
|
||||
* - Reputation: Keep track of reputation accrued to each expert
|
||||
*/
|
||||
export class DAO extends Actor {
|
||||
constructor(name, scene) {
|
||||
super(name, scene);
|
||||
|
||||
/* Contracts */
|
||||
this.forum = new Forum(this, 'Forum', scene);
|
||||
this.availability = new Availability(this, 'Availability', scene);
|
||||
this.business = new Business(this, 'Business', scene);
|
||||
this.reputation = new ReputationTokenContract();
|
||||
|
||||
/* Data */
|
||||
this.validationPools = new Map();
|
||||
this.experts = new Map();
|
||||
|
||||
this.actions = {
|
||||
};
|
||||
}
|
||||
|
||||
listValidationPools() {
|
||||
Array.from(this.validationPools.values());
|
||||
}
|
||||
|
||||
listActiveVoters() {
|
||||
return Array.from(this.experts.values()).filter((voter) => {
|
||||
const hasVoted = !!voter.dateLastVote;
|
||||
const withinThreshold = !params.activeVoterThreshold
|
||||
|| new Date() - voter.dateLastVote >= params.activeVoterThreshold;
|
||||
return hasVoted && withinThreshold;
|
||||
});
|
||||
}
|
||||
|
||||
getActiveReputation() {
|
||||
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))
|
||||
.reduce((acc, cur) => (acc += cur), 0);
|
||||
}
|
||||
|
||||
async initiateValidationPool(poolOptions, stakeOptions) {
|
||||
const validationPoolNumber = this.validationPools.size + 1;
|
||||
const name = `Pool${validationPoolNumber}`;
|
||||
const pool = new ValidationPool(this, poolOptions, name, this.scene);
|
||||
this.validationPools.set(pool.id, pool);
|
||||
|
||||
if (stakeOptions) {
|
||||
const { reputationPublicKey, tokenId, authorStakeAmount } = stakeOptions;
|
||||
await pool.stake(reputationPublicKey, {
|
||||
tokenId,
|
||||
position: true,
|
||||
amount: authorStakeAmount,
|
||||
});
|
||||
}
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
async submitPost(reputationPublicKey, postContent) {
|
||||
const post = await this.forum.addPost(reputationPublicKey, postContent);
|
||||
return post.id;
|
||||
}
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
import { Action } from '../display/action.js';
|
||||
import { PostMessage } from '../forum-network/message.js';
|
||||
import { CryptoUtil } from '../util/crypto.js';
|
||||
import { ReputationHolder } from './reputation-holder.js';
|
||||
|
||||
export class Expert extends ReputationHolder {
|
||||
constructor(dao, name, scene) {
|
||||
super(name, scene);
|
||||
this.dao = dao;
|
||||
this.actions = {
|
||||
submitPostViaNetwork: new Action('submit post via network', scene),
|
||||
submitPost: new Action('submit post', scene),
|
||||
initiateValidationPool: new Action('initiate validation pool', scene),
|
||||
stake: new Action('stake on post', scene),
|
||||
registerAvailability: new Action('register availability', scene),
|
||||
getAssignedWork: new Action('get assigned work', scene),
|
||||
submitWork: new Action('submit work evidence', scene),
|
||||
};
|
||||
this.validationPools = new Map();
|
||||
this.tokens = [];
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.reputationKey = await CryptoUtil.generateAsymmetricKey();
|
||||
// this.reputationPublicKey = await CryptoUtil.exportKey(this.reputationKey.publicKey);
|
||||
this.reputationPublicKey = this.name;
|
||||
this.status.set('Initialized');
|
||||
return this;
|
||||
}
|
||||
|
||||
async submitPostViaNetwork(forumNode, post, stake) {
|
||||
// TODO: Include fee
|
||||
const postMessage = new PostMessage({ post, stake });
|
||||
await postMessage.sign(this.reputationKey);
|
||||
await this.actions.submitPostViaNetwork.log(this, forumNode);
|
||||
// For now, directly call forumNode.receiveMessage();
|
||||
await forumNode.receiveMessage(JSON.stringify(postMessage.toJSON()));
|
||||
}
|
||||
|
||||
async submitPostWithFee(postContent, poolOptions) {
|
||||
const post = await this.dao.forum.addPost(this.reputationPublicKey, postContent);
|
||||
await this.actions.submitPost.log(this, post);
|
||||
const postId = post.id;
|
||||
const pool = await this.initiateValidationPool({ ...poolOptions, postId });
|
||||
this.tokens.push(pool.tokenId);
|
||||
return { postId, pool };
|
||||
}
|
||||
|
||||
async initiateValidationPool(poolOptions) {
|
||||
// For now, directly call bench.initiateValidationPool();
|
||||
poolOptions.reputationPublicKey = this.reputationPublicKey;
|
||||
const pool = await this.dao.initiateValidationPool(poolOptions);
|
||||
this.tokens.push(pool.tokenId);
|
||||
this.validationPools.set(pool.id, poolOptions);
|
||||
await this.actions.initiateValidationPool.log(
|
||||
this,
|
||||
pool,
|
||||
`(fee: ${poolOptions.fee}, stake: ${poolOptions.authorStakeAmount ?? 0})`,
|
||||
);
|
||||
return pool;
|
||||
}
|
||||
|
||||
async stake(validationPool, {
|
||||
position, amount, lockingTime,
|
||||
}) {
|
||||
// TODO: encrypt stake
|
||||
// TODO: sign message
|
||||
await this.actions.stake.log(
|
||||
this,
|
||||
validationPool,
|
||||
`(${position ? 'for' : 'against'}, stake: ${amount})`,
|
||||
);
|
||||
return validationPool.stake(this.reputationPublicKey, {
|
||||
position, amount, lockingTime, tokenId: this.tokens[0],
|
||||
});
|
||||
}
|
||||
|
||||
async registerAvailability(stakeAmount, duration) {
|
||||
await this.actions.registerAvailability.log(
|
||||
this,
|
||||
this.dao.availability,
|
||||
`(stake: ${stakeAmount}, duration: ${duration})`,
|
||||
);
|
||||
this.workerId = await this.dao.availability.register(this.reputationPublicKey, {
|
||||
stakeAmount,
|
||||
tokenId: this.tokens[0],
|
||||
duration,
|
||||
});
|
||||
}
|
||||
|
||||
async getAssignedWork() {
|
||||
const requestId = await this.dao.availability.getAssignedWork(this.workerId);
|
||||
const request = await this.dao.business.getRequest(requestId);
|
||||
return request;
|
||||
}
|
||||
|
||||
async submitWork(requestId, evidence, { tokenLossRatio, duration }) {
|
||||
await this.actions.submitWork.log(this, this.dao.business);
|
||||
return this.dao.business.submitWork(this.reputationPublicKey, requestId, evidence, { tokenLossRatio, duration });
|
||||
}
|
||||
}
|
|
@ -1,193 +0,0 @@
|
|||
import { WDAG } from '../supporting/wdag.js';
|
||||
import { Action } from '../display/action.js';
|
||||
import params from '../../params.js';
|
||||
import { ReputationHolder } from './reputation-holder.js';
|
||||
import { displayNumber, EPSILON } from '../../util.js';
|
||||
import { Post } from './post.js';
|
||||
|
||||
const CITATION = 'citation';
|
||||
const BALANCE = 'balance';
|
||||
|
||||
/**
|
||||
* Purpose:
|
||||
* - Forum: Maintain a directed, acyclic, graph of positively and negatively weighted citations.
|
||||
* and the value accrued via each post and citation.
|
||||
*/
|
||||
export class Forum extends ReputationHolder {
|
||||
constructor(dao, name, scene) {
|
||||
super(name, scene);
|
||||
this.dao = dao;
|
||||
this.id = this.reputationPublicKey;
|
||||
this.posts = new WDAG(scene);
|
||||
this.actions = {
|
||||
propagate: new Action('propagate', scene),
|
||||
confirm: new Action('confirm', scene),
|
||||
transfer: new Action('transfer', scene),
|
||||
};
|
||||
}
|
||||
|
||||
async addPost(authorId, postContent) {
|
||||
const post = new Post(this, authorId, postContent);
|
||||
this.posts.addVertex(post.id, post, post.getLabel());
|
||||
for (const { postId: citedPostId, weight } of post.citations) {
|
||||
this.posts.addEdge(CITATION, post.id, citedPostId, weight);
|
||||
}
|
||||
return post;
|
||||
}
|
||||
|
||||
getPost(postId) {
|
||||
return this.posts.getVertexData(postId);
|
||||
}
|
||||
|
||||
getPosts() {
|
||||
return this.posts.getVerticesData();
|
||||
}
|
||||
|
||||
async setPostValue(post, value) {
|
||||
post.value = value;
|
||||
await post.setValue('value', value);
|
||||
this.posts.setVertexLabel(post.id, post.getLabel());
|
||||
}
|
||||
|
||||
getTotalValue() {
|
||||
return this.getPosts().reduce((total, { value }) => total += value, 0);
|
||||
}
|
||||
|
||||
async onValidate({
|
||||
pool, postId, tokenId,
|
||||
}) {
|
||||
const initialValue = this.dao.reputation.valueOf(tokenId);
|
||||
const postVertex = this.posts.getVertex(postId);
|
||||
const post = postVertex.data;
|
||||
post.setStatus('Validated');
|
||||
post.initialValue = initialValue;
|
||||
this.posts.setVertexLabel(post.id, post.getLabel());
|
||||
|
||||
// Store a reference to the reputation token associated with this post,
|
||||
// so that its value can be updated by future validated posts.
|
||||
post.tokenId = tokenId;
|
||||
|
||||
const rewardsAccumulator = new Map();
|
||||
|
||||
// Compute rewards
|
||||
await this.propagateValue(
|
||||
{ to: postVertex, from: { data: pool } },
|
||||
{ rewardsAccumulator, increment: initialValue },
|
||||
);
|
||||
|
||||
// Apply computed rewards to update values of tokens
|
||||
for (const [id, value] of rewardsAccumulator) {
|
||||
if (value < 0) {
|
||||
this.dao.reputation.transferValueFrom(id, post.tokenId, -value);
|
||||
} else {
|
||||
this.dao.reputation.transferValueFrom(post.tokenId, id, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer ownership of the minted/staked token, from the posts to the post author
|
||||
this.dao.reputation.transfer(this.id, post.authorPublicKey, post.tokenId);
|
||||
// const toActor = this.scene?.findActor((actor) => actor.reputationPublicKey === post.authorPublicKey);
|
||||
// const value = this.dao.reputation.valueOf(post.tokenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Edge} edge
|
||||
* @param {Object} opaqueData
|
||||
*/
|
||||
async propagateValue(edge, {
|
||||
rewardsAccumulator, increment, depth = 0, initialNegative = false,
|
||||
}) {
|
||||
const postVertex = edge.to;
|
||||
const post = postVertex?.data;
|
||||
this.actions.propagate.log(edge.from.data, post, `(${increment})`);
|
||||
|
||||
if (!!params.referenceChainLimit && depth > params.referenceChainLimit) {
|
||||
this.actions.propagate.log(
|
||||
edge.from.data,
|
||||
post,
|
||||
`referenceChainLimit (${params.referenceChainLimit}) reached`,
|
||||
null,
|
||||
'-x',
|
||||
);
|
||||
return increment;
|
||||
}
|
||||
|
||||
console.log('propagateValue start', {
|
||||
from: edge.from.id ?? edge.from,
|
||||
to: edge.to.id,
|
||||
depth,
|
||||
value: post.value,
|
||||
increment,
|
||||
initialNegative,
|
||||
});
|
||||
|
||||
const propagate = async (positive) => {
|
||||
let totalOutboundAmount = 0;
|
||||
const citationEdges = postVertex.getEdges(CITATION, true)
|
||||
.filter(({ weight }) => (positive ? weight > 0 : weight < 0));
|
||||
for (const citationEdge of citationEdges) {
|
||||
const { weight } = citationEdge;
|
||||
let outboundAmount = weight * increment;
|
||||
const balanceToOutbound = this.posts.getEdgeWeight(BALANCE, citationEdge.from, citationEdge.to) ?? 0;
|
||||
// We need to ensure that we at most undo the prior effects of this post
|
||||
if (initialNegative) {
|
||||
outboundAmount = outboundAmount < 0
|
||||
? Math.max(outboundAmount, -balanceToOutbound)
|
||||
: Math.min(outboundAmount, -balanceToOutbound);
|
||||
}
|
||||
if (Math.abs(outboundAmount) > EPSILON) {
|
||||
const refundFromOutbound = await this.propagateValue(citationEdge, {
|
||||
rewardsAccumulator,
|
||||
increment: outboundAmount,
|
||||
depth: depth + 1,
|
||||
initialNegative: initialNegative || (depth === 0 && outboundAmount < 0),
|
||||
});
|
||||
|
||||
outboundAmount -= refundFromOutbound;
|
||||
this.posts.setEdgeWeight(BALANCE, citationEdge.from, citationEdge.to, balanceToOutbound + outboundAmount);
|
||||
totalOutboundAmount += outboundAmount;
|
||||
|
||||
this.actions.confirm.log(
|
||||
citationEdge.to.data,
|
||||
citationEdge.from.data,
|
||||
`(refund: ${displayNumber(refundFromOutbound)}, leach: ${outboundAmount * params.leachingValue})`,
|
||||
undefined,
|
||||
'-->>',
|
||||
);
|
||||
}
|
||||
}
|
||||
return totalOutboundAmount;
|
||||
};
|
||||
|
||||
// First, leach value via negative citations
|
||||
const totalLeachingAmount = await propagate(false);
|
||||
increment -= totalLeachingAmount * params.leachingValue;
|
||||
|
||||
// Now propagate value via positive citations
|
||||
const totalDonationAmount = await propagate(true);
|
||||
increment -= totalDonationAmount * params.leachingValue;
|
||||
|
||||
// Apply the remaining increment to the present post
|
||||
const rawNewValue = post.value + increment;
|
||||
const newValue = Math.max(0, rawNewValue);
|
||||
const appliedIncrement = newValue - post.value;
|
||||
const refundToInbound = increment - appliedIncrement;
|
||||
|
||||
console.log('propagateValue end', {
|
||||
depth,
|
||||
increment,
|
||||
rawNewValue,
|
||||
newValue,
|
||||
appliedIncrement,
|
||||
refundToInbound,
|
||||
});
|
||||
|
||||
// Award reputation to post author
|
||||
rewardsAccumulator.set(post.tokenId, appliedIncrement);
|
||||
|
||||
// Increment the value of the post
|
||||
await this.setPostValue(post, newValue);
|
||||
|
||||
return refundToInbound;
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
import { Actor } from '../display/actor.js';
|
||||
import { displayNumber } from '../../util.js';
|
||||
import params from '../../params.js';
|
||||
|
||||
export class Post extends Actor {
|
||||
constructor(forum, authorPublicKey, postContent) {
|
||||
const index = forum.posts.countVertices();
|
||||
const name = `Post${index + 1}`;
|
||||
super(name, forum.scene);
|
||||
this.id = postContent.id ?? name;
|
||||
this.authorPublicKey = authorPublicKey;
|
||||
this.value = 0;
|
||||
this.initialValue = 0;
|
||||
this.citations = postContent.citations;
|
||||
this.title = postContent.title;
|
||||
const leachingTotal = this.citations
|
||||
.filter(({ weight }) => weight < 0)
|
||||
.reduce((total, { weight }) => total += -weight, 0);
|
||||
const donationTotal = this.citations
|
||||
.filter(({ weight }) => weight > 0)
|
||||
.reduce((total, { weight }) => total += weight, 0);
|
||||
if (leachingTotal > params.revaluationLimit) {
|
||||
throw new Error('Post leaching total exceeds revaluation limit '
|
||||
+ `(${leachingTotal} > ${params.revaluationLimit})`);
|
||||
}
|
||||
if (donationTotal > params.revaluationLimit) {
|
||||
throw new Error('Post donation total exceeds revaluation limit '
|
||||
+ `(${donationTotal} > ${params.revaluationLimit})`);
|
||||
}
|
||||
if (this.citations.some(({ weight }) => Math.abs(weight) > params.revaluationLimit)) {
|
||||
throw new Error(`Each citation magnitude must not exceed revaluation limit ${params.revaluationLimit}`);
|
||||
}
|
||||
}
|
||||
|
||||
getLabel() {
|
||||
return `${this.name}
|
||||
<table><tr>
|
||||
<td>initial</td>
|
||||
<td>${displayNumber(this.initialValue)}</td>
|
||||
</tr><tr>
|
||||
<td>value</td>
|
||||
<td>${displayNumber(this.value)}</td>
|
||||
</tr></table>`
|
||||
.replaceAll(/\n\s*/g, '');
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import { Action } from '../display/action.js';
|
||||
import { Actor } from '../display/actor.js';
|
||||
|
||||
export class Public extends Actor {
|
||||
constructor(name, scene) {
|
||||
super(name, scene);
|
||||
this.actions = {
|
||||
submitRequest: new Action('submit work request', scene),
|
||||
};
|
||||
}
|
||||
|
||||
async submitRequest(business, { fee }, content) {
|
||||
this.actions.submitRequest.log(this, business, `(fee: ${fee})`);
|
||||
return business.submitRequest(fee, content);
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { randomID } from '../../util.js';
|
||||
import { Actor } from '../display/actor.js';
|
||||
|
||||
export class ReputationHolder extends Actor {
|
||||
constructor(name, scene) {
|
||||
super(name, scene);
|
||||
this.reputationPublicKey = `${name}_${randomID()}`;
|
||||
}
|
||||
}
|
|
@ -1,293 +0,0 @@
|
|||
import { ReputationHolder } from './reputation-holder.js';
|
||||
import { Stake } from '../supporting/stake.js';
|
||||
import { Voter } from '../supporting/voter.js';
|
||||
import params from '../../params.js';
|
||||
import { Action } from '../display/action.js';
|
||||
import { displayNumber } from '../../util.js';
|
||||
|
||||
const ValidationPoolStates = Object.freeze({
|
||||
OPEN: 'OPEN',
|
||||
CLOSED: 'CLOSED',
|
||||
RESOLVED: 'RESOLVED',
|
||||
});
|
||||
|
||||
/**
|
||||
* Purpose: Enable voting
|
||||
*/
|
||||
export class ValidationPool extends ReputationHolder {
|
||||
constructor(
|
||||
dao,
|
||||
{
|
||||
postId,
|
||||
reputationPublicKey,
|
||||
fee,
|
||||
duration,
|
||||
tokenLossRatio,
|
||||
contentiousDebate = false,
|
||||
},
|
||||
name,
|
||||
scene,
|
||||
) {
|
||||
super(name, scene);
|
||||
this.id = this.reputationPublicKey;
|
||||
|
||||
this.actions = {
|
||||
reward: new Action('reward', scene),
|
||||
transfer: new Action('transfer', scene),
|
||||
mint: new Action('mint', scene),
|
||||
};
|
||||
|
||||
// If contentiousDebate = true, we will follow the progression defined by getTokenLossRatio()
|
||||
if (
|
||||
!contentiousDebate
|
||||
&& (tokenLossRatio < 0
|
||||
|| tokenLossRatio > 1
|
||||
|| [null, undefined].includes(tokenLossRatio))
|
||||
) {
|
||||
throw new Error(
|
||||
`Token loss ratio must be in the range [0, 1]; got ${tokenLossRatio}`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
duration < params.voteDuration.min
|
||||
|| (params.voteDuration.max && duration > params.voteDuration.max)
|
||||
|| [null, undefined].includes(duration)
|
||||
) {
|
||||
throw new Error(
|
||||
`Duration must be in the range [${params.voteDuration.min}, ${
|
||||
params.voteDuration.max ?? 'Inf'
|
||||
}]; got ${duration}`,
|
||||
);
|
||||
}
|
||||
this.dao = dao;
|
||||
this.postId = postId;
|
||||
this.state = ValidationPoolStates.OPEN;
|
||||
this.setStatus('Open');
|
||||
this.stakes = new Set();
|
||||
this.dateStart = new Date();
|
||||
this.authorReputationPublicKey = reputationPublicKey;
|
||||
this.fee = fee;
|
||||
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})`);
|
||||
|
||||
// Keep a record of voters and their votes
|
||||
const voter = this.dao.experts.get(reputationPublicKey) ?? new Voter(reputationPublicKey);
|
||||
voter.addVoteRecord(this);
|
||||
this.dao.experts.set(reputationPublicKey, voter);
|
||||
|
||||
this.activate();
|
||||
}
|
||||
|
||||
getTokenLossRatio() {
|
||||
if (!this.contentiousDebate) {
|
||||
return this.tokenLossRatio;
|
||||
}
|
||||
const elapsed = new Date() - this.dateStart;
|
||||
let stageDuration = params.contentiousDebate.period / 2;
|
||||
let stage = 0;
|
||||
let t = 0;
|
||||
while (true) {
|
||||
t += stageDuration;
|
||||
stageDuration /= 2;
|
||||
if (t > elapsed) {
|
||||
break;
|
||||
}
|
||||
stage += 1;
|
||||
if (stage >= params.contentiousDebate.stages - 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return stage / (params.contentiousDebate.stages - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
|
||||
* @param {boolean} options.excludeSystem: Whether to exclude votes cast during pool initialization
|
||||
* @returns stake[]
|
||||
*/
|
||||
getStakes(outcome, { excludeSystem }) {
|
||||
return Array.from(this.stakes.values())
|
||||
.filter(({ tokenId }) => !excludeSystem || tokenId !== this.tokenId)
|
||||
.filter(({ position }) => outcome === null || position === outcome);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
|
||||
* @returns number
|
||||
*/
|
||||
getTotalStakedOnPost(outcome) {
|
||||
return this.getStakes(outcome, { excludeSystem: false })
|
||||
.map((stake) => stake.getStakeValue())
|
||||
.reduce((acc, cur) => (acc += cur), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
|
||||
* @returns number
|
||||
*/
|
||||
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,
|
||||
}) {
|
||||
if (this.state === ValidationPoolStates.CLOSED) {
|
||||
throw new Error(`Validation pool ${this.id} is closed.`);
|
||||
}
|
||||
|
||||
if (this.duration && new Date() - this.dateStart > this.duration) {
|
||||
throw new Error(
|
||||
`Validation pool ${this.id} has expired, no new votes may be cast.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (reputationPublicKey !== this.dao.reputation.ownerOf(tokenId)) {
|
||||
throw new Error('Reputation may only be staked by its owner!');
|
||||
}
|
||||
|
||||
const stake = new Stake({
|
||||
tokenId, 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);
|
||||
|
||||
// Keep a record of voters and their votes
|
||||
if (reputationPublicKey !== this.id) {
|
||||
const voter = this.dao.experts.get(reputationPublicKey) ?? new Voter(reputationPublicKey);
|
||||
voter.addVoteRecord(this);
|
||||
this.dao.experts.set(reputationPublicKey, voter);
|
||||
|
||||
// Update computed display values
|
||||
const actor = this.scene?.findActor((a) => a.reputationPublicKey === voter.reputationPublicKey);
|
||||
await actor.computeValues();
|
||||
}
|
||||
}
|
||||
|
||||
applyTokenLocking() {
|
||||
// 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);
|
||||
// TODO: If there is an exception here, the voter may have voted incorrectly. Consider penalties.
|
||||
}
|
||||
}
|
||||
|
||||
async evaluateWinningConditions() {
|
||||
if (this.state === ValidationPoolStates.RESOLVED) {
|
||||
throw new Error('Validation pool has already been resolved!');
|
||||
}
|
||||
const elapsed = new Date() - this.dateStart;
|
||||
if (elapsed < this.duration) {
|
||||
throw new Error(`Validation pool duration has not yet elapsed! ${this.duration - elapsed} ms remaining.`);
|
||||
}
|
||||
// Now we can evaluate winning conditions
|
||||
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;
|
||||
const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation;
|
||||
|
||||
const result = {
|
||||
votePasses,
|
||||
upvoteValue,
|
||||
downvoteValue,
|
||||
};
|
||||
|
||||
if (quorumMet) {
|
||||
this.setStatus(`Resolved - ${votePasses ? 'Won' : 'Lost'}`);
|
||||
this.scene?.sequence.log(`note over ${this.name} : ${votePasses ? 'Win' : 'Lose'}`);
|
||||
this.applyTokenLocking();
|
||||
await this.distributeReputation({ votePasses });
|
||||
// TODO: distribute fees
|
||||
} else {
|
||||
this.setStatus('Resolved - Quorum not met');
|
||||
this.scene?.sequence.log(`note over ${this.name} : Quorum not met`);
|
||||
}
|
||||
|
||||
// Update computed display values
|
||||
for (const voter of this.dao.experts.values()) {
|
||||
const actor = this.scene?.findActor((a) => a.reputationPublicKey === voter.reputationPublicKey);
|
||||
if (!actor) {
|
||||
throw new Error('Actor not found!');
|
||||
}
|
||||
await actor.computeValues();
|
||||
}
|
||||
await this.dao.computeValues();
|
||||
|
||||
this.scene?.stateToTable(`validation pool ${this.name} complete`);
|
||||
|
||||
await this.deactivate();
|
||||
this.state = ValidationPoolStates.RESOLVED;
|
||||
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
|
||||
|
||||
// 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();
|
||||
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)})`);
|
||||
}
|
||||
|
||||
if (votePasses) {
|
||||
// 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)}`);
|
||||
|
||||
// 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.actions.transfer.log(this, this.dao.forum, `(${value})`);
|
||||
|
||||
// Recurse through forum to determine reputation effects
|
||||
await this.dao.forum.onValidate({
|
||||
pool: this,
|
||||
postId: this.postId,
|
||||
tokenId: this.tokenId,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('pool complete');
|
||||
}
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
/**
|
||||
* Note: Copied from openzepplin-contracts/contracts/token/ERC20/ERC20.sol
|
||||
* As of commit d59306b: Improve ERC20.decimals documentation (#3933)
|
||||
* on 2023-02-02
|
||||
* by Ladd Hoffman <laddhoffman@gmail.com>
|
||||
*
|
||||
* @dev Implementation of the {IERC20} interface.
|
||||
*
|
||||
* This implementation is agnostic to the way tokens are created. This means
|
||||
* that a supply mechanism has to be added in a derived contract using {_mint}.
|
||||
* For a generic mechanism see {ERC20PresetMinterPauser}.
|
||||
*
|
||||
* TIP: For a detailed writeup see our guide
|
||||
* https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How
|
||||
* to implement supply mechanisms].
|
||||
*
|
||||
* The default value of {decimals} is 18. To change this, you should override
|
||||
* this function so it returns a different value.
|
||||
*
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* This Javascript implementation is incomplete. It lacks the following:
|
||||
* - allowance
|
||||
* - transferFrom
|
||||
* - approve
|
||||
* - increaseAllowance
|
||||
* - decreaseAllowance
|
||||
* - _beforeTokenTransfer
|
||||
* - _afterTokenTransfer
|
||||
*/
|
||||
export class ERC20 {
|
||||
/**
|
||||
* @dev Sets the values for {name} and {symbol}.
|
||||
*
|
||||
* All two of these values are immutable: they can only be set once during
|
||||
* construction.
|
||||
* @param {string} name
|
||||
* @param {string} symbol
|
||||
*/
|
||||
constructor(name, symbol) {
|
||||
this.name = name;
|
||||
this.symbol = symbol;
|
||||
this.totalSupply = 0;
|
||||
this.balances = new Map(); // <address, number>
|
||||
this.allowances = new Map(); // <address, number>
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the number of decimals used to get its user representation.
|
||||
* For example, if `decimals` equals `2`, a balance of `505` tokens should
|
||||
* be displayed to a user as `5.05` (`505 / 10 ** 2`).
|
||||
*
|
||||
* Tokens usually opt for a value of 18, imitating the relationship between
|
||||
* Ether and Wei. This is the default value returned by this function, unless
|
||||
* it's overridden.
|
||||
*
|
||||
* NOTE: This information is only used for _display_ purposes: it in
|
||||
* no way affects any of the arithmetic of the contract, including
|
||||
* {IERC20-balanceOf} and {IERC20-transfer}.
|
||||
*/
|
||||
static decimals() {
|
||||
return 18;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC20-balanceOf}.
|
||||
*/
|
||||
balanceOf(account) {
|
||||
return this.balances.get(account);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC20-transfer}.
|
||||
*
|
||||
* Emits an {Approval} event indicating the updated allowance. This is not
|
||||
* required by the EIP. See the note at the beginning of {ERC20}.
|
||||
*
|
||||
* NOTE: Does not update the allowance if the current allowance
|
||||
* is the maximum `uint256`.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `from` and `to` cannot be the zero address.
|
||||
* - `from` must have a balance of at least `amount`.
|
||||
* - the caller must have allowance for ``from``'s tokens of at least
|
||||
* `amount`.
|
||||
*/
|
||||
transfer(from, to, amount) {
|
||||
if (!from) throw new Error('ERC20: transfer from the zero address');
|
||||
if (!to) throw new Error('ERC20: transfer to the zero address');
|
||||
|
||||
// _beforeTokenTransfer(from, to, amount);
|
||||
|
||||
const fromBalance = this.balances.get(from);
|
||||
if (fromBalance < amount) throw new Error('ERC20: transfer amount exceeds balance');
|
||||
this.balances.set(from, fromBalance - amount);
|
||||
// Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
|
||||
// decrementing then incrementing.
|
||||
this.balances.set(to, this.balances.get(to) + amount);
|
||||
|
||||
// emit Transfer(from, to, amount);
|
||||
|
||||
// _afterTokenTransfer(from, to, amount);
|
||||
}
|
||||
|
||||
/** @dev Creates `amount` tokens and assigns them to `account`, increasing
|
||||
* the total supply.
|
||||
*
|
||||
* Emits a {Transfer} event with `from` set to the zero address.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `account` cannot be the zero address.
|
||||
*/
|
||||
mint(account, amount) {
|
||||
if (!account) throw new Error('ERC20: mint to the zero address');
|
||||
|
||||
// _beforeTokenTransfer(address(0), account, amount);
|
||||
|
||||
this.totalSupply += amount;
|
||||
// Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
|
||||
this.balances.set(account, this.balances.get(account) + amount);
|
||||
// emit Transfer(address(0), account, amount);
|
||||
|
||||
// _afterTokenTransfer(address(0), account, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Destroys `amount` tokens from `account`, reducing the
|
||||
* total supply.
|
||||
*
|
||||
* Emits a {Transfer} event with `to` set to the zero address.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `account` cannot be the zero address.
|
||||
* - `account` must have at least `amount` tokens.
|
||||
*/
|
||||
burn(account, amount) {
|
||||
if (!account) throw new Error('ERC20: burn from the zero address');
|
||||
|
||||
// _beforeTokenTransfer(account, address(0), amount);
|
||||
|
||||
const accountBalance = this.balances.get(account);
|
||||
if (accountBalance < amount) throw new Error('ERC20: burn amount exceeds balance');
|
||||
this.balances.set(account, accountBalance - amount);
|
||||
// Overflow not possible: amount <= accountBalance <= totalSupply.
|
||||
this.totalSupply -= amount;
|
||||
// emit Transfer(address(0), account, amount);
|
||||
|
||||
// _afterTokenTransfer(address(0), account, amount);
|
||||
}
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
/**
|
||||
* ERC-721 Non-Fungible Token Standard
|
||||
* See https://eips.ethereum.org/EIPS/eip-721
|
||||
* and https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol
|
||||
*
|
||||
* This implementation is currently incomplete. It lacks the following:
|
||||
* - Token approvals
|
||||
* - Operator approvals
|
||||
* - Emitting events
|
||||
*/
|
||||
|
||||
export class ERC721 {
|
||||
constructor(name, symbol) {
|
||||
this.name = name;
|
||||
this.symbol = symbol;
|
||||
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.events = {
|
||||
// Transfer: (_from, _to, _tokenId) => {},
|
||||
// Approval: (_owner, _approved, _tokenId) => {},
|
||||
// ApprovalForAll: (_owner, _operator, _approved) => {},
|
||||
};
|
||||
}
|
||||
|
||||
incrementBalance(owner, increment) {
|
||||
const balance = this.balances.get(owner) ?? 0;
|
||||
this.balances.set(owner, balance + increment);
|
||||
}
|
||||
|
||||
mint(to, tokenId) {
|
||||
if (this.owners.get(tokenId)) {
|
||||
throw new Error('ERC721: token already minted');
|
||||
}
|
||||
this.incrementBalance(to, 1);
|
||||
this.owners.set(tokenId, to);
|
||||
}
|
||||
|
||||
burn(tokenId) {
|
||||
const owner = this.owners.get(tokenId);
|
||||
this.incrementBalance(owner, -1);
|
||||
this.owners.delete(tokenId);
|
||||
}
|
||||
|
||||
balanceOf(owner) {
|
||||
if (!owner) {
|
||||
throw new Error('ERC721: address zero is not a valid owner');
|
||||
}
|
||||
return this.balances.get(owner) ?? 0;
|
||||
}
|
||||
|
||||
ownerOf(tokenId) {
|
||||
const owner = this.owners.get(tokenId);
|
||||
if (!owner) {
|
||||
throw new Error(`ERC721: invalid token ID: ${tokenId}`);
|
||||
}
|
||||
return owner;
|
||||
}
|
||||
|
||||
transfer(from, to, tokenId) {
|
||||
const owner = this.owners.get(tokenId);
|
||||
if (owner !== from) {
|
||||
throw new Error('ERC721: transfer from incorrect owner');
|
||||
}
|
||||
this.incrementBalance(from, -1);
|
||||
this.incrementBalance(to, 1);
|
||||
this.owners.set(tokenId, 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.
|
||||
/// @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
|
||||
/// @return The approved address for this NFT, or the zero address if there is none
|
||||
// getApproved(_tokenId) {}
|
||||
|
||||
/// @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
|
||||
/// @return True if `_operator` is an approved operator for `_owner`, false otherwise
|
||||
// isApprovedForAll(_owner, _operator) {}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
import { ERC721 } from './erc721.js';
|
||||
|
||||
import { EPSILON, randomID } from '../../util.js';
|
||||
|
||||
class Lock {
|
||||
constructor(tokenId, amount, duration) {
|
||||
this.dateCreated = new Date();
|
||||
this.tokenId = tokenId;
|
||||
this.amount = amount;
|
||||
this.duration = duration;
|
||||
}
|
||||
}
|
||||
|
||||
export class ReputationTokenContract extends ERC721 {
|
||||
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}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
incrementValue(tokenId, increment, context) {
|
||||
const value = this.values.get(tokenId);
|
||||
const newValue = value + increment;
|
||||
const history = this.histories.get(tokenId) || [];
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
transferValueFrom(fromTokenId, toTokenId, amount) {
|
||||
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(fromTokenId);
|
||||
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);
|
||||
}
|
||||
|
||||
lock(tokenId, amount, duration) {
|
||||
const lock = new Lock(tokenId, amount, duration);
|
||||
this.locks.add(lock);
|
||||
}
|
||||
|
||||
historyOf(tokenId) {
|
||||
return this.histories.get(tokenId);
|
||||
}
|
||||
|
||||
valueOf(tokenId) {
|
||||
return this.values.get(tokenId);
|
||||
}
|
||||
|
||||
availableValueOf(tokenId) {
|
||||
const amountLocked = Array.from(this.locks.values())
|
||||
.filter(({ tokenId: lockTokenId }) => lockTokenId === tokenId)
|
||||
.filter(({ dateCreated, duration }) => new Date() - dateCreated < duration)
|
||||
.reduce((total, { amount }) => total += amount, 0);
|
||||
|
||||
return this.valueOf(tokenId) - amountLocked;
|
||||
}
|
||||
|
||||
valueOwnedBy(ownerId) {
|
||||
return Array.from(this.owners.entries())
|
||||
.filter(([__, owner]) => owner === ownerId)
|
||||
.map(([tokenId, __]) => this.valueOf(tokenId))
|
||||
.reduce((total, value) => total += value, 0);
|
||||
}
|
||||
|
||||
availableValueOwnedBy(ownerId) {
|
||||
return Array.from(this.owners.entries())
|
||||
.filter(([__, owner]) => owner === ownerId)
|
||||
.map(([tokenId, __]) => this.availableValueOf(tokenId))
|
||||
.reduce((total, value) => total += value, 0);
|
||||
}
|
||||
|
||||
getTotal() {
|
||||
return Array.from(this.values.values()).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;
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
export class Action {
|
||||
constructor(name, scene) {
|
||||
this.name = name;
|
||||
this.scene = scene;
|
||||
}
|
||||
|
||||
async log(src, dest, msg, obj, symbol = '->>') {
|
||||
await this.scene?.sequence?.log(
|
||||
`${src.name} ${symbol} ${dest.name} : ${this.name} ${msg ?? ''} ${
|
||||
JSON.stringify(obj) ?? ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
import { displayNumber } from '../../util.js';
|
||||
|
||||
export class Actor {
|
||||
constructor(name, scene) {
|
||||
if (!scene) throw new Error('An actor without a scene!');
|
||||
this.name = name;
|
||||
this.scene = scene;
|
||||
this.callbacks = new Map();
|
||||
this.status = scene.addDisplayValue(`${this.name} status`);
|
||||
this.status.set('Created');
|
||||
this.values = new Map();
|
||||
this.valueFunctions = new Map();
|
||||
this.active = 0;
|
||||
scene?.registerActor(this);
|
||||
}
|
||||
|
||||
activate() {
|
||||
this.active += 1;
|
||||
this.scene?.sequence?.activate(this.name);
|
||||
}
|
||||
|
||||
async deactivate() {
|
||||
if (!this.active) {
|
||||
throw new Error(`${this.name} is not active, can not deactivate`);
|
||||
}
|
||||
this.active -= 1;
|
||||
await this.scene?.sequence?.deactivate(this.name);
|
||||
}
|
||||
|
||||
async send(dest, action, detail) {
|
||||
await action.log(this, dest, detail ? JSON.stringify(detail) : '');
|
||||
await dest.recv(this, action, detail);
|
||||
return this;
|
||||
}
|
||||
|
||||
async recv(src, action, detail) {
|
||||
const cb = this.callbacks.get(action.name);
|
||||
if (!cb) {
|
||||
throw new Error(
|
||||
`[${this.scene?.name} actor ${this.name} does not have a callback registered for ${action.name}`,
|
||||
);
|
||||
}
|
||||
await cb(src, detail);
|
||||
return this;
|
||||
}
|
||||
|
||||
on(action, cb) {
|
||||
this.callbacks.set(action.name, cb);
|
||||
return this;
|
||||
}
|
||||
|
||||
setStatus(status) {
|
||||
this.status.set(status);
|
||||
return this;
|
||||
}
|
||||
|
||||
async addComputedValue(label, fn) {
|
||||
this.values.set(label, this.scene?.addDisplayValue(`${this.name} ${label}`));
|
||||
if (fn) {
|
||||
this.valueFunctions.set(label, fn);
|
||||
await this.computeValues();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
async setValue(label, value) {
|
||||
if (typeof value === 'function') {
|
||||
return this.addComputedValue(label, value);
|
||||
}
|
||||
const displayValue = this.values.get(label) ?? this.scene?.addDisplayValue(`${this.name} ${label}`);
|
||||
if (value !== displayValue.get()) {
|
||||
await this.scene?.sequence?.log(`note over ${this.name} : ${label} = ${displayNumber(value)}`);
|
||||
}
|
||||
displayValue.set(value);
|
||||
this.values.set(label, displayValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
async computeValues() {
|
||||
for (const [label, fn] of this.valueFunctions.entries()) {
|
||||
const value = fn();
|
||||
await this.setValue(label, value);
|
||||
}
|
||||
}
|
||||
|
||||
getValuesMap() {
|
||||
return new Map(Array.from(this.values.entries())
|
||||
.map(([key, displayValue]) => [key, {
|
||||
name: displayValue.getName(),
|
||||
value: displayValue.get(),
|
||||
}]));
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
import { DisplayValue } from './display-value.js';
|
||||
import { randomID } from '../../util.js';
|
||||
|
||||
export class Box {
|
||||
constructor(name, parentEl, elementType = 'div') {
|
||||
this.name = name;
|
||||
this.el = document.createElement(elementType);
|
||||
this.el.id = `box_${randomID()}`;
|
||||
this.el.classList.add('box');
|
||||
if (name) {
|
||||
this.el.setAttribute('box-name', name);
|
||||
}
|
||||
if (parentEl) {
|
||||
parentEl.appendChild(this.el);
|
||||
}
|
||||
}
|
||||
|
||||
flex() {
|
||||
this.addClass('flex');
|
||||
return this;
|
||||
}
|
||||
|
||||
monospace() {
|
||||
this.addClass('monospace');
|
||||
return this;
|
||||
}
|
||||
|
||||
hidden() {
|
||||
this.addClass('hidden');
|
||||
return this;
|
||||
}
|
||||
|
||||
addClass(className) {
|
||||
this.el.classList.add(className);
|
||||
return this;
|
||||
}
|
||||
|
||||
addBox(name, elementType) {
|
||||
const box = new Box(name, this.el, elementType);
|
||||
return box;
|
||||
}
|
||||
|
||||
addDisplayValue(value) {
|
||||
const box = this.addBox(value.name).flex();
|
||||
return new DisplayValue(value, box);
|
||||
}
|
||||
|
||||
setInnerHTML(html) {
|
||||
this.el.innerHTML = html;
|
||||
return this;
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.el.id;
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
import { displayNumber } from '../../util.js';
|
||||
|
||||
export class DisplayValue {
|
||||
constructor(name, box) {
|
||||
this.value = undefined;
|
||||
this.name = name;
|
||||
this.box = box;
|
||||
this.nameBox = this.box.addBox(`${this.name}-name`).addClass('name');
|
||||
this.valueBox = this.box.addBox(`${this.name}-value`).addClass('value');
|
||||
this.nameBox.setInnerHTML(this.name);
|
||||
}
|
||||
|
||||
render() {
|
||||
this.valueBox.setInnerHTML(typeof this.value === 'number' ? displayNumber(this.value, 6) : this.value);
|
||||
}
|
||||
|
||||
set(value) {
|
||||
this.value = value;
|
||||
this.render();
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { MermaidDiagram } from './mermaid.js';
|
||||
|
||||
export class Flowchart extends MermaidDiagram {
|
||||
constructor(box, logBox, direction = 'BT') {
|
||||
super(box, logBox);
|
||||
|
||||
this.log(`graph ${direction}`, false);
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
import mermaid from 'https://unpkg.com/mermaid@9.2.2/dist/mermaid.esm.min.mjs';
|
||||
import { debounce } from '../../util.js';
|
||||
|
||||
export class MermaidDiagram {
|
||||
constructor(box, logBox) {
|
||||
this.box = box;
|
||||
this.container = this.box.addBox('Container');
|
||||
this.element = this.box.addBox('Element');
|
||||
this.renderBox = this.box.addBox('Render');
|
||||
this.box.addBox('Spacer').setInnerHTML(' ');
|
||||
this.logBoxPre = logBox.el.appendChild(document.createElement('pre'));
|
||||
this.inSection = 0;
|
||||
}
|
||||
|
||||
static initializeAPI() {
|
||||
mermaid.mermaidAPI.initialize({
|
||||
startOnLoad: false,
|
||||
theme: 'base',
|
||||
themeVariables: {
|
||||
darkMode: true,
|
||||
primaryColor: '#2a5b6c',
|
||||
primaryTextColor: '#b6b6b6',
|
||||
// lineColor: '#349cbd',
|
||||
lineColor: '#57747d',
|
||||
signalColor: '#57747d',
|
||||
// signalColor: '#349cbd',
|
||||
noteBkgColor: '#516f77',
|
||||
noteTextColor: '#cecece',
|
||||
activationBkgColor: '#1d3f49',
|
||||
activationBorderColor: '#569595',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async log(msg, render = true) {
|
||||
if (this.logBoxPre.textContent && !this.logBoxPre.textContent.endsWith('\n')) {
|
||||
this.logBoxPre.textContent = `${this.logBoxPre.textContent}\n`;
|
||||
}
|
||||
this.logBoxPre.textContent = `${this.logBoxPre.textContent}${msg}\n`;
|
||||
if (render) {
|
||||
await this.render();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
getText() {
|
||||
return this.logBoxPre.textContent;
|
||||
}
|
||||
|
||||
async render() {
|
||||
return debounce(async () => {
|
||||
const text = this.getText();
|
||||
try {
|
||||
const graph = await mermaid.mermaidAPI.render(
|
||||
this.element.getId(),
|
||||
text,
|
||||
);
|
||||
this.renderBox.setInnerHTML(graph);
|
||||
} catch (e) {
|
||||
console.error(`render text:\n${text}`);
|
||||
throw e;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
import { Action } from './action.js';
|
||||
import { CryptoUtil } from '../util/crypto.js';
|
||||
import { MermaidDiagram } from './mermaid.js';
|
||||
import { SequenceDiagram } from './sequence.js';
|
||||
import { Table } from './table.js';
|
||||
import { Flowchart } from './flowchart.js';
|
||||
|
||||
export class Scene {
|
||||
constructor(name, rootBox) {
|
||||
this.name = name;
|
||||
this.box = rootBox.addBox(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');
|
||||
this.middleSection = this.box.addBox('Middle section');
|
||||
this.box.addBox('Spacer').setInnerHTML(' ');
|
||||
this.actors = new Set();
|
||||
this.dateStart = new Date();
|
||||
this.flowcharts = new Map();
|
||||
|
||||
MermaidDiagram.initializeAPI();
|
||||
|
||||
this.options = {
|
||||
edgeNodeColor: '#4d585c',
|
||||
};
|
||||
}
|
||||
|
||||
withSequenceDiagram() {
|
||||
const box = this.box.addBox('Sequence diagram');
|
||||
this.box.addBox('Spacer').setInnerHTML(' ');
|
||||
const logBox = this.box.addBox('Sequence diagram text').addClass('dim');
|
||||
this.sequence = new SequenceDiagram(box, logBox);
|
||||
return this;
|
||||
}
|
||||
|
||||
withFlowchart({ direction = 'BT' } = {}) {
|
||||
const box = this.topSection.addBox('Flowchart').addClass('padded');
|
||||
this.box.addBox('Spacer').setInnerHTML(' ');
|
||||
const logBox = this.box.addBox('Flowchart text').addClass('dim');
|
||||
this.flowchart = new Flowchart(box, logBox, direction);
|
||||
return this;
|
||||
}
|
||||
|
||||
withAdditionalFlowchart({ id, name, direction = 'BT' } = {}) {
|
||||
const index = this.flowcharts.size;
|
||||
name = name ?? `Flowchart ${index}`;
|
||||
id = id ?? `flowchart_${CryptoUtil.randomUUID().slice(0, 4)}`;
|
||||
const container = this.middleSection.addBox(name).flex();
|
||||
const box = container.addBox('Flowchart').addClass('padded');
|
||||
const logBox = container.addBox('Flowchart text').addClass('dim');
|
||||
const flowchart = new MermaidDiagram(box, logBox);
|
||||
flowchart.log(`graph ${direction}`, false);
|
||||
this.flowcharts.set(id, flowchart);
|
||||
return this;
|
||||
}
|
||||
|
||||
lastFlowchart() {
|
||||
if (!this.flowcharts.size) {
|
||||
if (this.flowchart) {
|
||||
return this.flowchart;
|
||||
}
|
||||
throw new Error('lastFlowchart: No additional flowcharts have been added.');
|
||||
}
|
||||
const flowcharts = Array.from(this.flowcharts.values());
|
||||
return flowcharts[flowcharts.length - 1];
|
||||
}
|
||||
|
||||
withTable() {
|
||||
if (this.table) {
|
||||
return this;
|
||||
}
|
||||
const box = this.middleSection.addBox('Table').addClass('padded');
|
||||
this.box.addBox('Spacer').setInnerHTML(' ');
|
||||
this.table = new Table(box);
|
||||
return this;
|
||||
}
|
||||
|
||||
registerActor(actor) {
|
||||
this.actors.add(actor);
|
||||
// this.sequence?.log(`participant ${actor.name}`);
|
||||
}
|
||||
|
||||
findActor(fn) {
|
||||
return Array.from(this.actors.values()).find(fn);
|
||||
}
|
||||
|
||||
addAction(name) {
|
||||
const action = new Action(name, this);
|
||||
return action;
|
||||
}
|
||||
|
||||
addDisplayValue(name) {
|
||||
const dv = this.displayValuesBox.addDisplayValue(name);
|
||||
return dv;
|
||||
}
|
||||
|
||||
stateToTable(label) {
|
||||
const row = new Map();
|
||||
const columns = [];
|
||||
columns.push({ key: 'seqNum', title: '#' });
|
||||
columns.push({ key: 'elapsedMs', title: 'Time (ms)' });
|
||||
row.set('seqNum', this.table.rows.length + 1);
|
||||
row.set('elapsedMs', new Date() - this.dateStart);
|
||||
row.set('label', label);
|
||||
for (const actor of this.actors) {
|
||||
for (const [aKey, { name, value }] of actor.getValuesMap()) {
|
||||
const key = `${actor.name}:${aKey}`;
|
||||
columns.push({ key, title: name });
|
||||
row.set(key, value);
|
||||
}
|
||||
}
|
||||
columns.push({ key: 'label', title: '' });
|
||||
this.table.setColumns(columns);
|
||||
this.table.addRow(row);
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
import { hexToRGB } from '../../util.js';
|
||||
import { MermaidDiagram } from './mermaid.js';
|
||||
|
||||
export class SequenceDiagram extends MermaidDiagram {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.activations = [];
|
||||
this.sections = [];
|
||||
|
||||
this.log('sequenceDiagram', false);
|
||||
}
|
||||
|
||||
async log(...args) {
|
||||
this.sections.forEach(async (section, index) => {
|
||||
const {
|
||||
empty, r, g, b,
|
||||
} = section;
|
||||
if (empty) {
|
||||
section.empty = false;
|
||||
await super.log(`rect rgb(${r}, ${g}, ${b}) # ${index}`, false);
|
||||
}
|
||||
});
|
||||
return super.log(...args);
|
||||
}
|
||||
|
||||
activate(name) {
|
||||
this.log(`activate ${name}`, false);
|
||||
this.activations.push(name);
|
||||
}
|
||||
|
||||
async deactivate(name) {
|
||||
const index = this.activations.findLastIndex((n) => n === name);
|
||||
if (index === -1) throw new Error(`${name} does not appear to be active!`);
|
||||
this.activations.splice(index, 1);
|
||||
await this.log(`deactivate ${name}`);
|
||||
}
|
||||
|
||||
getDeactivationsText() {
|
||||
const text = Array.from(this.activations).reverse().reduce((str, name) => str += `deactivate ${name}\n`, '');
|
||||
return text;
|
||||
}
|
||||
|
||||
async startSection(color = '#08252c') {
|
||||
let { r, g, b } = hexToRGB(color);
|
||||
for (let i = 0; i < this.sections.length; i++) {
|
||||
r += (0xff - r) / 16;
|
||||
g += (0xff - g) / 16;
|
||||
b += (0xff - b) / 16;
|
||||
}
|
||||
this.sections.push({
|
||||
empty: true, r, g, b,
|
||||
});
|
||||
}
|
||||
|
||||
async endSection() {
|
||||
const section = this.sections.pop();
|
||||
if (section && !section.empty) {
|
||||
await this.log('end');
|
||||
}
|
||||
}
|
||||
|
||||
getSectionEndText() {
|
||||
if (this.sections[this.sections.length - 1]?.empty) {
|
||||
this.sections.pop();
|
||||
}
|
||||
return this.sections.map(() => 'end\n').join('');
|
||||
}
|
||||
|
||||
getText() {
|
||||
let text = super.getText();
|
||||
if (!text.endsWith('\n')) text = `${text}\n`;
|
||||
text += this.getDeactivationsText();
|
||||
text += this.getSectionEndText();
|
||||
return text;
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
import { displayNumber } from '../../util.js';
|
||||
|
||||
export class Table {
|
||||
constructor(box) {
|
||||
this.box = box;
|
||||
this.columns = [];
|
||||
this.rows = [];
|
||||
this.table = box.el.appendChild(document.createElement('table'));
|
||||
this.headings = this.table.appendChild(document.createElement('tr'));
|
||||
}
|
||||
|
||||
setColumns(columns) {
|
||||
if (JSON.stringify(columns) === JSON.stringify(this.columns)) {
|
||||
return;
|
||||
}
|
||||
if (this.columns.length) {
|
||||
this.table.innerHTML = '';
|
||||
this.headings = this.table.appendChild(document.createElement('tr'));
|
||||
this.columns = [];
|
||||
}
|
||||
this.columns = columns;
|
||||
for (const { title } of columns) {
|
||||
const heading = document.createElement('th');
|
||||
this.headings.appendChild(heading);
|
||||
heading.innerHTML = title ?? '';
|
||||
}
|
||||
if (this.rows.length) {
|
||||
const { rows } = this;
|
||||
this.rows = [];
|
||||
for (const row of rows) {
|
||||
this.addRow(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addRow(rowMap) {
|
||||
this.rows.push(rowMap);
|
||||
const row = this.table.appendChild(document.createElement('tr'));
|
||||
for (const { key } of this.columns) {
|
||||
const value = rowMap.get(key);
|
||||
const cell = row.appendChild(document.createElement('td'));
|
||||
cell.innerHTML = typeof value === 'number' ? displayNumber(value) : value ?? '';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
import { Action } from '../display/action.js';
|
||||
import {
|
||||
Message, PostMessage, PeerMessage, messageFromJSON,
|
||||
} from './message.js';
|
||||
import { ForumView } from './forum-view.js';
|
||||
import { NetworkNode } from './network-node.js';
|
||||
import { randomID } from '../../util.js';
|
||||
|
||||
export class ForumNode extends NetworkNode {
|
||||
constructor(name, scene) {
|
||||
super(name, scene);
|
||||
this.forumView = new ForumView();
|
||||
this.actions = {
|
||||
...this.actions,
|
||||
storePost: new Action('store post', scene),
|
||||
};
|
||||
}
|
||||
|
||||
// Process a message from the queue
|
||||
async processMessage(messageJson) {
|
||||
try {
|
||||
await Message.verify(messageJson);
|
||||
} catch (e) {
|
||||
await this.actions.processMessage.log(this, this, 'invalid signature', null, '-x');
|
||||
console.log(`${this.name}: received message with invalid signature`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { publicKey } = messageJson;
|
||||
const message = messageFromJSON(messageJson);
|
||||
|
||||
if (message instanceof PostMessage) {
|
||||
await this.processPostMessage(publicKey, message.content);
|
||||
} else if (message instanceof PeerMessage) {
|
||||
await this.processPeerMessage(publicKey, message.content);
|
||||
} else {
|
||||
// Unknown message type
|
||||
// Penalize sender for wasting our time
|
||||
}
|
||||
}
|
||||
|
||||
// Process an incoming post, received by whatever means
|
||||
async processPost(authorId, post) {
|
||||
if (!post.id) {
|
||||
post.id = randomID();
|
||||
}
|
||||
await this.actions.storePost.log(this, this);
|
||||
// this.forumView.addPost(authorId, post.id, post, stake);
|
||||
}
|
||||
|
||||
// Process a post we received in a message
|
||||
async processPostMessage(authorId, { post, stake }) {
|
||||
this.processPost(authorId, post, stake);
|
||||
await this.broadcast(
|
||||
new PeerMessage({
|
||||
posts: [{ authorId, post, stake }],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Process a message we receive from a peer
|
||||
async processPeerMessage(peerId, { posts }) {
|
||||
// We are trusting that the peer verified the signatures of the posts they're forwarding.
|
||||
// We could instead have the peer forward the signed messages and re-verify them.
|
||||
for (const { authorId, post, stake } of posts) {
|
||||
this.processPost(authorId, post, stake);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
import { WDAG } from '../supporting/wdag.js';
|
||||
|
||||
class Author {
|
||||
constructor() {
|
||||
this.posts = new Map();
|
||||
this.reputation = 0;
|
||||
}
|
||||
}
|
||||
|
||||
class PostVertex {
|
||||
constructor(id, author, stake, content, citations) {
|
||||
this.id = id;
|
||||
this.author = author;
|
||||
this.content = content;
|
||||
this.stake = stake;
|
||||
this.citations = citations;
|
||||
this.reputation = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class ForumView {
|
||||
constructor() {
|
||||
this.reputations = new Map();
|
||||
this.posts = new WDAG();
|
||||
this.authors = new Map();
|
||||
}
|
||||
|
||||
getReputation(id) {
|
||||
return this.reputations.get(id);
|
||||
}
|
||||
|
||||
setReputation(id, reputation) {
|
||||
this.reputations.set(id, reputation);
|
||||
}
|
||||
|
||||
incrementReputation(publicKey, increment, _reason) {
|
||||
const reputation = this.getReputation(publicKey) || 0;
|
||||
return this.reputations.set(publicKey, reputation + increment);
|
||||
}
|
||||
|
||||
getOrInitializeAuthor(authorId) {
|
||||
let author = this.authors.get(authorId);
|
||||
if (!author) {
|
||||
author = new Author(authorId);
|
||||
this.authors.set(authorId, author);
|
||||
}
|
||||
return author;
|
||||
}
|
||||
|
||||
addPost(authorId, postId, postContent, stake) {
|
||||
const { citations = [], content } = postContent;
|
||||
const author = this.getOrInitializeAuthor(authorId);
|
||||
const postVertex = new PostVertex(postId, author, stake, content, citations);
|
||||
this.posts.addVertex(postId, postVertex);
|
||||
for (const { postId: citedPostId, weight } of citations) {
|
||||
this.posts.addEdge('citation', postId, citedPostId, weight);
|
||||
}
|
||||
}
|
||||
|
||||
getPost(postId) {
|
||||
return this.posts.getVertexData(postId);
|
||||
}
|
||||
|
||||
getPosts() {
|
||||
return this.posts.getVertices();
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
import { CryptoUtil } from '../util/crypto.js';
|
||||
import { PostContent } from '../util/post-content.js';
|
||||
|
||||
export class Message {
|
||||
constructor(content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
async sign({ publicKey, privateKey }) {
|
||||
this.publicKey = await CryptoUtil.exportKey(publicKey);
|
||||
// Call toJSON before signing, to match what we'll later send
|
||||
this.signature = await CryptoUtil.sign(this.contentToJSON(), privateKey);
|
||||
return this;
|
||||
}
|
||||
|
||||
static async verify({ content, publicKey, signature }) {
|
||||
return CryptoUtil.verify(content, publicKey, signature);
|
||||
}
|
||||
|
||||
contentToJSON() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
type: this.type,
|
||||
content: this.contentToJSON(),
|
||||
publicKey: this.publicKey,
|
||||
signature: this.signature,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class PostMessage extends Message {
|
||||
type = 'post';
|
||||
|
||||
constructor({ post, stake }) {
|
||||
super({
|
||||
post: PostContent.fromJSON(post),
|
||||
stake,
|
||||
});
|
||||
}
|
||||
|
||||
contentToJSON() {
|
||||
return {
|
||||
post: this.content.post.toJSON(),
|
||||
stakeAmount: this.content.stake,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class PeerMessage extends Message {
|
||||
type = 'peer';
|
||||
}
|
||||
|
||||
const messageTypes = new Map([
|
||||
['post', PostMessage],
|
||||
['peer', PeerMessage],
|
||||
]);
|
||||
|
||||
export const messageFromJSON = ({ type, content }) => {
|
||||
const MessageType = messageTypes.get(type) || Message;
|
||||
// const messageContent = MessageType.contentFromJSON(content);
|
||||
return new MessageType(content);
|
||||
};
|
|
@ -1,57 +0,0 @@
|
|||
import { Actor } from '../display/actor.js';
|
||||
import { Action } from '../display/action.js';
|
||||
import { CryptoUtil } from '../util/crypto.js';
|
||||
import { PrioritizedQueue } from '../util/prioritized-queue.js';
|
||||
|
||||
export class NetworkNode extends Actor {
|
||||
constructor(name, scene) {
|
||||
super(name, scene);
|
||||
this.queue = new PrioritizedQueue();
|
||||
this.actions = {
|
||||
peerMessage: new Action('peer message', scene),
|
||||
};
|
||||
}
|
||||
|
||||
// Generate a signing key pair and connect to the network
|
||||
async initialize(forumNetwork) {
|
||||
this.keyPair = await CryptoUtil.generateAsymmetricKey();
|
||||
this.forumNetwork = forumNetwork.addNode(this);
|
||||
this.status.set('Initialized');
|
||||
return this;
|
||||
}
|
||||
|
||||
// Send a message to all other nodes in the network
|
||||
async broadcast(message) {
|
||||
await message.sign(this.keyPair);
|
||||
const otherForumNodes = this.forumNetwork
|
||||
.listNodes()
|
||||
.filter((forumNode) => forumNode.keyPair.publicKey !== this.keyPair.publicKey);
|
||||
for (const forumNode of otherForumNodes) {
|
||||
// For now just call receiveMessage on the target node
|
||||
// await this.actions.peerMessage.log(this, forumNode, null, message.content);
|
||||
await this.actions.peerMessage.log(this, forumNode);
|
||||
await forumNode.receiveMessage(JSON.stringify(message.toJSON()));
|
||||
}
|
||||
}
|
||||
|
||||
// Perform minimal processing to ingest a message.
|
||||
// Enqueue it for further processing.
|
||||
async receiveMessage(messageStr) {
|
||||
const messageJson = JSON.parse(messageStr);
|
||||
const senderReputation = this.forumView.getReputation(messageJson.publicKey) || 0;
|
||||
this.queue.add(messageJson, senderReputation);
|
||||
}
|
||||
|
||||
// Process next highest priority message in the queue
|
||||
async processNextMessage() {
|
||||
const messageJson = this.queue.pop();
|
||||
if (!messageJson) {
|
||||
return null;
|
||||
}
|
||||
return this.processMessage(messageJson);
|
||||
}
|
||||
|
||||
// Process a message from the queue
|
||||
// async processMessage(messageJson) {
|
||||
// }
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
export class Network {
|
||||
constructor() {
|
||||
this.nodes = new Map();
|
||||
}
|
||||
|
||||
addNode(node) {
|
||||
this.nodes.set(node.keyPair.publicKey, node);
|
||||
return this;
|
||||
}
|
||||
|
||||
listNodes() {
|
||||
return Array.from(this.nodes.values());
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export class BlockConsensus {
|
||||
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
export class Token {
|
||||
constructor(ownerPublicKey) {
|
||||
this.ownerPublicKey = ownerPublicKey;
|
||||
}
|
||||
|
||||
transfer(newOwnerPublicKey) {
|
||||
// TODO: Current owner must sign this request
|
||||
this.ownerPublicKey = newOwnerPublicKey;
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import params from '../../params.js';
|
||||
|
||||
export class Stake {
|
||||
constructor({
|
||||
tokenId, position, amount, lockingTime,
|
||||
}) {
|
||||
this.tokenId = tokenId;
|
||||
this.position = position;
|
||||
this.amount = amount;
|
||||
this.lockingTime = lockingTime;
|
||||
}
|
||||
|
||||
getStakeValue() {
|
||||
return this.amount * this.lockingTime ** params.lockingTimeExponent;
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
import { Action } from '../display/action.js';
|
||||
|
||||
class ContractRecord {
|
||||
constructor(id, instance) {
|
||||
this.id = id;
|
||||
this.instance = instance;
|
||||
}
|
||||
}
|
||||
|
||||
export class VMHandle {
|
||||
constructor(vm, sender) {
|
||||
this.vm = vm;
|
||||
this.sender = sender;
|
||||
this.actions = {
|
||||
call: new Action('call', vm.scene),
|
||||
return: new Action('return', vm.scene),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id Contract ID
|
||||
* @param {string} method
|
||||
*/
|
||||
async callContract(id, method, ...args) {
|
||||
const instance = this.vm.getContractInstance(id);
|
||||
const fn = instance[method];
|
||||
if (!fn) throw new Error(`Contract ${id} method ${method} not found!`);
|
||||
await this.actions.call.log(this.sender, instance, method);
|
||||
const result = await fn.call(instance, this.sender, ...args);
|
||||
await this.actions.return.log(instance, this.sender, undefined, undefined, '-->>');
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export class VM {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.contracts = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {class} ContractClass
|
||||
* @param {any[]} ...args Passed to contractClass constructor after `vm`
|
||||
*/
|
||||
addContract(id, ContractClass, ...args) {
|
||||
const instance = new ContractClass(this, ...args);
|
||||
const contract = new ContractRecord(id, instance);
|
||||
this.contracts.set(id, contract);
|
||||
}
|
||||
|
||||
getHandle(sender) {
|
||||
return new VMHandle(this, sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
getContract(id) {
|
||||
return this.contracts.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
getContractInstance(id) {
|
||||
return this.getContract(id)?.instance;
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
export class Voter {
|
||||
constructor(reputationPublicKey) {
|
||||
this.reputationPublicKey = reputationPublicKey;
|
||||
this.voteHistory = [];
|
||||
this.dateLastVote = null;
|
||||
}
|
||||
|
||||
addVoteRecord(stake) {
|
||||
this.voteHistory.push(stake);
|
||||
if (!this.dateLastVote || stake.dateStart > this.dateLastVote) {
|
||||
this.dateLastVote = stake.dateStart;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,152 +0,0 @@
|
|||
export class Vertex {
|
||||
constructor(id, data) {
|
||||
this.id = id;
|
||||
this.data = data;
|
||||
this.edges = {
|
||||
from: [],
|
||||
to: [],
|
||||
};
|
||||
}
|
||||
|
||||
getEdges(label, away) {
|
||||
return this.edges[away ? 'from' : 'to'].filter(
|
||||
(edge) => edge.label === label,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class Edge {
|
||||
constructor(label, from, to, weight) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.label = label;
|
||||
this.weight = weight;
|
||||
}
|
||||
}
|
||||
|
||||
export class WDAG {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.vertices = new Map();
|
||||
this.edgeLabels = new Map();
|
||||
this.nextVertexId = 0;
|
||||
this.flowchart = scene?.flowchart;
|
||||
}
|
||||
|
||||
withFlowchart() {
|
||||
this.scene?.withAdditionalFlowchart();
|
||||
this.flowchart = this.scene?.lastFlowchart();
|
||||
return this;
|
||||
}
|
||||
|
||||
addVertex(id, data, label) {
|
||||
// Support simple case of auto-incremented numeric ids
|
||||
if (typeof id === 'object') {
|
||||
data = id;
|
||||
id = this.nextVertexId++;
|
||||
}
|
||||
if (this.vertices.has(id)) {
|
||||
throw new Error(`Vertex already exists with id: ${id}`);
|
||||
}
|
||||
const vertex = new Vertex(id, data);
|
||||
this.vertices.set(id, vertex);
|
||||
this.flowchart?.log(`${id}[${label ?? id}]`);
|
||||
return this;
|
||||
}
|
||||
|
||||
setVertexLabel(id, label) {
|
||||
this.flowchart?.log(`${id}[${label}]`);
|
||||
}
|
||||
|
||||
getVertex(id) {
|
||||
return this.vertices.get(id);
|
||||
}
|
||||
|
||||
getVertexData(id) {
|
||||
return this.getVertex(id)?.data;
|
||||
}
|
||||
|
||||
getVerticesData() {
|
||||
return Array.from(this.vertices.values()).map(({ data }) => data);
|
||||
}
|
||||
|
||||
static getEdgeKey({ from, to }) {
|
||||
return btoa([from.id, to.id]).replaceAll(/[^A-Za-z0-9]+/g, '');
|
||||
}
|
||||
|
||||
getEdge(label, from, to) {
|
||||
from = from instanceof Vertex ? from : this.getVertex(from);
|
||||
to = to instanceof Vertex ? to : this.getVertex(to);
|
||||
if (!from || !to) {
|
||||
return undefined;
|
||||
}
|
||||
const edges = this.edgeLabels.get(label);
|
||||
const edgeKey = WDAG.getEdgeKey({ from, to });
|
||||
return edges?.get(edgeKey);
|
||||
}
|
||||
|
||||
getEdgeWeight(label, from, to) {
|
||||
return this.getEdge(label, from, to)?.weight;
|
||||
}
|
||||
|
||||
getEdgeHtml({ from, to }) {
|
||||
let html = '<table>';
|
||||
for (const { label, weight } of this.getEdges(null, from, to)) {
|
||||
html += `<tr><td>${label}</td><td>${weight}</td></tr>`;
|
||||
}
|
||||
html += '</table>';
|
||||
return html;
|
||||
}
|
||||
|
||||
getEdgeFlowchartNode(edge) {
|
||||
const edgeKey = WDAG.getEdgeKey(edge);
|
||||
return `${edgeKey}(${this.getEdgeHtml(edge)})`;
|
||||
}
|
||||
|
||||
setEdgeWeight(label, from, to, weight) {
|
||||
from = from instanceof Vertex ? from : this.getVertex(from);
|
||||
to = to instanceof Vertex ? to : this.getVertex(to);
|
||||
const edge = new Edge(label, from, to, weight);
|
||||
let edges = this.edgeLabels.get(label);
|
||||
if (!edges) {
|
||||
edges = new Map();
|
||||
this.edgeLabels.set(label, edges);
|
||||
}
|
||||
const edgeKey = WDAG.getEdgeKey(edge);
|
||||
edges.set(edgeKey, edge);
|
||||
this.flowchart?.log(this.getEdgeFlowchartNode(edge));
|
||||
return edge;
|
||||
}
|
||||
|
||||
addEdge(label, from, to, weight) {
|
||||
from = from instanceof Vertex ? from : this.getVertex(from);
|
||||
to = to instanceof Vertex ? to : this.getVertex(to);
|
||||
if (this.getEdge(label, from, to)) {
|
||||
throw new Error(`Edge ${label} from ${from.id} to ${to.id} already exists`);
|
||||
}
|
||||
const edge = this.setEdgeWeight(label, from, to, weight);
|
||||
from.edges.from.push(edge);
|
||||
to.edges.to.push(edge);
|
||||
this.flowchart?.log(`${from.id} --- ${this.getEdgeFlowchartNode(edge)} --> ${to.id}`);
|
||||
this.flowchart?.log(`class ${WDAG.getEdgeKey(edge)} edge`);
|
||||
return this;
|
||||
}
|
||||
|
||||
getEdges(label, from, to) {
|
||||
from = from instanceof Vertex ? from : this.getVertex(from);
|
||||
to = to instanceof Vertex ? to : this.getVertex(to);
|
||||
const edgeLabels = label ? [label] : Array.from(this.edgeLabels.keys());
|
||||
return edgeLabels.flatMap((edgeLabel) => {
|
||||
const edges = this.edgeLabels.get(edgeLabel);
|
||||
return Array.from(edges?.values() || []).filter((edge) => {
|
||||
const matchFrom = from === null || from === undefined || from === edge.from;
|
||||
const matchTo = to === null || to === undefined || to === edge.to;
|
||||
return matchFrom && matchTo;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
countVertices() {
|
||||
return this.vertices.size;
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
export class CryptoUtil {
|
||||
static algorithm = 'RSASSA-PKCS1-v1_5';
|
||||
|
||||
static hash = 'SHA-256';
|
||||
|
||||
static async generateAsymmetricKey() {
|
||||
return window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: CryptoUtil.algorithm,
|
||||
hash: CryptoUtil.hash,
|
||||
modulusLength: 2048,
|
||||
publicExponent: new Uint8Array([1, 0, 1]),
|
||||
},
|
||||
true,
|
||||
['sign', 'verify'],
|
||||
);
|
||||
}
|
||||
|
||||
static async sign(content, privateKey) {
|
||||
const encoder = new TextEncoder();
|
||||
const encoded = encoder.encode(content);
|
||||
const signature = await window.crypto.subtle.sign(CryptoUtil.algorithm, privateKey, encoded);
|
||||
// Return base64-encoded signature
|
||||
return btoa(String.fromCharCode(...new Uint8Array(signature)));
|
||||
}
|
||||
|
||||
static async verify(content, b64publicKey, b64signature) {
|
||||
// Convert base64 javascript web key to CryptoKey
|
||||
const publicKey = await CryptoUtil.importKey(b64publicKey);
|
||||
// Convert base64 signature to an ArrayBuffer
|
||||
const signature = Uint8Array.from(atob(b64signature), (c) => c.charCodeAt(0));
|
||||
// TODO: make a single TextEncoder instance and reuse it
|
||||
const encoder = new TextEncoder();
|
||||
const encoded = encoder.encode(content);
|
||||
return window.crypto.subtle.verify(CryptoUtil.algorithm, publicKey, signature, encoded);
|
||||
}
|
||||
|
||||
static async exportKey(publicKey) {
|
||||
// Store public key as base64 javascript web key
|
||||
const jwk = await window.crypto.subtle.exportKey('jwk', publicKey);
|
||||
return btoa(JSON.stringify(jwk));
|
||||
}
|
||||
|
||||
static async importKey(b64jwk) {
|
||||
// Convert base64 javascript web key to CryptoKey
|
||||
const jwk = JSON.parse(atob(b64jwk));
|
||||
return window.crypto.subtle.importKey(
|
||||
'jwk',
|
||||
jwk,
|
||||
{
|
||||
name: CryptoUtil.algorithm,
|
||||
hash: CryptoUtil.hash,
|
||||
},
|
||||
false,
|
||||
['verify'],
|
||||
);
|
||||
}
|
||||
|
||||
static randomUUID() {
|
||||
return window.crypto.randomUUID();
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
export class Citation {
|
||||
constructor(postId, weight) {
|
||||
this.postId = postId;
|
||||
this.weight = weight;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
postId: this.postId,
|
||||
weight: this.weight,
|
||||
};
|
||||
}
|
||||
|
||||
static fromJSON({ postId, weight }) {
|
||||
return new Citation(postId, weight);
|
||||
}
|
||||
}
|
||||
|
||||
export class PostContent {
|
||||
constructor(content) {
|
||||
this.content = content;
|
||||
this.citations = [];
|
||||
}
|
||||
|
||||
addCitation(postId, weight) {
|
||||
const citation = new Citation(postId, weight);
|
||||
this.citations.push(citation);
|
||||
return this;
|
||||
}
|
||||
|
||||
setTitle(title) {
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
content: this.content,
|
||||
citations: this.citations.map((citation) => citation.toJSON()),
|
||||
...(this.id ? { id: this.id } : {}),
|
||||
title: this.title,
|
||||
};
|
||||
}
|
||||
|
||||
static fromJSON({
|
||||
id, content, citations, title,
|
||||
}) {
|
||||
const post = new PostContent(content);
|
||||
post.citations = citations.map((citation) => Citation.fromJSON(citation));
|
||||
post.id = id;
|
||||
post.title = title;
|
||||
return post;
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
export class PrioritizedQueue {
|
||||
constructor() {
|
||||
this.buffer = [];
|
||||
}
|
||||
|
||||
// Add an item to the buffer, ahead of the next lowest priority item
|
||||
add(message, priority) {
|
||||
const idx = this.buffer.findIndex((item) => item.priority < priority);
|
||||
if (idx < 0) {
|
||||
this.buffer.push({ message, priority });
|
||||
} else {
|
||||
this.buffer.splice(idx, 0, { message, priority });
|
||||
}
|
||||
}
|
||||
|
||||
// Return the highest priority item in the buffer
|
||||
pop() {
|
||||
if (!this.buffer.length) {
|
||||
return null;
|
||||
}
|
||||
const item = this.buffer.shift();
|
||||
return item.message;
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 3.5 KiB |
|
@ -1,51 +0,0 @@
|
|||
body {
|
||||
background-color: #09343f;
|
||||
color: #b6b6b6;
|
||||
font-family: monospace;
|
||||
font-size: 8pt;
|
||||
margin: 1em;
|
||||
}
|
||||
a {
|
||||
color: #c6f4ff;
|
||||
}
|
||||
a:visited {
|
||||
color: #85b7c3;
|
||||
}
|
||||
.box {
|
||||
width: fit-content;
|
||||
}
|
||||
.box .name {
|
||||
width: 15em;
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
margin-right: 6pt;
|
||||
}
|
||||
.box .value {
|
||||
width: fit-content;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.monospace {
|
||||
font-family: monospace;
|
||||
font-size: 8pt;
|
||||
}
|
||||
.dim {
|
||||
opacity: 0.25;
|
||||
}
|
||||
.padded {
|
||||
padding: 20px;
|
||||
}
|
||||
svg {
|
||||
width: 800px;
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
}
|
||||
td {
|
||||
background-color: #0c2025;
|
||||
}
|
||||
.edge > rect {
|
||||
fill: #216262 !important;
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Decentralized Governance Framework - Tests</title>
|
||||
<link type="text/css" rel="stylesheet" href="./index.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>DGF Tests</h2>
|
||||
<ul>
|
||||
<li><a href="./tests/validation-pool.test.html">Validation Pool</a></li>
|
||||
<li><a href="./tests/availability.test.html">Availability + Business</a></li>
|
||||
</ul>
|
||||
|
||||
<ul>
|
||||
<h3>Forum</h3>
|
||||
<ol>
|
||||
<li><a href="./tests/forum1.test.html">Negative citation of a negative citation</a></li>
|
||||
<li><a href="./tests/forum2.test.html">Negative citation of a weaker negative citation</a></li>
|
||||
<li><a href="./tests/forum3.test.html">Redistribute power</a></li>
|
||||
<li><a href="./tests/forum4.test.html">Redistribute power through subsequent support</a></li>
|
||||
<li><a href="./tests/forum5.test.html"> Destroy a post after it has received positive citations</a></li>
|
||||
</ol>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="./tests/forum-network.test.html">Forum Network</a></li>
|
||||
<li><a href="./tests/vm.test.html">VM</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="./tests/basic.test.html">Basic Sequencing</a></li>
|
||||
<li><a href="./tests/basic2.test.html">Basic Sequencing 2</a></li>
|
||||
<li><a href="./tests/wdag.test.html">WDAG</a></li>
|
||||
<li><a href="./tests/debounce.test.html">Debounce</a></li>
|
||||
<li><a href="./tests/flowchart.test.html">Flowchart</a></li>
|
||||
<li><a href="./tests/mocha.test.html">Mocha</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<h4><a href="./tests/all.test.html">All</a></h4>
|
||||
</ul>
|
||||
</body>
|
|
@ -1,28 +0,0 @@
|
|||
const params = {
|
||||
/* Validation Pool parameters */
|
||||
mintingRatio: () => 1, // c1
|
||||
// NOTE: c2 overlaps with c3 and adds excess complexity, so we omit it for now
|
||||
stakeForAuthor: 0.5, // c3
|
||||
winningRatio: 0.5, // c4
|
||||
quorum: 0, // c5
|
||||
activeVoterThreshold: null, // c6
|
||||
voteDuration: {
|
||||
// c7
|
||||
min: 0,
|
||||
max: null,
|
||||
},
|
||||
// NOTE: c8 is the token loss ratio, which is specified as a runtime argument
|
||||
contentiousDebate: {
|
||||
period: 5000, // c9
|
||||
stages: 3, // c10
|
||||
},
|
||||
lockingTimeExponent: 0, // c11
|
||||
|
||||
/* Forum parameters */
|
||||
initialPostValue: () => 1, // q1
|
||||
revaluationLimit: 1, // q2
|
||||
referenceChainLimit: 3, // q3
|
||||
leachingValue: 1, // q4
|
||||
};
|
||||
|
||||
export default params;
|
|
@ -1,43 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>VM</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" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2><a href="../">DGF Tests</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/mocha/mocha.js"></script>
|
||||
<script src="https://unpkg.com/chai/chai.js"></script>
|
||||
<script type="module" src="./scripts/availability.test.js"></script>
|
||||
<script type="module" src="./scripts/business.test.js"></script>
|
||||
<script type="module" src="./scripts/forum-network.test.js"></script>
|
||||
<script type="module" src="./scripts/mocha.test.js"></script>
|
||||
<script type="module" src="./scripts/validation-pool.test.js"></script>
|
||||
<script type="module" src="./scripts/vm.test.js"></script>
|
||||
<script type="module" src="./scripts/wdag.test.js"></script>
|
||||
<script type="module" src="./scripts/forum/forum1.test.js"></script>
|
||||
<script type="module" src="./scripts/forum/forum2.test.js"></script>
|
||||
<script type="module" src="./scripts/forum/forum3.test.js"></script>
|
||||
<script type="module" src="./scripts/forum/forum4.test.js"></script>
|
||||
<script type="module" src="./scripts/forum/forum5.test.js"></script>
|
||||
<script defer class="mocha-init">
|
||||
mocha.setup({
|
||||
ui: 'bdd',
|
||||
globals: ['scene', 'dao', 'experts', 'posts', 'vm', 'graph', '__REACT_DEVTOOLS_*'],
|
||||
});
|
||||
mocha.checkLeaks();
|
||||
window.should = chai.should();
|
||||
</script>
|
||||
<script defer class="mocha-exec">
|
||||
// TODO: Weird race condition -- resolve this in a better way
|
||||
setTimeout(() => mocha.run(), 1000);
|
||||
</script>
|
|
@ -1,37 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Availability 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" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2><a href="../">DGF Tests</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://cdnjs.cloudflare.com/ajax/libs/mocha/10.2.0/mocha.min.js"
|
||||
integrity="sha512-jsP/sG70bnt0xNVJt+k9NxQqGYvRrLzWhI+46SSf7oNJeCwdzZlBvoyrAN0zhtVyolGcHNh/9fEgZppG2pH+eA=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/4.3.7/chai.min.js"
|
||||
integrity="sha512-Pwgr3yHn4Gvztp1GKl0ihhAWLZfqgp4/SbMt4HKW7AymuTQODMCNPE7v1uGapTeOoQQ5Hoz367b4seKpx6j7Zg=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script type="module" src="./scripts/availability.test.js"></script>
|
||||
<script defer class="mocha-init">
|
||||
mocha.setup({
|
||||
ui: 'bdd',
|
||||
globals: ['scene', 'dao', 'experts', 'requestor', '__REACT_DEVTOOLS_*'],
|
||||
});
|
||||
mocha.checkLeaks();
|
||||
window.should = chai.should();
|
||||
</script>
|
||||
<script defer class="mocha-exec">
|
||||
// TODO: Weird race condition -- resolve this in a better way
|
||||
setTimeout(() => mocha.run(), 1000);
|
||||
</script>
|
|
@ -1,13 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Forum Network</title>
|
||||
<link type="text/css" rel="stylesheet" href="../index.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2><a href="../">DGF Tests</a></h2>
|
||||
<div id="basic"></div>
|
||||
</body>
|
||||
<script type="module" src="./scripts/basic.test.js">
|
||||
</script>
|
|
@ -1,13 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Forum Network</title>
|
||||
<link type="text/css" rel="stylesheet" href="../index.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2><a href="../">DGF Tests</a></h2>
|
||||
<div id="basic"></div>
|
||||
</body>
|
||||
<script type="module" src="./scripts/basic2.test.js">
|
||||
</script>
|
|
@ -1,32 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Business</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" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2><a href="../">DGF Tests</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/mocha/mocha.js"></script>
|
||||
<script src="https://unpkg.com/chai/chai.js"></script>
|
||||
<script type="module" src="./scripts/business.test.js"></script>
|
||||
<script defer class="mocha-init">
|
||||
mocha.setup({
|
||||
ui: 'bdd',
|
||||
globals: ['scene', 'dao', 'experts', 'posts', '__REACT_DEVTOOLS_*'],
|
||||
});
|
||||
mocha.checkLeaks();
|
||||
window.should = chai.should();
|
||||
</script>
|
||||
<script defer class="mocha-exec">
|
||||
// TODO: Weird race condition -- resolve this in a better way
|
||||
setTimeout(() => mocha.run(), 1000);
|
||||
</script>
|
|
@ -1,36 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Debounce 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" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2><a href="../">DGF Tests</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/mocha/mocha.js"></script>
|
||||
<script src="https://unpkg.com/chai/chai.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sinon-chai@3.7.0/lib/sinon-chai.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/sinon.js/10.0.1/sinon.min.js"
|
||||
integrity="sha512-pNdrcn83nlZaY1zDLGVtHH2Baxe86kDMqspVOChVjxN71s6DZtcZVqyHXofexm/d8K/1qbJfNUGku3wHIbjSpw=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script type="module" src="./scripts/debounce.test.js"></script>
|
||||
<script defer class="mocha-init">
|
||||
mocha.setup({
|
||||
ui: 'bdd',
|
||||
globals: ['scene', 'dao', 'experts', 'posts', 'vm', 'graph', '__REACT_DEVTOOLS_*'],
|
||||
});
|
||||
mocha.checkLeaks();
|
||||
window.should = chai.should();
|
||||
</script>
|
||||
<script defer class="mocha-exec">
|
||||
// TODO: Weird race condition -- resolve this in a better way
|
||||
setTimeout(() => mocha.run(), 1000);
|
||||
</script>
|
|
@ -1,41 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Flowchart test</title>
|
||||
<link type="text/css" rel="stylesheet" href="../index.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2><a href="../">DGF Tests</a></h2>
|
||||
<div id="flowchart-test"></div>
|
||||
</body>
|
||||
<script type="module">
|
||||
import { Box } from '../classes/box.js';
|
||||
import { Scene } from '../classes/scene.js';
|
||||
import { Actor } from '../classes/actor.js';
|
||||
import { Action } from '../classes/action.js';
|
||||
import { delay } from '../util.js';
|
||||
|
||||
const DEFAULT_DELAY_INTERVAL = 500;
|
||||
|
||||
const rootElement = document.getElementById('flowchart-test');
|
||||
const rootBox = new Box('rootBox', rootElement).flex();
|
||||
|
||||
const scene = (window.scene = new Scene('Flowchart test', rootBox));
|
||||
scene.withSequenceDiagram();
|
||||
|
||||
const actor1 = new Actor('A', scene);
|
||||
const actor2 = new Actor('B', scene);
|
||||
const action1 = new Action('Action 1', scene);
|
||||
await action1.log(actor1, actor2);
|
||||
await actor1.setValue('value', 1);
|
||||
|
||||
await scene.withFlowchart();
|
||||
await scene.flowchart.log('A --> B');
|
||||
|
||||
await delay(DEFAULT_DELAY_INTERVAL);
|
||||
action1.log(actor1, actor2);
|
||||
|
||||
await delay(DEFAULT_DELAY_INTERVAL);
|
||||
await scene.flowchart.log('A --> C');
|
||||
</script>
|
|
@ -1,33 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Forum Network 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" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2><a href="../">DGF Tests</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/mocha/mocha.js"></script>
|
||||
<script src="https://unpkg.com/chai/chai.js"></script>
|
||||
<script type="module" src="./scripts/forum-network.test.js"></script>
|
||||
<script defer class="mocha-init">
|
||||
mocha.setup({
|
||||
ui: 'bdd',
|
||||
globals: ['scene', 'dao', 'experts', 'posts', 'vm', 'graph', '__REACT_DEVTOOLS_*'],
|
||||
});
|
||||
mocha.checkLeaks();
|
||||
window.should = chai.should();
|
||||
</script>
|
||||
<script defer class="mocha-exec">
|
||||
// TODO: Weird race condition -- resolve this in a better way
|
||||
setTimeout(() => mocha.run(), 1000);
|
||||
</script>
|
|
@ -1,32 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Forum test 1</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" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2><a href="../">DGF Tests</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/mocha/mocha.js"></script>
|
||||
<script src="https://unpkg.com/chai/chai.js"></script>
|
||||
<script type="module" src="./scripts/forum/forum1.test.js"></script>
|
||||
<script defer class="mocha-init">
|
||||
mocha.setup({
|
||||
ui: 'bdd',
|
||||
globals: ['scene', 'dao', 'experts', 'posts', '__REACT_DEVTOOLS_*'],
|
||||
});
|
||||
mocha.checkLeaks();
|
||||
chai.should();
|
||||
</script>
|
||||
<script defer class="mocha-exec">
|
||||
// TODO: Weird race condition -- resolve this in a better way
|
||||
setTimeout(() => mocha.run(), 1000);
|
||||
</script>
|
|
@ -1,32 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Forum test 2</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" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2><a href="../">DGF Tests</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/mocha/mocha.js"></script>
|
||||
<script src="https://unpkg.com/chai/chai.js"></script>
|
||||
<script type="module" src="./scripts/forum/forum2.test.js"></script>
|
||||
<script defer class="mocha-init">
|
||||
mocha.setup({
|
||||
ui: 'bdd',
|
||||
globals: ['scene', 'dao', 'experts', 'posts', '__REACT_DEVTOOLS_*'],
|
||||
});
|
||||
mocha.checkLeaks();
|
||||
chai.should();
|
||||
</script>
|
||||
<script defer class="mocha-exec">
|
||||
// TODO: Weird race condition -- resolve this in a better way
|
||||
setTimeout(() => mocha.run(), 1000);
|
||||
</script>
|
|
@ -1,32 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Forum test 3</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" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2><a href="../">DGF Tests</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/mocha/mocha.js"></script>
|
||||
<script src="https://unpkg.com/chai/chai.js"></script>
|
||||
<script type="module" src="./scripts/forum/forum3.test.js"></script>
|
||||
<script defer class="mocha-init">
|
||||
mocha.setup({
|
||||
ui: 'bdd',
|
||||
globals: ['scene', 'dao', 'experts', 'posts', '__REACT_DEVTOOLS_*'],
|
||||
});
|
||||
mocha.checkLeaks();
|
||||
chai.should();
|
||||
</script>
|
||||
<script defer class="mocha-exec">
|
||||
// TODO: Weird race condition -- resolve this in a better way
|
||||
setTimeout(() => mocha.run(), 1000);
|
||||
</script>
|
|
@ -1,32 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Forum test 4</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" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2><a href="../">DGF Tests</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/mocha/mocha.js"></script>
|
||||
<script src="https://unpkg.com/chai/chai.js"></script>
|
||||
<script type="module" src="./scripts/forum/forum4.test.js"></script>
|
||||
<script defer class="mocha-init">
|
||||
mocha.setup({
|
||||
ui: 'bdd',
|
||||
globals: ['scene', 'dao', 'experts', 'posts', '__REACT_DEVTOOLS_*'],
|
||||
});
|
||||
mocha.checkLeaks();
|
||||
chai.should();
|
||||
</script>
|
||||
<script defer class="mocha-exec">
|
||||
// TODO: Weird race condition -- resolve this in a better way
|
||||
setTimeout(() => mocha.run(), 1000);
|
||||
</script>
|
|
@ -1,32 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Forum test 5</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" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2><a href="../">DGF Tests</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/mocha/mocha.js"></script>
|
||||
<script src="https://unpkg.com/chai/chai.js"></script>
|
||||
<script type="module" src="./scripts/forum/forum5.test.js"></script>
|
||||
<script defer class="mocha-init">
|
||||
mocha.setup({
|
||||
ui: 'bdd',
|
||||
globals: ['scene', 'dao', 'experts', 'posts', '__REACT_DEVTOOLS_*'],
|
||||
});
|
||||
mocha.checkLeaks();
|
||||
chai.should();
|
||||
</script>
|
||||
<script defer class="mocha-exec">
|
||||
// TODO: Weird race condition -- resolve this in a better way
|
||||
setTimeout(() => mocha.run(), 1000);
|
||||
</script>
|
|
@ -1,32 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Mocha Tests</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2><a href="../">DGF Tests</a></h2>
|
||||
<div id="mocha"></div>
|
||||
<div id="scene"></div>
|
||||
|
||||
<script src="https://unpkg.com/chai/chai.js"></script>
|
||||
<script src="https://unpkg.com/mocha/mocha.js"></script>
|
||||
|
||||
<script class="mocha-init">
|
||||
mocha.setup({
|
||||
ui: 'bdd',
|
||||
});
|
||||
mocha.checkLeaks();
|
||||
chai.should();
|
||||
</script>
|
||||
<script src="./scripts/mocha.test.js"></script>
|
||||
<script class="mocha-exec">
|
||||
mocha.run();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,35 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Reputation test</title>
|
||||
<link type="text/css" rel="stylesheet" href="../index.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2><a href="../">DGF Tests</a></h2>
|
||||
<div id="scene"></div>
|
||||
</body>
|
||||
<script type="module">
|
||||
import { Box } from '../classes/box.js';
|
||||
import { Scene } from '../classes/scene.js';
|
||||
// import { ValidationPool } from '../classes/validation-pool.js';
|
||||
// import { TokenHolder } from '../classes/token-holder.js';
|
||||
// import { ReputationToken } from '../classes/reputation-token.js';
|
||||
import { delay } from '../util.js';
|
||||
|
||||
const DEFAULT_DELAY_INTERVAL = 500;
|
||||
|
||||
const rootElement = document.getElementById('scene');
|
||||
const rootBox = new Box('rootBox', rootElement).flex();
|
||||
|
||||
const scene = (window.scene = new Scene('Reputation test', rootBox));
|
||||
scene.withSequenceDiagram();
|
||||
scene.withFlowchart();
|
||||
|
||||
// const pool = new ValidationPool();
|
||||
// const repToken = new ReputationToken();
|
||||
|
||||
// const tokenMinter = new TokenHolder('TokenMinter', scene);
|
||||
|
||||
await delay(DEFAULT_DELAY_INTERVAL);
|
||||
</script>
|
|
@ -1,169 +0,0 @@
|
|||
import { Box } from '../../classes/display/box.js';
|
||||
import { Scene } from '../../classes/display/scene.js';
|
||||
import { Expert } from '../../classes/actors/expert.js';
|
||||
import { delay } from '../../util.js';
|
||||
import { DAO } from '../../classes/actors/dao.js';
|
||||
import { Public } from '../../classes/actors/public.js';
|
||||
import { PostContent } from '../../classes/util/post-content.js';
|
||||
|
||||
const DELAY_INTERVAL = 100;
|
||||
const POOL_DURATION = 200;
|
||||
let dao;
|
||||
let experts;
|
||||
let requestor;
|
||||
let scene;
|
||||
|
||||
const newExpert = async () => {
|
||||
const index = experts.length;
|
||||
const name = `Expert${index + 1}`;
|
||||
const expert = await new Expert(dao, name, scene).initialize();
|
||||
expert.setValue(
|
||||
'rep',
|
||||
() => dao.reputation.valueOwnedBy(expert.reputationPublicKey),
|
||||
);
|
||||
experts.push(expert);
|
||||
return expert;
|
||||
};
|
||||
|
||||
const setup = async () => {
|
||||
const rootElement = document.getElementById('scene');
|
||||
const rootBox = new Box('rootBox', rootElement).flex();
|
||||
|
||||
scene = new Scene('Availability test', rootBox);
|
||||
scene.withSequenceDiagram();
|
||||
scene.withFlowchart();
|
||||
scene.withTable();
|
||||
|
||||
dao = new DAO('DGF', scene);
|
||||
await dao.setValue('total rep', () => dao.reputation.getTotal());
|
||||
|
||||
experts = [];
|
||||
|
||||
await newExpert();
|
||||
await newExpert();
|
||||
requestor = new Public('Public', scene);
|
||||
|
||||
await delay(DELAY_INTERVAL);
|
||||
|
||||
// Experts gain initial reputation by submitting a post with fee
|
||||
const { postId: postId1, pool: pool1 } = await experts[0].submitPostWithFee(
|
||||
new PostContent({ hello: 'there' }).setTitle('Post 1'),
|
||||
{
|
||||
fee: 10,
|
||||
duration: POOL_DURATION,
|
||||
tokenLossRatio: 1,
|
||||
},
|
||||
);
|
||||
await delay(POOL_DURATION);
|
||||
|
||||
await pool1.evaluateWinningConditions();
|
||||
await delay(DELAY_INTERVAL);
|
||||
|
||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(10);
|
||||
|
||||
const { pool: pool2 } = await experts[1].submitPostWithFee(
|
||||
new PostContent({ hello: 'to you as well' })
|
||||
.setTitle('Post 2')
|
||||
.addCitation(postId1, 0.5),
|
||||
{
|
||||
fee: 10,
|
||||
duration: POOL_DURATION,
|
||||
tokenLossRatio: 1,
|
||||
},
|
||||
);
|
||||
await delay(POOL_DURATION);
|
||||
|
||||
await pool2.evaluateWinningConditions();
|
||||
await delay(DELAY_INTERVAL);
|
||||
|
||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(15);
|
||||
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(5);
|
||||
};
|
||||
|
||||
const getActiveWorker = async () => {
|
||||
let worker;
|
||||
let request;
|
||||
for (const expert of experts) {
|
||||
request = await expert.getAssignedWork();
|
||||
if (request) {
|
||||
worker = expert;
|
||||
await worker.actions.getAssignedWork.log(worker, dao.availability);
|
||||
worker.activate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { worker, request };
|
||||
};
|
||||
|
||||
const voteForWorkEvidence = async (worker, pool) => {
|
||||
for (const expert of experts) {
|
||||
if (expert !== worker) {
|
||||
await expert.stake(pool, {
|
||||
position: true,
|
||||
amount: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe('Availability + Business', () => {
|
||||
before(async () => {
|
||||
await setup();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// await scene.sequence.startSection();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// await scene.sequence.endSection();
|
||||
});
|
||||
|
||||
it('Experts can register their availability for some duration', async () => {
|
||||
await experts[0].registerAvailability(1, 10000);
|
||||
await experts[1].registerAvailability(1, 10000);
|
||||
await delay(DELAY_INTERVAL);
|
||||
});
|
||||
|
||||
it('Public can submit a work request', async () => {
|
||||
await requestor.submitRequest(
|
||||
dao.business,
|
||||
{ fee: 100 },
|
||||
{ please: 'do some work' },
|
||||
);
|
||||
await delay(DELAY_INTERVAL);
|
||||
});
|
||||
|
||||
it('Expert can submit work evidence', async () => {
|
||||
// Receive work request
|
||||
const { worker, request } = await getActiveWorker();
|
||||
const pool3 = await worker.submitWork(
|
||||
request.id,
|
||||
{
|
||||
here: 'is some evidence of work product',
|
||||
},
|
||||
{
|
||||
tokenLossRatio: 1,
|
||||
duration: POOL_DURATION,
|
||||
},
|
||||
);
|
||||
await worker.deactivate();
|
||||
|
||||
// Stake on work evidence
|
||||
await voteForWorkEvidence(worker, pool3);
|
||||
|
||||
// Wait for validation pool duration to elapse
|
||||
await delay(POOL_DURATION);
|
||||
|
||||
// Distribute reputation awards and fees
|
||||
await pool3.evaluateWinningConditions();
|
||||
await delay(DELAY_INTERVAL);
|
||||
|
||||
// This should throw an exception since the pool is already resolved
|
||||
try {
|
||||
await pool3.evaluateWinningConditions();
|
||||
} catch (e) {
|
||||
e.should.match(/Validation pool has already been resolved/);
|
||||
}
|
||||
}).timeout(10000);
|
||||
});
|
|
@ -1,67 +0,0 @@
|
|||
import { Box } from '../../classes/display/box.js';
|
||||
import { Scene } from '../../classes/display/scene.js';
|
||||
import { Actor } from '../../classes/display/actor.js';
|
||||
import { Action } from '../../classes/display/action.js';
|
||||
|
||||
const rootElement = document.getElementById('basic');
|
||||
const rootBox = new Box('rootBox', rootElement).flex();
|
||||
|
||||
function randomDelay(min, max) {
|
||||
const delayMs = min + Math.random() * max;
|
||||
return delayMs;
|
||||
}
|
||||
|
||||
(function run() {
|
||||
const scene = new Scene('Scene 1', rootBox).withSequenceDiagram();
|
||||
const webClientStatus = scene.addDisplayValue('WebClient Status');
|
||||
const node1Status = scene.addDisplayValue('Node 1 Status');
|
||||
const blockchainStatus = scene.addDisplayValue('Blockchain Status');
|
||||
|
||||
const webClient = new Actor('web client', scene);
|
||||
const node1 = new Actor('node 1', scene);
|
||||
const blockchain = new Actor('blockchain', scene);
|
||||
const requestForumPage = new Action('requestForumPage', scene);
|
||||
const readBlockchainData = new Action('readBlockchainData', scene);
|
||||
const blockchainData = new Action('blockchainData', scene);
|
||||
const forumPage = new Action('forumPage', scene);
|
||||
|
||||
webClientStatus.set('Initialized');
|
||||
node1Status.set('Idle');
|
||||
blockchainStatus.set('Idle');
|
||||
|
||||
node1.on(requestForumPage, (src, detail) => {
|
||||
node1Status.set('Processing request');
|
||||
node1.on(blockchainData, (_src, data) => {
|
||||
node1Status.set('Processing response');
|
||||
setTimeout(() => {
|
||||
node1.send(src, forumPage, data);
|
||||
node1Status.set('Idle');
|
||||
}, randomDelay(500, 1000));
|
||||
});
|
||||
setTimeout(() => {
|
||||
node1.send(blockchain, readBlockchainData, detail);
|
||||
}, randomDelay(500, 1500));
|
||||
});
|
||||
|
||||
blockchain.on(readBlockchainData, (src, _detail) => {
|
||||
blockchainStatus.set('Processing request');
|
||||
setTimeout(() => {
|
||||
blockchain.send(src, blockchainData, {});
|
||||
blockchainStatus.set('Idle');
|
||||
}, randomDelay(500, 1500));
|
||||
});
|
||||
|
||||
webClient.on(forumPage, (_src, _detail) => {
|
||||
webClientStatus.set('Received forum page');
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
webClient.send(node1, requestForumPage);
|
||||
webClientStatus.set('Requested forum page');
|
||||
}, randomDelay(500, 1500));
|
||||
|
||||
setInterval(() => {
|
||||
webClient.send(node1, requestForumPage);
|
||||
webClientStatus.set('Requested forum page');
|
||||
}, randomDelay(6000, 12000));
|
||||
}());
|
|
@ -1,139 +0,0 @@
|
|||
import { Box } from '../../classes/display/box.js';
|
||||
import { Scene } from '../../classes/display/scene.js';
|
||||
import { Actor } from '../../classes/display/actor.js';
|
||||
|
||||
const rootElement = document.getElementById('basic');
|
||||
const rootBox = new Box('rootBox', rootElement).flex();
|
||||
|
||||
function delay(min, max = min) {
|
||||
const delayMs = min + Math.random() * (max - min);
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, delayMs);
|
||||
});
|
||||
}
|
||||
|
||||
(async function run() {
|
||||
const scene = new Scene('Scene 2', rootBox).withSequenceDiagram();
|
||||
|
||||
const webClient = new Actor('webClient', scene);
|
||||
|
||||
const nodes = [];
|
||||
const memories = [];
|
||||
const storages = [];
|
||||
|
||||
async function addNode() {
|
||||
const idx = nodes.length;
|
||||
const node = new Actor(`node${idx}`, scene);
|
||||
const memory = new Actor(`memory${idx}`, scene);
|
||||
const storage = new Actor(`storage${idx}`, scene);
|
||||
node.memory = memory;
|
||||
node.storage = storage;
|
||||
nodes.push(node);
|
||||
memories.push(memory);
|
||||
storages.push(storage);
|
||||
return node;
|
||||
}
|
||||
|
||||
function getPeer(node) {
|
||||
const peers = nodes.filter((peer) => peer !== node);
|
||||
const idx = Math.floor(Math.random() * peers.length);
|
||||
return peers[idx];
|
||||
}
|
||||
|
||||
await addNode();
|
||||
await addNode();
|
||||
|
||||
const [
|
||||
seekTruth,
|
||||
considerInfo,
|
||||
evaluateConfidence,
|
||||
chooseResponse,
|
||||
qualifiedOpinions,
|
||||
requestMemoryData,
|
||||
memoryData,
|
||||
requestStorageData,
|
||||
storageData,
|
||||
] = [
|
||||
'seek truth',
|
||||
'consider available information',
|
||||
'evaluate confidence',
|
||||
'choose response',
|
||||
'qualified opinions',
|
||||
'request in-memory data',
|
||||
'in-memory data',
|
||||
'request storage data',
|
||||
'storage data',
|
||||
].map((name) => scene.addAction(name));
|
||||
|
||||
memories.forEach((memory) => {
|
||||
memory.setStatus('Idle');
|
||||
memory.on(requestMemoryData, async (src, _detail) => {
|
||||
memory.setStatus('Retrieving data');
|
||||
await delay(1000);
|
||||
memory.send(src, memoryData, {});
|
||||
memory.setStatus('Idle');
|
||||
});
|
||||
});
|
||||
|
||||
storages.forEach((storage) => {
|
||||
storage.setStatus('Idle');
|
||||
storage.on(requestStorageData, async (src, _detail) => {
|
||||
storage.setStatus('Retrieving data');
|
||||
await delay(1000);
|
||||
storage.send(src, storageData, {});
|
||||
storage.setStatus('Idle');
|
||||
});
|
||||
});
|
||||
|
||||
nodes.forEach((node) => {
|
||||
node.setStatus('Idle');
|
||||
node.on(seekTruth, async (seeker, detail) => {
|
||||
node.setStatus('Processing request');
|
||||
|
||||
node.on(chooseResponse, async (_src, _info) => {
|
||||
node.setStatus('Choosing response');
|
||||
await delay(1000);
|
||||
node.send(seeker, qualifiedOpinions, {});
|
||||
node.setStatus('Idle');
|
||||
});
|
||||
|
||||
node.on(evaluateConfidence, async (_src, _info) => {
|
||||
node.setStatus('Evaluating confidence');
|
||||
await delay(1000);
|
||||
node.send(node, chooseResponse);
|
||||
});
|
||||
|
||||
node.on(considerInfo, async (_src, _info) => {
|
||||
node.setStatus('Considering info');
|
||||
await delay(1000);
|
||||
node.send(node, evaluateConfidence);
|
||||
});
|
||||
|
||||
node.on(memoryData, (_src, _data) => {
|
||||
node.on(storageData, (__src, __data) => {
|
||||
if (detail?.readConcern === 'single') {
|
||||
node.send(node, considerInfo, {});
|
||||
} else {
|
||||
const peer = getPeer(node);
|
||||
node.on(qualifiedOpinions, (___src, info) => {
|
||||
node.send(node, considerInfo, info);
|
||||
});
|
||||
node.send(peer, seekTruth, { readConcern: 'single' });
|
||||
}
|
||||
});
|
||||
node.send(node.storage, requestStorageData);
|
||||
});
|
||||
|
||||
await delay(1000);
|
||||
node.send(node.memory, requestMemoryData);
|
||||
});
|
||||
});
|
||||
|
||||
webClient.on(qualifiedOpinions, (_src, _detail) => {
|
||||
webClient.setStatus('Received opinions and qualifications');
|
||||
});
|
||||
|
||||
await delay(1000);
|
||||
webClient.setStatus('Seek truth');
|
||||
webClient.send(nodes[0], seekTruth);
|
||||
}());
|
|
@ -1,16 +0,0 @@
|
|||
import { Business } from '../../classes/actors/business.js';
|
||||
import { Scene } from '../../classes/display/scene.js';
|
||||
import { Box } from '../../classes/display/box.js';
|
||||
|
||||
describe('Business', () => {
|
||||
let scene;
|
||||
before(async () => {
|
||||
const rootElement = document.getElementById('scene');
|
||||
const rootBox = new Box('rootBox', rootElement).flex();
|
||||
scene = new Scene('Business', rootBox);
|
||||
});
|
||||
it('Should exist', () => {
|
||||
const business = new Business(null, 'Business', scene);
|
||||
should.exist(business);
|
||||
});
|
||||
});
|
|
@ -1,72 +0,0 @@
|
|||
import { Box } from '../../classes/display/box.js';
|
||||
import { Scene } from '../../classes/display/scene.js';
|
||||
import { Action } from '../../classes/display/action.js';
|
||||
import { Actor } from '../../classes/display/actor.js';
|
||||
import { debounce, delay } from '../../util.js';
|
||||
|
||||
describe('Debounce', () => {
|
||||
let scene;
|
||||
let caller;
|
||||
let debouncer;
|
||||
let method;
|
||||
|
||||
let call;
|
||||
let execute;
|
||||
|
||||
before(() => {
|
||||
const rootElement = document.getElementById('scene');
|
||||
const rootBox = new Box('rootBox', rootElement).flex();
|
||||
|
||||
scene = new Scene('Debounce test', rootBox).withSequenceDiagram();
|
||||
caller = new Actor('Caller', scene);
|
||||
debouncer = new Actor('Debouncer', scene);
|
||||
method = new Actor('Target method', scene);
|
||||
|
||||
call = new Action('call', scene);
|
||||
execute = new Action('execute', scene);
|
||||
});
|
||||
|
||||
it('Suppresses extra events that occur within the specified window', async () => {
|
||||
let eventCount = 0;
|
||||
const event = sinon.spy(async () => {
|
||||
eventCount++;
|
||||
await execute.log(debouncer, method, eventCount);
|
||||
});
|
||||
|
||||
await scene.sequence.startSection();
|
||||
await call.log(caller, debouncer, '1');
|
||||
await debounce(event, 500);
|
||||
await call.log(caller, debouncer, '2');
|
||||
await debounce(event, 500);
|
||||
|
||||
await delay(500);
|
||||
event.should.have.been.calledOnce;
|
||||
|
||||
await call.log(caller, debouncer, '3');
|
||||
await debounce(event, 500);
|
||||
await call.log(caller, debouncer, '4');
|
||||
await debounce(event, 500);
|
||||
|
||||
eventCount.should.equal(2);
|
||||
event.should.have.been.calledTwice;
|
||||
await scene.sequence.endSection();
|
||||
});
|
||||
|
||||
it('Propagates exceptions', async () => {
|
||||
const event = sinon.spy(async () => {
|
||||
await execute.log(debouncer, method, undefined, undefined, '-x');
|
||||
throw new Error('An error occurs in the callback');
|
||||
});
|
||||
await scene.sequence.startSection();
|
||||
|
||||
try {
|
||||
await call.log(caller, debouncer);
|
||||
await debounce(event, 500);
|
||||
} catch (e) {
|
||||
event.should.have.been.calledOnce;
|
||||
e.should.exist;
|
||||
e.should.match(/An error occurs in the callback/);
|
||||
}
|
||||
await scene.sequence.endSection();
|
||||
});
|
||||
});
|
|
@ -1,76 +0,0 @@
|
|||
import { Box } from '../../classes/display/box.js';
|
||||
import { Scene } from '../../classes/display/scene.js';
|
||||
import { PostContent } from '../../classes/util/post-content.js';
|
||||
import { Expert } from '../../classes/actors/expert.js';
|
||||
import { ForumNode } from '../../classes/forum-network/forum-node.js';
|
||||
import { Network } from '../../classes/forum-network/network.js';
|
||||
import { delay, randomID } from '../../util.js';
|
||||
|
||||
describe('Forum Network', () => {
|
||||
let scene;
|
||||
let author1;
|
||||
let author2;
|
||||
let forumNetwork;
|
||||
let forumNode1;
|
||||
let forumNode2;
|
||||
let forumNode3;
|
||||
let processInterval;
|
||||
|
||||
before(async () => {
|
||||
const rootElement = document.getElementById('scene');
|
||||
const rootBox = new Box('rootBox', rootElement).flex();
|
||||
|
||||
scene = new Scene('Forum Network test', rootBox).withSequenceDiagram();
|
||||
|
||||
author1 = await new Expert(null, 'author1', scene).initialize();
|
||||
author2 = await new Expert(null, 'author2', scene).initialize();
|
||||
|
||||
forumNetwork = new Network();
|
||||
|
||||
forumNode1 = await new ForumNode('node1', scene).initialize(
|
||||
forumNetwork,
|
||||
);
|
||||
forumNode2 = await new ForumNode('node2', scene).initialize(
|
||||
forumNetwork,
|
||||
);
|
||||
forumNode3 = await new ForumNode('node3', scene).initialize(
|
||||
forumNetwork,
|
||||
);
|
||||
|
||||
processInterval = setInterval(async () => {
|
||||
await forumNode1.processNextMessage();
|
||||
await forumNode2.processNextMessage();
|
||||
await forumNode3.processNextMessage();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
clearInterval(processInterval);
|
||||
});
|
||||
|
||||
// const blockchain = new Blockchain();
|
||||
|
||||
specify('Author can submit a post to the network', async () => {
|
||||
const post1 = new PostContent({ message: 'hi' });
|
||||
post1.id = randomID();
|
||||
const post2 = new PostContent({ message: 'hello' }).addCitation(
|
||||
post1.id,
|
||||
1.0,
|
||||
);
|
||||
|
||||
await delay(1000);
|
||||
await author1.submitPostViaNetwork(
|
||||
forumNode1,
|
||||
post1,
|
||||
50,
|
||||
);
|
||||
await delay(1000);
|
||||
await author2.submitPostViaNetwork(
|
||||
forumNode2,
|
||||
post2,
|
||||
100,
|
||||
);
|
||||
|
||||
await delay(1000);
|
||||
}).timeout(10000);
|
||||
});
|
|
@ -1,87 +0,0 @@
|
|||
import { Box } from '../../../classes/display/box.js';
|
||||
import { Scene } from '../../../classes/display/scene.js';
|
||||
import { Expert } from '../../../classes/actors/expert.js';
|
||||
import { PostContent } from '../../../classes/util/post-content.js';
|
||||
import { delay } from '../../../util.js';
|
||||
import params from '../../../params.js';
|
||||
import { DAO } from '../../../classes/actors/dao.js';
|
||||
|
||||
export class ForumTest {
|
||||
constructor(options) {
|
||||
this.scene = null;
|
||||
this.dao = null;
|
||||
this.experts = null;
|
||||
this.posts = null;
|
||||
this.options = {
|
||||
defaultDelayMs: 1,
|
||||
poolDurationMs: 50,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
async addPost(author, fee, citations = []) {
|
||||
const postIndex = this.posts.length;
|
||||
const title = `posts[${postIndex}]`;
|
||||
await this.scene.sequence.startSection();
|
||||
|
||||
const postContent = new PostContent({}).setTitle(title);
|
||||
for (const { postId, weight } of citations) {
|
||||
postContent.addCitation(postId, weight);
|
||||
}
|
||||
|
||||
const { pool, postId } = await author.submitPostWithFee(
|
||||
postContent,
|
||||
{
|
||||
fee,
|
||||
duration: this.options.poolDurationMs,
|
||||
tokenLossRatio: 1,
|
||||
},
|
||||
);
|
||||
this.posts.push(postId);
|
||||
await delay(this.options.poolDurationMs);
|
||||
await pool.evaluateWinningConditions();
|
||||
await this.scene.sequence.endSection();
|
||||
await delay(this.options.defaultDelayMs);
|
||||
return postId;
|
||||
}
|
||||
|
||||
async newExpert() {
|
||||
const index = this.experts.length;
|
||||
const name = `Expert${index + 1}`;
|
||||
const expert = await new Expert(this.dao, name, this.scene).initialize();
|
||||
this.experts.push(expert);
|
||||
// await expert.addComputedValue('rep', () => this.dao.reputation.valueOwnedBy(expert.reputationPublicKey));
|
||||
return expert;
|
||||
}
|
||||
|
||||
async setup() {
|
||||
const rootElement = document.getElementById('scene');
|
||||
const rootBox = new Box('rootBox', rootElement).flex();
|
||||
|
||||
const scene = this.scene = new Scene('Forum test', rootBox);
|
||||
scene.withSequenceDiagram();
|
||||
scene.withFlowchart();
|
||||
scene.withTable();
|
||||
|
||||
scene.addDisplayValue('c3. stakeForAuthor').set(params.stakeForAuthor);
|
||||
scene.addDisplayValue('q2. revaluationLimit').set(params.revaluationLimit);
|
||||
scene
|
||||
.addDisplayValue('q3. referenceChainLimit')
|
||||
.set(params.referenceChainLimit);
|
||||
scene.addDisplayValue('q4. leachingValue').set(params.leachingValue);
|
||||
scene.addDisplayValue(' ');
|
||||
|
||||
this.dao = new DAO('DAO', scene);
|
||||
this.forum = this.dao.forum;
|
||||
this.experts = [];
|
||||
this.posts = [];
|
||||
|
||||
await this.newExpert();
|
||||
// await newExpert();
|
||||
// await newExpert();
|
||||
|
||||
await this.dao.addComputedValue('total value', () => this.dao.reputation.getTotal());
|
||||
// await this.dao.addComputedValue('total reputation', () => this.dao.forum.getTotalValue());
|
||||
this.dao.computeValues();
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import { ForumTest } from './forum.test-util.js';
|
||||
|
||||
describe('Forum', () => {
|
||||
const forumTest = new ForumTest();
|
||||
|
||||
before(async () => {
|
||||
await forumTest.setup();
|
||||
});
|
||||
|
||||
context('Negative citation of a negative citation', async () => {
|
||||
it('Post1', async () => {
|
||||
const { forum, experts, posts } = forumTest;
|
||||
await forumTest.addPost(experts[0], 10);
|
||||
forum.getPost(posts[0]).value.should.equal(10);
|
||||
});
|
||||
|
||||
it('Post2 negatively cites Post1', async () => {
|
||||
const { forum, experts, posts } = forumTest;
|
||||
await forumTest.addPost(experts[0], 10, [{ postId: posts[0], weight: -1 }]);
|
||||
forum.getPost(posts[0]).value.should.equal(0);
|
||||
forum.getPost(posts[1]).value.should.equal(20);
|
||||
});
|
||||
|
||||
it('Post3 negatively cites Post2, restoring Post1 post to its initial value', async () => {
|
||||
const { forum, experts, posts } = forumTest;
|
||||
await forumTest.addPost(experts[0], 10, [{ postId: posts[1], weight: -1 }]);
|
||||
forum.getPost(posts[0]).value.should.equal(10);
|
||||
forum.getPost(posts[1]).value.should.equal(0);
|
||||
forum.getPost(posts[2]).value.should.equal(20);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// await addPost(experts[0], 10);
|
||||
// await addPost(experts[0], 10, [{ postId: posts[3], weight: -1 }]);
|
||||
// await addPost(experts[0], 10, [{ postId: posts[4], weight: -1 }]);
|
||||
|
||||
// await addPost(expert3, 'Post 4', 100, [{ postId: postId2, weight: -1 }]);
|
||||
// await addPost(expert1, 'Post 5', 100, [{ postId: postId3, weight: -1 }]);
|
|
@ -1,39 +0,0 @@
|
|||
import { ForumTest } from './forum.test-util.js';
|
||||
|
||||
describe('Forum', () => {
|
||||
const forumTest = new ForumTest();
|
||||
|
||||
before(async () => {
|
||||
await forumTest.setup();
|
||||
});
|
||||
|
||||
context('Negative citation of a weaker negative citation', async () => {
|
||||
it('Post4', async () => {
|
||||
const { forum, experts, posts } = forumTest;
|
||||
await forumTest.addPost(experts[0], 10);
|
||||
forum.getPost(posts[0]).value.should.equal(10);
|
||||
});
|
||||
|
||||
it('Post5 negatively cites Post4', async () => {
|
||||
const { forum, experts, posts } = forumTest;
|
||||
await forumTest.addPost(experts[0], 10, [{ postId: posts[0], weight: -0.5 }]);
|
||||
forum.getPost(posts[0]).value.should.equal(5);
|
||||
forum.getPost(posts[1]).value.should.equal(15);
|
||||
});
|
||||
|
||||
it('Post6 negatively cites Post5, restoring Post4 post to its initial value', async () => {
|
||||
const { forum, experts, posts } = forumTest;
|
||||
await forumTest.addPost(experts[0], 20, [{ postId: posts[1], weight: -1 }]);
|
||||
forum.getPost(posts[0]).value.should.equal(10);
|
||||
forum.getPost(posts[1]).value.should.equal(0);
|
||||
forum.getPost(posts[2]).value.should.equal(30);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// await addPost(experts[0], 10);
|
||||
// await addPost(experts[0], 10, [{ postId: posts[3], weight: -1 }]);
|
||||
// await addPost(experts[0], 10, [{ postId: posts[4], weight: -1 }]);
|
||||
|
||||
// await addPost(expert3, 'Post 4', 100, [{ postId: postId2, weight: -1 }]);
|
||||
// await addPost(expert1, 'Post 5', 100, [{ postId: postId3, weight: -1 }]);
|
|
@ -1,42 +0,0 @@
|
|||
import { ForumTest } from './forum.test-util.js';
|
||||
|
||||
describe('Forum', () => {
|
||||
const forumTest = new ForumTest();
|
||||
|
||||
before(async () => {
|
||||
await forumTest.setup();
|
||||
});
|
||||
|
||||
context('Redistribute power', async () => {
|
||||
it('Post1', async () => {
|
||||
const { forum, experts, posts } = forumTest;
|
||||
await forumTest.addPost(experts[0], 10);
|
||||
forum.getPost(posts[0]).value.should.equal(10);
|
||||
});
|
||||
|
||||
it('Post2', async () => {
|
||||
const { forum, experts, posts } = forumTest;
|
||||
await forumTest.addPost(experts[0], 10);
|
||||
forum.getPost(posts[0]).value.should.equal(10);
|
||||
forum.getPost(posts[1]).value.should.equal(10);
|
||||
});
|
||||
|
||||
it('Post3 cites Post2 and negatively cites Post1', async () => {
|
||||
const { forum, experts, posts } = forumTest;
|
||||
await forumTest.addPost(experts[0], 10, [
|
||||
{ postId: posts[0], weight: -1 },
|
||||
{ postId: posts[1], weight: 1 },
|
||||
]);
|
||||
forum.getPost(posts[0]).value.should.equal(0);
|
||||
forum.getPost(posts[1]).value.should.equal(30);
|
||||
forum.getPost(posts[2]).value.should.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// await addPost(experts[0], 10);
|
||||
// await addPost(experts[0], 10, [{ postId: posts[3], weight: -1 }]);
|
||||
// await addPost(experts[0], 10, [{ postId: posts[4], weight: -1 }]);
|
||||
|
||||
// await addPost(expert3, 'Post 4', 100, [{ postId: postId2, weight: -1 }]);
|
||||
// await addPost(expert1, 'Post 5', 100, [{ postId: postId3, weight: -1 }]);
|
|
@ -1,70 +0,0 @@
|
|||
import { ForumTest } from './forum.test-util.js';
|
||||
|
||||
describe('Forum', () => {
|
||||
const forumTest = new ForumTest();
|
||||
|
||||
before(async () => {
|
||||
await forumTest.setup();
|
||||
});
|
||||
|
||||
context('Redistribute power through subsequent support', async () => {
|
||||
let forum;
|
||||
let experts;
|
||||
let posts;
|
||||
|
||||
before(() => {
|
||||
forum = forumTest.forum;
|
||||
experts = forumTest.experts;
|
||||
posts = forumTest.posts;
|
||||
});
|
||||
|
||||
it('Post1', async () => {
|
||||
await forumTest.addPost(experts[0], 10);
|
||||
forum.getPost(posts[0]).value.should.equal(10);
|
||||
});
|
||||
|
||||
it('Post2', async () => {
|
||||
await forumTest.addPost(experts[0], 10);
|
||||
forum.getPost(posts[0]).value.should.equal(10);
|
||||
forum.getPost(posts[1]).value.should.equal(10);
|
||||
});
|
||||
|
||||
it('Post3 cites Post2 and negatively cites Post1', async () => {
|
||||
await forumTest.addPost(experts[0], 0, [
|
||||
{ postId: posts[0], weight: -1 },
|
||||
{ postId: posts[1], weight: 1 },
|
||||
]);
|
||||
forum.getPost(posts[0]).value.should.equal(10);
|
||||
forum.getPost(posts[1]).value.should.equal(10);
|
||||
forum.getPost(posts[2]).value.should.equal(0);
|
||||
});
|
||||
|
||||
it('Post4 cites Post3 to strengthen its effect', async () => {
|
||||
await forumTest.addPost(experts[0], 10, [
|
||||
{ postId: posts[2], weight: 1 },
|
||||
]);
|
||||
forum.getPost(posts[0]).value.should.equal(0);
|
||||
forum.getPost(posts[1]).value.should.equal(30);
|
||||
forum.getPost(posts[2]).value.should.equal(0);
|
||||
forum.getPost(posts[3]).value.should.equal(0);
|
||||
});
|
||||
|
||||
it('Post5 cites Post3 to strengthen its effect', async () => {
|
||||
await forumTest.addPost(experts[0], 10, [
|
||||
{ postId: posts[2], weight: 1 },
|
||||
]);
|
||||
forum.getPost(posts[0]).value.should.equal(0);
|
||||
forum.getPost(posts[1]).value.should.equal(40);
|
||||
forum.getPost(posts[2]).value.should.equal(0);
|
||||
forum.getPost(posts[3]).value.should.equal(0);
|
||||
forum.getPost(posts[4]).value.should.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// await addPost(experts[0], 10);
|
||||
// await addPost(experts[0], 10, [{ postId: posts[3], weight: -1 }]);
|
||||
// await addPost(experts[0], 10, [{ postId: posts[4], weight: -1 }]);
|
||||
|
||||
// await addPost(expert3, 'Post 4', 100, [{ postId: postId2, weight: -1 }]);
|
||||
// await addPost(expert1, 'Post 5', 100, [{ postId: postId3, weight: -1 }]);
|
|
@ -1,59 +0,0 @@
|
|||
import { ForumTest } from './forum.test-util.js';
|
||||
|
||||
describe('Forum', () => {
|
||||
const forumTest = new ForumTest();
|
||||
|
||||
before(async () => {
|
||||
await forumTest.setup();
|
||||
});
|
||||
|
||||
context('Destroy a post after it has received positive citations', async () => {
|
||||
let forum;
|
||||
let experts;
|
||||
let posts;
|
||||
|
||||
before(() => {
|
||||
forum = forumTest.forum;
|
||||
experts = forumTest.experts;
|
||||
posts = forumTest.posts;
|
||||
});
|
||||
|
||||
it('Post1', async () => {
|
||||
await forumTest.addPost(experts[0], 100);
|
||||
forum.getPost(posts[0]).value.should.equal(100);
|
||||
});
|
||||
|
||||
it('Post2 negatively cites Post1', async () => {
|
||||
await forumTest.addPost(experts[0], 10, [
|
||||
{ postId: posts[0], weight: -0.5 },
|
||||
]);
|
||||
forum.getPost(posts[0]).value.should.equal(95);
|
||||
forum.getPost(posts[1]).value.should.equal(15);
|
||||
});
|
||||
|
||||
it('Post3 positively cites Post2', async () => {
|
||||
await forumTest.addPost(experts[0], 50, [
|
||||
{ postId: posts[1], weight: 0.5 },
|
||||
]);
|
||||
forum.getPost(posts[0]).value.should.equal(95 - 12.5);
|
||||
forum.getPost(posts[1]).value.should.equal(15 + 25 + 12.5);
|
||||
forum.getPost(posts[2]).value.should.equal(25);
|
||||
});
|
||||
|
||||
it('Post4 negatively cites Post2', async () => {
|
||||
await forumTest.addPost(experts[0], 100, [
|
||||
{ postId: posts[1], weight: -1 },
|
||||
]);
|
||||
// forum.getPost(posts[0]).value.should.equal(95 - 12.5);
|
||||
// forum.getPost(posts[1]).value.should.equal(15 + 25 + 12.5);
|
||||
// forum.getPost(posts[2]).value.should.equal(25);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// await addPost(experts[0], 10);
|
||||
// await addPost(experts[0], 10, [{ postId: posts[3], weight: -1 }]);
|
||||
// await addPost(experts[0], 10, [{ postId: posts[4], weight: -1 }]);
|
||||
|
||||
// await addPost(expert3, 'Post 4', 100, [{ postId: postId2, weight: -1 }]);
|
||||
// await addPost(expert1, 'Post 5', 100, [{ postId: postId3, weight: -1 }]);
|
|
@ -1,23 +0,0 @@
|
|||
describe('Array', () => {
|
||||
before(() => {
|
||||
// ...
|
||||
});
|
||||
|
||||
describe('#indexOf()', () => {
|
||||
context('when not present', () => {
|
||||
it('should not throw an error', () => {
|
||||
(function aFunc() {
|
||||
[1, 2, 3].indexOf(4);
|
||||
}.should.not.throw());
|
||||
});
|
||||
it('should return -1', () => {
|
||||
[1, 2, 3].indexOf(4).should.equal(-1);
|
||||
});
|
||||
});
|
||||
context('when present', () => {
|
||||
it('should return the index where the element first appears in the array', () => {
|
||||
[1, 2, 3].indexOf(3).should.equal(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,111 +0,0 @@
|
|||
import { Box } from '../../classes/display/box.js';
|
||||
import { Scene } from '../../classes/display/scene.js';
|
||||
import { Expert } from '../../classes/actors/expert.js';
|
||||
import { PostContent } from '../../classes/util/post-content.js';
|
||||
import { delay } from '../../util.js';
|
||||
import { DAO } from '../../classes/actors/dao.js';
|
||||
|
||||
const POOL_DURATION_MS = 100;
|
||||
const DEFAULT_DELAY_MS = 100;
|
||||
|
||||
let scene;
|
||||
let experts;
|
||||
let dao;
|
||||
|
||||
async function newExpert() {
|
||||
const index = experts.length;
|
||||
const name = `Expert${index + 1}`;
|
||||
const expert = await new Expert(dao, name, scene).initialize();
|
||||
await expert.addComputedValue('rep', () => dao.reputation.valueOwnedBy(expert.reputationPublicKey));
|
||||
experts.push(expert);
|
||||
return expert;
|
||||
}
|
||||
|
||||
async function setup() {
|
||||
const rootElement = document.getElementById('scene');
|
||||
const rootBox = new Box('rootBox', rootElement).flex();
|
||||
|
||||
scene = (window.scene = new Scene('Validation Pool test', rootBox));
|
||||
scene.withSequenceDiagram();
|
||||
scene.withTable();
|
||||
|
||||
dao = new DAO('DGF', scene);
|
||||
|
||||
experts = [];
|
||||
await newExpert();
|
||||
await newExpert();
|
||||
|
||||
await delay(DEFAULT_DELAY_MS);
|
||||
}
|
||||
|
||||
describe('Validation Pool', () => {
|
||||
before(async () => {
|
||||
await setup();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await scene.sequence.startSection();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await scene.sequence.endSection();
|
||||
});
|
||||
|
||||
it('First expert can self-approve', async () => {
|
||||
await scene.sequence.startSection();
|
||||
const { pool } = await experts[0].submitPostWithFee(new PostContent(), {
|
||||
fee: 7,
|
||||
duration: POOL_DURATION_MS,
|
||||
tokenLossRatio: 1,
|
||||
});
|
||||
// Attempting to evaluate winning conditions before the duration has expired
|
||||
// should result in an exception
|
||||
try {
|
||||
await pool.evaluateWinningConditions();
|
||||
} catch (e) {
|
||||
if (e.message.match(/Validation pool duration has not yet elapsed/)) {
|
||||
console.log(
|
||||
'Caught expected error: Validation pool duration has not yet elapsed',
|
||||
);
|
||||
} else {
|
||||
console.error('Unexpected error');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
await scene.sequence.endSection();
|
||||
await delay(POOL_DURATION_MS);
|
||||
await pool.evaluateWinningConditions(); // Vote passes
|
||||
await delay(DEFAULT_DELAY_MS);
|
||||
});
|
||||
|
||||
it('Failure example: second expert can not self-approve', async () => {
|
||||
try {
|
||||
const { pool } = await experts[1].submitPostWithFee(new PostContent(), {
|
||||
fee: 1,
|
||||
duration: POOL_DURATION_MS,
|
||||
tokenLossRatio: 1,
|
||||
});
|
||||
await delay(POOL_DURATION_MS);
|
||||
await pool.evaluateWinningConditions(); // Quorum not met!
|
||||
await delay(DEFAULT_DELAY_MS);
|
||||
} catch (e) {
|
||||
e.message.should.match(/Quorum is not met/);
|
||||
}
|
||||
});
|
||||
|
||||
it('Second expert must be approved by first expert', async () => {
|
||||
const { pool } = await experts[1].submitPostWithFee(new PostContent(), {
|
||||
fee: 1,
|
||||
duration: POOL_DURATION_MS,
|
||||
tokenLossRatio: 1,
|
||||
});
|
||||
await experts[0].stake(pool, {
|
||||
position: true,
|
||||
amount: 4,
|
||||
lockingTime: 0,
|
||||
});
|
||||
await delay(POOL_DURATION_MS);
|
||||
await pool.evaluateWinningConditions(); // Stake passes
|
||||
await delay(DEFAULT_DELAY_MS);
|
||||
});
|
||||
});
|
|
@ -1,70 +0,0 @@
|
|||
import { Actor } from '../../classes/display/actor.js';
|
||||
import { Box } from '../../classes/display/box.js';
|
||||
import { Scene } from '../../classes/display/scene.js';
|
||||
import { VM } from '../../classes/supporting/vm.js';
|
||||
|
||||
const contractIds = ['contract-id-1', 'contract-id-2'];
|
||||
|
||||
class Greeter extends Actor {
|
||||
constructor(vm, value, scene) {
|
||||
super('Greeter', scene);
|
||||
this.vm = vm;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
hello(sender, message) {
|
||||
return `${sender.name} ${this.value}: ${message}`;
|
||||
}
|
||||
}
|
||||
|
||||
class Repeater extends Actor {
|
||||
constructor(vm, greeter, scene) {
|
||||
super('Repeater', scene);
|
||||
this.vmHandle = vm.getHandle(this);
|
||||
this.greeter = greeter;
|
||||
}
|
||||
|
||||
forward(__sender, method, message) {
|
||||
return this.vmHandle.callContract(this.greeter, method, message);
|
||||
}
|
||||
}
|
||||
|
||||
describe('VM', () => {
|
||||
let vm;
|
||||
let sender;
|
||||
let vmHandle;
|
||||
let scene;
|
||||
|
||||
before(() => {
|
||||
const rootElement = document.getElementById('scene');
|
||||
const rootBox = new Box('rootBox', rootElement).flex();
|
||||
scene = new Scene('VM test', rootBox).withSequenceDiagram();
|
||||
vm = new VM(scene);
|
||||
sender = new Actor('Sender', scene);
|
||||
vm.addContract(contractIds[0], Greeter, 'world', scene);
|
||||
vm.addContract(contractIds[1], Repeater, contractIds[0], scene);
|
||||
vmHandle = vm.getHandle(sender);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await scene.sequence.startSection();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await scene.sequence.endSection();
|
||||
});
|
||||
|
||||
it('Should exist', () => {
|
||||
should.exist(vm);
|
||||
});
|
||||
|
||||
it('Call a contract method', async () => {
|
||||
(await Promise.resolve(vmHandle.callContract(contractIds[0], 'hello', 'good morning')))
|
||||
.should.equal('Sender world: good morning');
|
||||
});
|
||||
|
||||
it('Call a contract method which calls another contract method', async () => {
|
||||
(await Promise.resolve(vmHandle.callContract(contractIds[1], 'forward', 'hello', 'good day')))
|
||||
.should.equal('Repeater world: good day');
|
||||
});
|
||||
});
|
|
@ -1,45 +0,0 @@
|
|||
import { Box } from '../../classes/display/box.js';
|
||||
import { Scene } from '../../classes/display/scene.js';
|
||||
import { WDAG } from '../../classes/supporting/wdag.js';
|
||||
|
||||
const rootElement = document.getElementById('scene');
|
||||
const rootBox = new Box('rootBox', rootElement).flex();
|
||||
window.scene = new Scene('WDAG test', rootBox);
|
||||
|
||||
describe('Query the graph', () => {
|
||||
let graph;
|
||||
|
||||
before(() => {
|
||||
graph = (window.graph = new WDAG()).withFlowchart();
|
||||
|
||||
graph.addVertex({});
|
||||
graph.addVertex({});
|
||||
graph.addVertex({});
|
||||
graph.addVertex({});
|
||||
graph.addVertex({});
|
||||
|
||||
graph.addEdge('e1', 0, 1, 1);
|
||||
graph.addEdge('e1', 2, 1, 0.5);
|
||||
graph.addEdge('e1', 3, 1, 0.25);
|
||||
graph.addEdge('e1', 1, 4, 0.125);
|
||||
});
|
||||
|
||||
it('can query for all e1 edges', () => {
|
||||
const edges = graph.getEdges('e1');
|
||||
edges.should.have.length(4);
|
||||
});
|
||||
|
||||
it('can query for all e1 edges from a particular vertex', () => {
|
||||
const edges = graph.getEdges('e1', 2);
|
||||
edges.map(({ from, to, weight }) => [from.id, to.id, weight]).should.have.deep.members([[2, 1, 0.5]]);
|
||||
});
|
||||
|
||||
it('can query for all e1 edges to a particular vertex', () => {
|
||||
const edges = graph.getEdges('e1', null, 1);
|
||||
edges.map(({ from, to, weight }) => [from.id, to.id, weight]).should.have.deep.members([
|
||||
[0, 1, 1],
|
||||
[2, 1, 0.5],
|
||||
[3, 1, 0.25],
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -1,36 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Validation Pool test</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link type="text/css" rel="stylesheet" href="../index.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2><a href="../">DGF Tests</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://cdnjs.cloudflare.com/ajax/libs/mocha/10.2.0/mocha.min.js"
|
||||
integrity="sha512-jsP/sG70bnt0xNVJt+k9NxQqGYvRrLzWhI+46SSf7oNJeCwdzZlBvoyrAN0zhtVyolGcHNh/9fEgZppG2pH+eA=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/4.3.7/chai.min.js"
|
||||
integrity="sha512-Pwgr3yHn4Gvztp1GKl0ihhAWLZfqgp4/SbMt4HKW7AymuTQODMCNPE7v1uGapTeOoQQ5Hoz367b4seKpx6j7Zg=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script type="module" src="./scripts/validation-pool.test.js"></script>
|
||||
<script defer class="mocha-init">
|
||||
mocha.setup({
|
||||
ui: 'bdd',
|
||||
globals: ['scene', 'dao', 'experts', 'posts', '__REACT_DEVTOOLS_*'],
|
||||
});
|
||||
mocha.checkLeaks();
|
||||
chai.should();
|
||||
</script>
|
||||
<script defer class="mocha-exec">
|
||||
// TODO: Weird race condition -- resolve this in a better way
|
||||
setTimeout(() => mocha.run(), 1000);
|
||||
</script>
|
|
@ -1,32 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>VM</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" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2><a href="../">DGF Tests</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/mocha/mocha.js"></script>
|
||||
<script src="https://unpkg.com/chai/chai.js"></script>
|
||||
<script type="module" src="./scripts/vm.test.js"></script>
|
||||
<script defer class="mocha-init">
|
||||
mocha.setup({
|
||||
ui: 'bdd',
|
||||
globals: ['vm', '__REACT_DEVTOOLS_*'],
|
||||
});
|
||||
mocha.checkLeaks();
|
||||
window.should = chai.should();
|
||||
</script>
|
||||
<script defer class="mocha-exec">
|
||||
// TODO: Weird race condition -- resolve this in a better way
|
||||
setTimeout(() => mocha.run(), 1000);
|
||||
</script>
|
|
@ -1,33 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>WDAG 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" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2><a href="../">DGF Tests</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',
|
||||
globals: ['graph', '__REACT_DEVTOOLS_*'],
|
||||
});
|
||||
mocha.checkLeaks();
|
||||
chai.should();
|
||||
</script>
|
||||
<script type="module" src="./scripts/wdag.test.js"></script>
|
||||
<script defer class="mocha-exec">
|
||||
// TODO: Weird race condition -- resolve this in a better way
|
||||
setTimeout(() => mocha.run(), 1000);
|
||||
</script>
|
|
@ -1,40 +0,0 @@
|
|||
import { CryptoUtil } from './classes/util/crypto.js';
|
||||
|
||||
const timers = new Map();
|
||||
|
||||
export const EPSILON = 2.23e-16;
|
||||
|
||||
export const debounce = async (fn, delayMs) => {
|
||||
const timer = timers.get(fn);
|
||||
if (timer) {
|
||||
return timer.result;
|
||||
}
|
||||
const result = await fn();
|
||||
timers.set(fn, { result });
|
||||
setTimeout(() => {
|
||||
timers.delete(fn);
|
||||
}, delayMs);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const delay = async (delayMs) => {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, delayMs);
|
||||
});
|
||||
};
|
||||
|
||||
export const hexToRGB = (input) => {
|
||||
if (input.startsWith('#')) {
|
||||
input = input.slice(1);
|
||||
}
|
||||
const r = parseInt(`${input[0]}${input[1]}`, 16);
|
||||
const g = parseInt(`${input[2]}${input[3]}`, 16);
|
||||
const b = parseInt(`${input[4]}${input[5]}`, 16);
|
||||
return { r, g, b };
|
||||
};
|
||||
|
||||
export const displayNumber = (value, decimals = 2) => (value.toString().length > decimals + 4
|
||||
? value.toFixed(decimals)
|
||||
: value);
|
||||
|
||||
export const randomID = () => CryptoUtil.randomUUID().replaceAll('-', '').slice(0, 8);
|
|
@ -0,0 +1 @@
|
|||
SEMANTIC_SCHOLAR_API_KEY=
|
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
.env
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "semantic-scholar-client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
default-run = "import"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-recursion = "1.0.0"
|
||||
clap = { version = "3.2.11", features = ["derive"] }
|
||||
dotenv = "0.15.0"
|
||||
mongodb = "2.2.2"
|
||||
reqwest = { version = "0.11.11", features = ["json"] }
|
||||
serde = { version = "1.0.139", features = ["derive"] }
|
||||
serde_json = "1.0.82"
|
||||
tokio = { version = "1.20.0", features = ["full"] }
|
|
@ -0,0 +1,25 @@
|
|||
#`semantic-scholar-client`
|
||||
|
||||
This utility is able to fetch data from Semantic Scholar API.
|
||||
|
||||
Initial proof of concept here writes the result to stdout.
|
||||
|
||||
Work in progress to pipe this data into an operating database.
|
||||
|
||||
### Usage
|
||||
|
||||
* (Optional) Copy `.env.example` to `.env` and set the value of `SEMANTIC_SCHOLAR_API_KEY`
|
||||
* Run the program
|
||||
|
||||
cargo run -- --paper-id <paper_id> --depth <depth>
|
||||
|
||||
* `paper_id` values are in accordance with [Semantic Scholar API](https://api.semanticscholar.org/api-docs/).
|
||||
* `depth` is the number of citations to traverse, from the starting paper.
|
||||
|
||||
### Notes
|
||||
|
||||
Ideas for followup work:
|
||||
- Consider strategies for deciding where to terminate a given traversal
|
||||
- Provide an HTTP/WebSocket interface that can be used to talk to this process during its operation.
|
||||
This can enable us to pipe the data to other tasks, to monitor, to start/stop, and even to make configuration changes.
|
||||
- Rate limit requests
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue