Compare commits
1 Commits
main
...
forum-test
Author | SHA1 | Date |
---|---|---|
Ladd Hoffman | e7ef21527a |
|
@ -0,0 +1,30 @@
|
||||||
|
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 @@
|
||||||
# DAO Governance Framework
|
# Science Publishing DAO
|
||||||
|
|
||||||
## Subprojects
|
## Subprojects
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [semantic-scholar-client](./semantic-scholar-client) | Rust utility for reading data from the [Semantic Scholar API](https://api.semanticscholar.org/api-docs) |
|
| [forum-network](./forum-network) | Javascript prototyping forum architecture |
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,3 @@
|
||||||
|
ssl/
|
||||||
|
node_modules/
|
||||||
|
git/
|
|
@ -0,0 +1,30 @@
|
||||||
|
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?
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Primary
|
||||||
|
|
||||||
|
## Forum
|
||||||
|
|
||||||
|
## ValidationPool
|
||||||
|
|
||||||
|
## ReputationToken
|
||||||
|
|
||||||
|
## WDAG
|
||||||
|
|
||||||
|
# Secondary
|
||||||
|
|
||||||
|
## Availability
|
||||||
|
|
||||||
|
## Business
|
||||||
|
|
||||||
|
## ERC721
|
||||||
|
|
||||||
|
## Expert
|
||||||
|
|
||||||
|
## Bench
|
||||||
|
|
||||||
|
# Tertiary
|
||||||
|
|
||||||
|
## Actor
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
## Scene
|
||||||
|
|
||||||
|
# To Explore
|
||||||
|
|
||||||
|
## Exchange
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
## Network
|
||||||
|
|
||||||
|
## Wallet
|
||||||
|
|
||||||
|
## Agent/UI
|
||||||
|
|
||||||
|
## BlockConsensus
|
|
@ -0,0 +1,35 @@
|
||||||
|
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.
|
|
@ -0,0 +1,12 @@
|
||||||
|
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
|
|
@ -0,0 +1,99 @@
|
||||||
|
# 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.
|
|
@ -0,0 +1,37 @@
|
||||||
|
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.
|
|
@ -0,0 +1,52 @@
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
|
@ -0,0 +1,5 @@
|
||||||
|
expert Expert1
|
||||||
|
expert Expert2
|
||||||
|
forum Forum
|
||||||
|
|
||||||
|
source -- action --> destination
|
|
@ -0,0 +1,4 @@
|
||||||
|
Possible statements
|
||||||
|
|
||||||
|
- It is what I would have done
|
||||||
|
- It is consistent with what I (would) have done
|
|
@ -0,0 +1,89 @@
|
||||||
|
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
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,193 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
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, '');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
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()}`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,293 @@
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
/**
|
||||||
|
* 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) {}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
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) ?? ''
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
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(),
|
||||||
|
}]));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { MermaidDiagram } from './mermaid.js';
|
||||||
|
|
||||||
|
export class Flowchart extends MermaidDiagram {
|
||||||
|
constructor(box, logBox, direction = 'BT') {
|
||||||
|
super(box, logBox);
|
||||||
|
|
||||||
|
this.log(`graph ${direction}`, false);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
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 ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
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);
|
||||||
|
};
|
|
@ -0,0 +1,57 @@
|
||||||
|
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) {
|
||||||
|
// }
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export class BlockConsensus {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
export class Token {
|
||||||
|
constructor(ownerPublicKey) {
|
||||||
|
this.ownerPublicKey = ownerPublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
transfer(newOwnerPublicKey) {
|
||||||
|
// TODO: Current owner must sign this request
|
||||||
|
this.ownerPublicKey = newOwnerPublicKey;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,152 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
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.
After Width: | Height: | Size: 3.5 KiB |
|
@ -0,0 +1,51 @@
|
||||||
|
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;
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
<!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>
|
||||||
|
<li><a href="./tests/forum6.test.html">Reversal of power redistribution</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>
|
|
@ -0,0 +1,28 @@
|
||||||
|
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;
|
|
@ -0,0 +1,44 @@
|
||||||
|
<!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 type="module" src="./scripts/forum/forum6.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>
|
|
@ -0,0 +1,37 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,36 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,41 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,33 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Forum test 6</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/forum6.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>
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,35 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,169 @@
|
||||||
|
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);
|
||||||
|
});
|
|
@ -0,0 +1,67 @@
|
||||||
|
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));
|
||||||
|
}());
|
|
@ -0,0 +1,139 @@
|
||||||
|
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);
|
||||||
|
}());
|
|
@ -0,0 +1,16 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,72 @@
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,76 @@
|
||||||
|
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);
|
||||||
|
});
|
|
@ -0,0 +1,87 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
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 }]);
|
|
@ -0,0 +1,39 @@
|
||||||
|
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 }]);
|
|
@ -0,0 +1,42 @@
|
||||||
|
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 }]);
|
|
@ -0,0 +1,70 @@
|
||||||
|
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 }]);
|
|
@ -0,0 +1,59 @@
|
||||||
|
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 }]);
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { ForumTest } from './forum.test-util.js';
|
||||||
|
|
||||||
|
describe('Forum', () => {
|
||||||
|
const forumTest = new ForumTest();
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
await forumTest.setup();
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Reversal of power redistribution', 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Post6 cites Post3 negatively', async () => {
|
||||||
|
await forumTest.addPost(experts[0], 10, [
|
||||||
|
{ postId: posts[2], weight: -1 },
|
||||||
|
]);
|
||||||
|
forum.getPost(posts[0]).value.should.equal(10);
|
||||||
|
forum.getPost(posts[1]).value.should.equal(20);
|
||||||
|
forum.getPost(posts[2]).value.should.equal(0);
|
||||||
|
forum.getPost(posts[3]).value.should.equal(0);
|
||||||
|
forum.getPost(posts[4]).value.should.equal(0);
|
||||||
|
forum.getPost(posts[5]).value.should.equal(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Post7 cites Post3 negatively', async () => {
|
||||||
|
await forumTest.addPost(experts[0], 10, [
|
||||||
|
{ postId: posts[2], 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);
|
||||||
|
forum.getPost(posts[3]).value.should.equal(0);
|
||||||
|
forum.getPost(posts[4]).value.should.equal(0);
|
||||||
|
forum.getPost(posts[5]).value.should.equal(20);
|
||||||
|
forum.getPost(posts[6]).value.should.equal(20);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,23 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,111 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,70 @@
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,45 @@
|
||||||
|
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],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,36 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,33 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,40 @@
|
||||||
|
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);
|
|
@ -1 +0,0 @@
|
||||||
SEMANTIC_SCHOLAR_API_KEY=
|
|
|
@ -1,2 +0,0 @@
|
||||||
/target
|
|
||||||
.env
|
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue