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