Initial commit: copied from science-publishing-dao/forum-network, commit 907b99bb65db0e4d5da5c93f74377e38c5cc8e1f
This commit is contained in:
parent
8c6f237134
commit
fac3d87009
|
@ -0,0 +1,50 @@
|
||||||
|
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'],
|
||||||
|
'no-underscore-dangle': ['off'],
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
_: 'readonly',
|
||||||
|
chai: 'readonly',
|
||||||
|
sinon: 'readonly',
|
||||||
|
sinonChai: 'readonly',
|
||||||
|
should: 'readonly',
|
||||||
|
mocha: 'readonly',
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,3 @@
|
||||||
|
ssl/
|
||||||
|
node_modules/
|
||||||
|
git/
|
|
@ -0,0 +1,101 @@
|
||||||
|
# Revenue-generating work
|
||||||
|
|
||||||
|
## Expert stakes REP to register availability
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph
|
||||||
|
|
||||||
|
subgraph EOA
|
||||||
|
expert(Expert)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Contracts
|
||||||
|
availability(Availability)
|
||||||
|
end
|
||||||
|
|
||||||
|
expert -- 1. Stake ℝ --> availability
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Public submits work request with fee
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph
|
||||||
|
|
||||||
|
subgraph EOA
|
||||||
|
expert(Expert)
|
||||||
|
public(Public)
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Contracts
|
||||||
|
business(Business)
|
||||||
|
availability(Availability)
|
||||||
|
end
|
||||||
|
|
||||||
|
public -- 1. Request<br />with fee $ --> business
|
||||||
|
business -- 2. Assign<br />work --> availability
|
||||||
|
availability -- 3. Transfer<br />staked ℝ --> business
|
||||||
|
availability -- 4. TODO Notify --> expert
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expert submits work evidence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph
|
||||||
|
|
||||||
|
subgraph EOA
|
||||||
|
expert(Expert)
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Contracts
|
||||||
|
business(Business)
|
||||||
|
forum(Forum)
|
||||||
|
pool(Pool)
|
||||||
|
end
|
||||||
|
|
||||||
|
expert -- 1. Work<br />evidence --> business
|
||||||
|
business -- 2. Post --> forum
|
||||||
|
business -- 3. Stake ℝ --> pool
|
||||||
|
```
|
||||||
|
|
||||||
|
## Peers validate the work evidence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph
|
||||||
|
|
||||||
|
subgraph EOA
|
||||||
|
peers(Peers)
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Contracts
|
||||||
|
forum(Forum)
|
||||||
|
pool(Pool)
|
||||||
|
end
|
||||||
|
|
||||||
|
peers -- 8. Stake ℝ --> pool
|
||||||
|
pool -- 9. Validate post,<br />Transfer ℝ --> forum
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rewards are distributed
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph
|
||||||
|
|
||||||
|
subgraph EOA
|
||||||
|
expert(Expert)
|
||||||
|
peers(Peers)
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Contracts
|
||||||
|
pool(Pool)
|
||||||
|
business(Business)
|
||||||
|
forum(Forum)
|
||||||
|
end
|
||||||
|
|
||||||
|
pool -- Reward ℝ --> peers
|
||||||
|
forum -- Award ℝ --> expert
|
||||||
|
forum -- Award ℝ<br />via citation<br />WDAG --> peers
|
||||||
|
business -- Award % fee $<br />weighted by ℝ--> expert
|
||||||
|
business -- Award % fee $<br />weighted by ℝ--> peers
|
||||||
|
```
|
|
@ -0,0 +1,46 @@
|
||||||
|
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?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Protocol brainstorming
|
||||||
|
|
||||||
|
Each node must build/maintain a view of the history and/or state of the "smart contract" operations.
|
||||||
|
|
||||||
|
Nodes must sign messages to each other with asymmetric keys.
|
||||||
|
|
||||||
|
This is intended to be an open network that anyone can join.
|
||||||
|
|
||||||
|
Each node must verify the results reported by other nodes, and themselves report results to other nodes.
|
||||||
|
|
||||||
|
In order to receive payments, the network must solve the same problems that (other) block chains have solved, i.e. must prevent double-spend; must prevent tampering with the ledger.
|
||||||
|
|
||||||
|
Storage may be ranked into tiers, where there is core data essential to the integrity of the ledger; ancillary data that is important or desirable for review of the ledger; and supplementary data that is of variable importants for particular use cases, but does not compose the core fabric of the system.
|
|
@ -0,0 +1,18 @@
|
||||||
|
## Client
|
||||||
|
|
||||||
|
Clients play a key role in an MVPR DAO.
|
||||||
|
|
||||||
|
Clients must be operated by reputation holders.
|
||||||
|
|
||||||
|
Clients are the agents that submit posts to the forum, initiate validation pools, and vote in validation pools.
|
||||||
|
|
||||||
|
We sometimes refer to the client as "the UI".
|
||||||
|
|
||||||
|
It will need to be a network-connected application. It will need a certain minimum of RAM,
|
||||||
|
and for some features disk storage,
|
||||||
|
and for some features uptime .
|
||||||
|
|
||||||
|
The behavior of the client constitutes what we refer to as the DAO's "soft protocols".
|
||||||
|
|
||||||
|
Malicious actors may freely modify their own client's behavior.
|
||||||
|
Therefore honest clients must engage in policing to preserve the integrity of the network.
|
|
@ -0,0 +1,146 @@
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Internal operations
|
||||||
|
|
||||||
|
## Expert starts new DAO
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph
|
||||||
|
|
||||||
|
subgraph EOA
|
||||||
|
expert(Expert)
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Contracts
|
||||||
|
forum(Forum)
|
||||||
|
pool(Pool)
|
||||||
|
end
|
||||||
|
|
||||||
|
expert -- Post --> forum
|
||||||
|
expert -- Fee $ --> pool
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expert joins existing DAO
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph
|
||||||
|
|
||||||
|
subgraph EOA
|
||||||
|
expert(Expert)
|
||||||
|
peers(Peers)
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Contracts
|
||||||
|
forum(Forum)
|
||||||
|
pool(Pool)
|
||||||
|
end
|
||||||
|
|
||||||
|
expert -- 1. Post --> forum
|
||||||
|
expert -- 2. Fee $ --> pool
|
||||||
|
peers -- 3. Stake ℝ<br />to approve --> pool
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expert submits governance post
|
||||||
|
|
||||||
|
A governance post can be considered one that is respected in some way by the operations of the [Client/UI](./client-or-ui.md).
|
||||||
|
|
||||||
|
This is a broad class of posts.
|
||||||
|
|
||||||
|
Each type of governance post can be individually (or by category?) handled by the business contract for a given DAO.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph
|
||||||
|
|
||||||
|
subgraph EOA
|
||||||
|
expert(Expert)
|
||||||
|
peers(Peers)
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Contracts
|
||||||
|
forum(Forum)
|
||||||
|
pool(Pool)
|
||||||
|
business(Business)
|
||||||
|
end
|
||||||
|
|
||||||
|
expert -- 1. Stake ℝ on<br />governance post --> business
|
||||||
|
business -- 2. Post --> forum
|
||||||
|
business -- 2. Fee $ from<br />internal fund? --> pool
|
||||||
|
peers -- 3. Stake ℝ<br />to approve --> pool
|
||||||
|
pool -- 4. Validate --> forum
|
||||||
|
```
|
||||||
|
|
||||||
|
Forum usage is open-ended.
|
||||||
|
DAO protocol consists of core contracts + client behaviors.
|
||||||
|
Core contracts provide resilience to attacks, since reputation minting is financially backed by future income
|
||||||
|
|
||||||
|
Question: What does the DAO do with funds it receives?
|
||||||
|
Awswer: Distributes the funds to members immediately upon resolution of the reputation effects of a funded validation pool.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Before we delve into example use cases, we need to talk about the [Client/UI](./client-or-ui.md), and make sure we have
|
||||||
|
a sound understanding of how client/ui behaviors interact with the core of the system.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The forum, pool, business, and availability contracts all work together to express a single DAO.
|
||||||
|
|
||||||
|
Each post in the forum can be its own new DAO
|
||||||
|
|
||||||
|
What it would take for that to happen:
|
||||||
|
|
||||||
|
The seed of the new DAO becomes the tokens minted by that post DAO when it receives fees.
|
||||||
|
|
||||||
|
When will it receive fees? When submitted to its business contract interface.
|
||||||
|
|
||||||
|
What happens then? Work is assigned via availability stakes.
|
||||||
|
|
||||||
|
Meaning that someone has staked reputation.
|
||||||
|
|
||||||
|
Meaning that they had previously been awarded reputation.
|
||||||
|
|
||||||
|
The business contract, or the DAO, or the seed post, must be able to accept an initial fee
|
||||||
|
to mint the reputation of the first expert.
|
||||||
|
|
||||||
|
Then, that expert can stake their reputation on availability to perform the work expressed by the post and its associated business contract.
|
||||||
|
|
||||||
|
These operations can be consolidated.
|
||||||
|
|
||||||
|
When submitting a post to the forum, you may include an optional fee
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Ecosystem Health
|
||||||
|
|
||||||
|
How do we incentivize and reward the producers and maintainers of infrastructure? Of essential goods and services?
|
||||||
|
|
||||||
|
How do we reward pro-social behavior?
|
||||||
|
|
||||||
|
How do we encourage creativity?
|
||||||
|
|
||||||
|
Vision/Mission
|
||||||
|
|
||||||
|
Craig: Give people tools to enable them to better express their values by collaborating
|
||||||
|
|
||||||
|
truth
|
||||||
|
good
|
||||||
|
beauty
|
||||||
|
|
||||||
|
thought
|
||||||
|
action
|
||||||
|
perception
|
||||||
|
|
||||||
|
ideas
|
||||||
|
knowledge
|
||||||
|
beliefs
|
||||||
|
|
||||||
|
utility
|
||||||
|
|
||||||
|
evolution in the true sense -- most directions it can change will be detrimental
|
|
@ -0,0 +1,20 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Examples to add:
|
||||||
|
|
||||||
|
- Incinerator
|
||||||
|
|
||||||
|
- Negatively cite a zero-value post -- intent is to show how governance might cite a post as a counter-example
|
|
@ -0,0 +1,12 @@
|
||||||
|
Each DAO needs to allocate some of its incoming fees to incentivize development.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Well, the above is not exactly correct. The incentive for development is to earn reputation.
|
||||||
|
|
||||||
|
However what is true is that a DAO may need to leverage some reputation toward governance-related actions.
|
||||||
|
For example gradually changing the weight of some posts bit by bit.
|
||||||
|
This can be accomplished by a work smart contract that allocates a fixed percentage of minted reputation in the desired way.
|
||||||
|
If no reputation is needed for such initiatives at a given time, it can be burned instead, to preserve fairness.
|
||||||
|
|
||||||
|
---
|
|
@ -0,0 +1,5 @@
|
||||||
|
At the base layer, we need a peer to peer protocol that allows new nodes to join the network and send and receive messages. It must protect against denial of service attacks. It must support the establishment of consensus, to varying strengths.
|
||||||
|
|
||||||
|
We need a lightweight messaging solution to facilitate gathering information from the edges of the network, but we also need to protect against denial of service by malicious actors.
|
||||||
|
|
||||||
|
[gossipsub](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md) looks like a good protocol for this.
|
|
@ -0,0 +1,34 @@
|
||||||
|
Matrix is a communications network.
|
||||||
|
It has a client-server, server-server decentralized architecture.
|
||||||
|
Rooms are synced (eventually consistent) among all servers with clients participating in the room.
|
||||||
|
|
||||||
|
Matrix supports "Application Services", which are limited to funcion in a passive mode, meaning they only piggyback on top of the existing protocols.
|
||||||
|
|
||||||
|
Synapse, a Matrix server implementation, supports "Modules"
|
||||||
|
|
||||||
|
The Matrix devs recognize the need for a robust reputation system and are in pursuit of funding and development for that purpose.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
|
||||||
|
subgraph Matrix
|
||||||
|
homeserver[Homeserver]
|
||||||
|
chainClient[Blockchain<br/>connected<br/>client]
|
||||||
|
publicClient[Public<br/>Matrix-only<br/>client]
|
||||||
|
end
|
||||||
|
|
||||||
|
blockchain[Blockchain]
|
||||||
|
%% subgraph Blockchain
|
||||||
|
%% forum[Forum]
|
||||||
|
%% post[Post]
|
||||||
|
%% availability[Availability]
|
||||||
|
%% wsc[WSC]
|
||||||
|
%% pool[Validation<br/>Pool]
|
||||||
|
%% end
|
||||||
|
|
||||||
|
publicClient --> homeserver
|
||||||
|
chainClient --> homeserver
|
||||||
|
chainClient --> blockchain
|
||||||
|
homeserver --> blockchain
|
||||||
|
|
||||||
|
```
|
|
@ -0,0 +1,99 @@
|
||||||
|
# Challenges
|
||||||
|
|
||||||
|
- Receiving payments
|
||||||
|
- Distributing payments to participants
|
||||||
|
- Computing updates to forum graph
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Receiving payments
|
||||||
|
|
||||||
|
Business SC will need to implement a financial model.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Excerpts from DeSciPubDAOArchit22July19PrintCut.pdf
|
||||||
|
|
||||||
|
> With today’s prices, however, we will begin by programming this all off-chain and simplify the reputation tokens to be less dynamic in their evaluation. Next iteration improves the decentralization commensurate with practical realities.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Validation pool termination
|
||||||
|
|
||||||
|
How do we want to handle this?
|
||||||
|
The validation pool specifies a duration.
|
||||||
|
We don't want to compute results until the end of this duration.
|
||||||
|
We're currently supporting anonymous voting.
|
||||||
|
With anonymous voting, we need to wait until the end of the vote duration,
|
||||||
|
and then have a separate interval in which voters reveal their identities.
|
||||||
|
For now, we can let anonymous voters reveal their identities at any time
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Bench.totalReputation is a very important quantity, isn't it? Basically determines inflation for reputation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Should availability registration encumber reputation?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- Is a particular availability stake amount required?
|
||||||
|
|
||||||
|
Currently we support updating the staked amount.
|
||||||
|
Seems like a soft protocol thing.
|
||||||
|
A given DAO can have a formula for deciding appropriate amounts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The following was a code comment on `Business.submitRequest(fee, ...)`:
|
||||||
|
|
||||||
|
> Fee should be held in escrow.
|
||||||
|
> That means there should be specific conditions under which the fee will be refunded.
|
||||||
|
> That means the submission should include some time value to indicate when it expires.
|
||||||
|
> There could be separate thresholds to indicate the earliest that the job may be cancelled,
|
||||||
|
> and the time at which the job will be automatically cancelled.
|
||||||
|
|
||||||
|
# Implementing forum
|
||||||
|
|
||||||
|
Does the following make sense?
|
||||||
|
We will link the forum to the bench
|
||||||
|
An author of a forum post /_ ? is always? can be? _/ a reputation holder.
|
||||||
|
This is what we call a expert. Let's update that terminology to be `reputationHolder`.
|
||||||
|
That's too long, though. Let's rename it to `expert`.
|
||||||
|
So we want to aim for the situation where the author of a forum post is an expert.
|
||||||
|
For now let's try thinking of them as experts no matter what;
|
||||||
|
The strength of their expertise is meant to be represented by reputation tokens.
|
||||||
|
So each reputation token must be a contract.
|
||||||
|
Minting a reputation token means to construct an instance of such a contract.
|
||||||
|
The reputation contract then has its own lifecycle.
|
||||||
|
We can support dynamic reevaluation if the reputation contract
|
||||||
|
|
||||||
|
- has an interface that allows (securely) updating
|
||||||
|
- Define secure :: passes validation pool
|
||||||
|
- How shall it know the operation is occurring as part of an "official" validation pool?
|
||||||
|
It can verify a signature...
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Tokens staked for and against a post.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Token loss ratio
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
parameter q_4 -- what is c_n?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
what is reputation?
|
||||||
|
valuable evidence that you're going to do what you say you'll do in the future
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
for now, combine c2 and c3
|
||||||
|
|
||||||
|
validation pool should compute rewards for author,
|
||||||
|
then send that to the forum to distribute.
|
|
@ -0,0 +1,37 @@
|
||||||
|
The communication protocol(s) among network nodes
|
||||||
|
Each communication protocol among network nodes
|
||||||
|
has its own purpose
|
||||||
|
has its own assumptions, expectations, requirements, constraints
|
||||||
|
|
||||||
|
I think it makes sense to identify the constraints for our protocols.
|
||||||
|
|
||||||
|
We need the general public to be able to reliably
|
||||||
|
|
||||||
|
- Query information about the reputation WDAG
|
||||||
|
- Submit requests and fees for work
|
||||||
|
- Obtain the products of the work submitted by forum experts
|
||||||
|
|
||||||
|
Suppose we want only the requestor to be able to access a given work product.
|
||||||
|
(Why might we want this?)
|
||||||
|
Then the (soft) protocol for reviewing the work product would consist of
|
||||||
|
validating a signature by the requestor, attesting to their acceptance of the work product.
|
||||||
|
|
||||||
|
Alternatively access could be permitted to some group, such as reputation holders (a.k.a. experts).
|
||||||
|
|
||||||
|
Otherwise, for maximum utility, we would want to make the work products available indefinitely, as valuable artifacts.
|
||||||
|
Value here can be equated to the expected fees that the work products will help attract, which can in turn be equated to
|
||||||
|
reputation awarded to authors and reviewers of the work products.
|
||||||
|
|
||||||
|
Thus, the work of making the artifacts available must be funded.
|
||||||
|
|
||||||
|
The work of participating in a gossip / forum node consensus protocol and validating forum chain blocks must also be funded.
|
||||||
|
|
||||||
|
Suppose we have a storage contract.
|
||||||
|
|
||||||
|
- There can be a market for specific pledges of storage.
|
||||||
|
- buy: (amount, duration, price)
|
||||||
|
- sell: (amount, duration, price)
|
||||||
|
- Governance: Management of storage price
|
||||||
|
- may negotiate via loose -> tight binding traversal forum post sequence
|
||||||
|
- reputation in accordance with majority opinions on price parameters
|
||||||
|
- Verification of storage must occur by (randomly) querying the storage nodes and validating their responses.
|
|
@ -0,0 +1,52 @@
|
||||||
|
Reputation Tokens
|
||||||
|
|
||||||
|
Minting
|
||||||
|
|
||||||
|
Suppose it's possible to mint a reputation token.
|
||||||
|
Say we have a contract that keeps track of all the reputation tokens.
|
||||||
|
Suppose the reputation contract implements ERC720 (NFT).
|
||||||
|
Assume a validation pool always corresponds to a post.
|
||||||
|
A single token could be minted for each validation pool.
|
||||||
|
That token could be subdivided so that each winning voter gets some.
|
||||||
|
Perhaps voters get tokens that are specifically identifiable as governance reputation tokens.
|
||||||
|
Then the main token can be awarded to the post author.
|
||||||
|
Each token should carry a specific value.
|
||||||
|
The forum will update the value of the token for the post author as well as posts affected by its chain of references.
|
||||||
|
|
||||||
|
Then, when participants wish to stake reputation (for voting or for availability),
|
||||||
|
they must specify the amount and the token address which carries that reputation.
|
||||||
|
The token should probably then lock that reputation, preventing it from being staked concurrently for another purpose.
|
||||||
|
|
||||||
|
Perhaps our interface can allow staking reputation from multiple tokens at the same time.
|
||||||
|
And/or we can provide a mechanism for consolidating tokens.
|
||||||
|
|
||||||
|
Or maybe, if reputation is staked via a given token, then the reputation awards should go to that same token.
|
||||||
|
In that case, when should new tokens be minted?
|
||||||
|
|
||||||
|
Maybe a token should be minted for each validation pool, but not subdivided.
|
||||||
|
Voter rewards can just add value to the existing tokens from which reputation was staked.
|
||||||
|
|
||||||
|
Maybe a new token should only be minted if the author did not provide a token from which to stake reputation on their own post.
|
||||||
|
This supports the case of a new author earning their first reputation.
|
||||||
|
In that case the author may need to pay a fee to buy in to the DAO.
|
||||||
|
Or perhaps they can be sponsored by one or more existing reputation token holders.
|
||||||
|
Existing reputation holders could grant some reputation to a new member.
|
||||||
|
Perhaps there could be a contract that allows sponsoring a new member, such that whatever fee is given,
|
||||||
|
that fee will automatically be repaid from the new member's earnings, before the new member starts receiving their share of earnings.
|
||||||
|
This could be a multi-party action, or could just be a generic operation that can be performed multiple times.
|
||||||
|
|
||||||
|
However, this effectively allows buying reputation, which goes against the core concept of reputation as evidence of performance.
|
||||||
|
|
||||||
|
It could make more sense for new members to submit some sort of petition, i.e. to make a post.
|
||||||
|
|
||||||
|
Maybe rather than submitting fees, existing members can grant some of their own reputation to a new member, and receive some sort of compensation if the new member does well.
|
||||||
|
|
||||||
|
So far the only workable model seems to be the idea that a new member must submit a post along with a fee, in order to be considered, and if the post is approved, they gain their first reputation.
|
||||||
|
The amount of this fee can be up to the applicant, and/or can be regulated by soft protocols within the DAO.
|
||||||
|
|
||||||
|
If this is the only scenario in which new rep tokens are minted, and from then on their value is updated as a result of each validation pool,
|
||||||
|
then we probably want each token to store information about the history of its value.
|
||||||
|
At a minimum this can be a list where each item includes the identifier of the corresponding validation pool, and the resulting increment/decrement of token value.
|
||||||
|
Each validation pool can then keep a record of the reputation staked by each voter, and the identifier of the corresponding post.
|
||||||
|
|
||||||
|
---
|
|
@ -0,0 +1,30 @@
|
||||||
|
Matrix uses rooms to establish contexts.
|
||||||
|
|
||||||
|
We can have a forum context,
|
||||||
|
wherein a few things happen.
|
||||||
|
|
||||||
|
One is that the forum will have a root post; equivalently, any post can be the root of a forum.
|
||||||
|
|
||||||
|
The context of that post can be preserved.
|
||||||
|
|
||||||
|
The forum is thus a collection of posts. Each post MAY have its own internal structure.
|
||||||
|
A post MAY "replace" a prior post. This consists of on-chain and off-chain elements.
|
||||||
|
On-chain, a new post replaces a prior post.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Reading Matrix spec, https://spec.matrix.org/latest/
|
||||||
|
|
||||||
|
> Events are signed by the originating server (the signature includes the parent relations, type, depth and payload hash) and are pushed over federation to the participating servers in a room, currently using full mesh topology. Servers may also request backfill of events over federation from the other servers participating in a room.
|
||||||
|
|
||||||
|
> In order to ensure that the mapping from 3PID to user ID is genuine, a globally federated cluster of trusted “identity servers” (IS) are used to verify the 3PID and persist and replicate the mappings.
|
||||||
|
>
|
||||||
|
> Usage of an IS is not required in order for a client application to be part of the Matrix ecosystem. However, without one clients will not be able to look up user IDs using 3PIDs.
|
||||||
|
|
||||||
|
> Users may publish arbitrary key/value data associated with their account
|
||||||
|
>
|
||||||
|
> - such as a human-readable display name, a profile photo URL, contact information (email address, phone numbers, website URLs etc).
|
||||||
|
|
||||||
|
> Users may also store arbitrary private key/value data in their account - such as client preferences, or server configuration settings which lack any other dedicated API. The API is symmetrical to managing Profile data.
|
||||||
|
|
||||||
|
> The client-server API allows clients to send messages, control rooms and synchronise conversation history. It is designed to support both lightweight clients which store no state and lazy-load data from the server as required - as well as heavyweight clients which maintain a full local persistent copy of server state.
|
|
@ -0,0 +1,5 @@
|
||||||
|
expert Expert1
|
||||||
|
expert Expert2
|
||||||
|
forum Forum
|
||||||
|
|
||||||
|
source -- action --> destination
|
|
@ -0,0 +1,4 @@
|
||||||
|
digraph {
|
||||||
|
layout=neato
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
```mermaid
|
||||||
|
graph
|
||||||
|
classDef blue fill:#08f, color:#d8d8d8, stroke-width: 0
|
||||||
|
classDef yellow fill:#dd0, color:#a0c, stroke-width: 0
|
||||||
|
classDef green fill:#8c8, color:#333, stroke-width: 0
|
||||||
|
classDef purple stroke:#c38, stroke-width:2px, fill:#bbf, color:#c38
|
||||||
|
classDef orange fill:#d60, color:#dff, stroke-width: 0
|
||||||
|
classDef fuscia fill:#f6c, color:#00c
|
||||||
|
|
||||||
|
nodeSpec(Node spec<br />Matrix homeservers):::blue
|
||||||
|
storageSpec(Storage spec<br />Matrix homeservers):::orange
|
||||||
|
archiveSpec(Archive spec<br />Weavechain):::green
|
||||||
|
blockchainSpec(Blockchain spec):::purple
|
||||||
|
peerProtocolSpec(Peer protocol spec<br />Matrix messaging):::yellow
|
||||||
|
uiSpec(UI spec<br />Matrix client):::fuscia
|
||||||
|
|
||||||
|
|
||||||
|
nodeSpec --- uiSpec
|
||||||
|
linkStyle 0 stroke:#08f
|
||||||
|
nodeSpec --- storageSpec
|
||||||
|
linkStyle 1 stroke:#08f
|
||||||
|
nodeSpec --- peerProtocolSpec
|
||||||
|
linkStyle 2 stroke:#08f
|
||||||
|
nodeSpec --- archiveSpec
|
||||||
|
linkStyle 3 stroke:#08f
|
||||||
|
nodeSpec --- blockchainSpec
|
||||||
|
linkStyle 4 stroke:#08f
|
||||||
|
|
||||||
|
peerProtocolSpec --- storageSpec
|
||||||
|
linkStyle 5 stroke:#d60
|
||||||
|
|
||||||
|
storageSpec --- blockchainSpec
|
||||||
|
linkStyle 6 stroke:#c38
|
||||||
|
storageSpec --- archiveSpec
|
||||||
|
linkStyle 7 stroke:#8c8
|
||||||
|
|
||||||
|
archiveSpec --- blockchainSpec
|
||||||
|
linkStyle 8 stroke:#c38
|
||||||
|
|
||||||
|
uiSpec --- blockchainSpec
|
||||||
|
linkStyle 9 stroke:#c38
|
||||||
|
uiSpec --- archiveSpec
|
||||||
|
linkStyle 10 stroke:#8c8
|
||||||
|
uiSpec --- storageSpec
|
||||||
|
linkStyle 11 stroke:#d60
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph
|
||||||
|
|
||||||
|
forum --- pool
|
||||||
|
availability --- business
|
||||||
|
business --- forum
|
||||||
|
business --- pool
|
||||||
|
|
||||||
|
```
|
|
@ -0,0 +1,17 @@
|
||||||
|
# signature verification
|
||||||
|
|
||||||
|
# voting topologies
|
||||||
|
|
||||||
|
# client implementations
|
||||||
|
|
||||||
|
## example workflows
|
||||||
|
|
||||||
|
- retroactive sign flip
|
||||||
|
|
||||||
|
# storage scaling
|
||||||
|
|
||||||
|
# blockchain underpinnings
|
||||||
|
|
||||||
|
# reputation token lifetime / decay
|
||||||
|
|
||||||
|
- active/passive
|
|
@ -0,0 +1,4 @@
|
||||||
|
Possible statements
|
||||||
|
|
||||||
|
- It is what I would have done
|
||||||
|
- It is consistent with what I (would) have done
|
|
@ -0,0 +1,89 @@
|
||||||
|
This system is meant to represent a body of experts receiving fees to perform work.
|
||||||
|
Experts register their availability to receive work via the availability contract.
|
||||||
|
Request and associated fees are sumbitted via the business contract.
|
||||||
|
Evidence of the work performed is submitted as a post to the forum.
|
||||||
|
A successful validation pool ratifies the post.
|
||||||
|
Reputation is minted and distributed.
|
||||||
|
Fees are distributed.
|
||||||
|
|
||||||
|
What if we want the work to be block production for a blockchain?
|
||||||
|
Then to perform this work, an expert will need to participate in a communications network
|
||||||
|
such that they can confidently arrive at a majority view of each block.
|
||||||
|
Or else must at least be able to attest that a proposed block is valid,
|
||||||
|
meaning that it
|
||||||
|
|
||||||
|
- does not conflict with what the node believes to be the majority view
|
||||||
|
- includes what the node believes must be included according to the majority view
|
||||||
|
note that with this scheme there be muliple possible valid proposed blocks.
|
||||||
|
|
||||||
|
In any case, block production will require some form of consensus protocol. (BFT).
|
||||||
|
|
||||||
|
We have to define an algorithm from the perspective of a single node.
|
||||||
|
|
||||||
|
That node will need to make certain assumptions. Let us identify those assumptions.
|
||||||
|
|
||||||
|
- continuity guarantees?
|
||||||
|
- sender identifiability?
|
||||||
|
- it needs to bo possible to verify an asymmetric signature
|
||||||
|
|
||||||
|
This leads us to the storage requirements for a node.
|
||||||
|
|
||||||
|
For a node to exist, it must be capable of at least some temporal continuity.
|
||||||
|
That's what distinguishes a node from a client.
|
||||||
|
|
||||||
|
We want our protocol to involve performing certain kinds of work.
|
||||||
|
|
||||||
|
- Block production
|
||||||
|
- Messaging protocol
|
||||||
|
- In-memory storage
|
||||||
|
- Queryable history
|
||||||
|
- Fast record storage
|
||||||
|
- Archival record storage
|
||||||
|
|
||||||
|
What is common among these?
|
||||||
|
|
||||||
|
- Availability stake represents commitment to perform the specified work
|
||||||
|
- Peers must validate the work product via validation pool
|
||||||
|
|
||||||
|
How can we adapt the following concepts?
|
||||||
|
|
||||||
|
- Business contract interfacing with availability contract
|
||||||
|
- Reputation is minted via validation pools
|
||||||
|
- Reputation is rewarded to validation pool winners
|
||||||
|
- Reputation is awarded to a post in the forum
|
||||||
|
- Reputation is propagated via citations
|
||||||
|
|
||||||
|
In a messaging system, the work is
|
||||||
|
|
||||||
|
- Listening for messages
|
||||||
|
- Receiving messages
|
||||||
|
- Processing messages
|
||||||
|
- Sending messages
|
||||||
|
- Maintaining context related to incoming messages for the duration of some operation
|
||||||
|
- Performing computations
|
||||||
|
|
||||||
|
The work of verifying peers in a messaging system is
|
||||||
|
|
||||||
|
- Detecting invalid messages
|
||||||
|
- Successfully defending against DoS attacks
|
||||||
|
- Initiating validation pools?
|
||||||
|
- Voting in validation pools?
|
||||||
|
|
||||||
|
The work of providing a storage service extends that of participating in a messaging system.
|
||||||
|
- Storing data
|
||||||
|
- Retrieving data
|
||||||
|
|
||||||
|
The work of verifying peers work products in a storage network is
|
||||||
|
|
||||||
|
- Periodically querying peers and verifying their responses
|
||||||
|
- Participating in validation pools to police peers
|
||||||
|
- Initiating validation pools
|
||||||
|
- Voting in validation pools
|
||||||
|
|
||||||
|
Governance of a storage network includes
|
||||||
|
tuning post and validation pool timing and other parameters.
|
||||||
|
This can be served via the forum and validation pool,
|
||||||
|
by having the clients agree on an interpretation of the forum,
|
||||||
|
such that clients can derive from forum posts, at least some operating parameters.
|
||||||
|
It may even be possible to use the forum to provide the client code itself,
|
||||||
|
or tools for generating such code.
|
|
@ -0,0 +1,39 @@
|
||||||
|
We are trying to build the reputation mechanisms for the X-prize project.
|
||||||
|
This project has the loosely stated goal of building tools for communities, but there is an expectation that this will use "MVPR."
|
||||||
|
|
||||||
|
My thinking is that the following basic features are needed of reputation:
|
||||||
|
|
||||||
|
- Bootstrap initial members
|
||||||
|
- Members can onboard new members
|
||||||
|
- Members can perform actions that affect each other's reputation
|
||||||
|
- Members can vote to prioritize content
|
||||||
|
- Public can view prioritized content
|
||||||
|
|
||||||
|
We need to provide detailed workflows for each of these.
|
||||||
|
|
||||||
|
# Bootstrap initial members
|
||||||
|
|
||||||
|
# Members can onboard new members
|
||||||
|
|
||||||
|
- A non-member is equivalent to a member with no reputation.
|
||||||
|
- Non-members can post and have their posts reviewed in order to gain reputation.
|
||||||
|
|
||||||
|
# Members can perform actions that affect each other's reputation
|
||||||
|
|
||||||
|
- Members can positively/negatively cite posts (what kind of posts?) by other members
|
||||||
|
|
||||||
|
- Stake reputation on these posts
|
||||||
|
- Validation pool determines the outcome - poster may gain or lose reputation,
|
||||||
|
- strength of effect can be influenced by ratio of upvotes
|
||||||
|
|
||||||
|
- Upvote = Post?
|
||||||
|
- Upvote = Vote in validation pool?
|
||||||
|
|
||||||
|
- Multiple types of reputation?
|
||||||
|
- Correctness
|
||||||
|
- "Goodness"
|
||||||
|
- Humor
|
||||||
|
|
||||||
|
# Members can vote to prioritize content
|
||||||
|
|
||||||
|
# Public can view prioritized content
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "forum-network",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^8.27.0",
|
||||||
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
|
"eslint-plugin-html": "^7.1.0",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
|
"prettier-eslint": "^15.0.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { Action } from '../display/action.js';
|
||||||
|
import { CryptoUtil } from '../supporting/crypto.js';
|
||||||
|
import { ReputationHolder } from '../reputation/reputation-holder.js';
|
||||||
|
import { EdgeTypes } from '../../util/constants.js';
|
||||||
|
|
||||||
|
export class Expert extends ReputationHolder {
|
||||||
|
constructor(dao, name, scene, options) {
|
||||||
|
super(name, scene, options);
|
||||||
|
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.tokens = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getReputation() {
|
||||||
|
const authorVertex = this.dao.forum.graph.getVertex(this.reputationPublicKey);
|
||||||
|
if (!authorVertex) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const authorEdges = authorVertex.getEdges(EdgeTypes.AUTHOR, false);
|
||||||
|
const tokenValues = authorEdges.map(({ data: { tokenId } }) => this.dao.reputation.valueOf(tokenId));
|
||||||
|
return tokenValues.reduce((value, total) => total += value, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 submitPostWithFee(postContent, { fee }, params) {
|
||||||
|
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({ fee, postId }, params);
|
||||||
|
this.tokens.push(pool.tokenId);
|
||||||
|
return { postId, pool };
|
||||||
|
}
|
||||||
|
|
||||||
|
async initiateValidationPool({ postId, fee }, params) {
|
||||||
|
// For now, make direct call rather than network
|
||||||
|
const pool = await this.dao.initiateValidationPool(this, {
|
||||||
|
reputationPublicKey: this.reputationPublicKey,
|
||||||
|
postId,
|
||||||
|
fee,
|
||||||
|
}, params);
|
||||||
|
this.tokens.push(pool.tokenId);
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
async stake(validationPool, {
|
||||||
|
position, amount, lockingTime,
|
||||||
|
}) {
|
||||||
|
// TODO: encrypt stake
|
||||||
|
// TODO: sign message
|
||||||
|
await this.actions.stake.log(
|
||||||
|
this,
|
||||||
|
validationPool,
|
||||||
|
`(${position ? 'for' : 'against'}, stake: ${amount})`,
|
||||||
|
);
|
||||||
|
return validationPool.stake(this.reputationPublicKey, {
|
||||||
|
position, amount, lockingTime, tokenId: this.tokens[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerAvailability(stakeAmount, duration) {
|
||||||
|
await this.actions.registerAvailability.log(
|
||||||
|
this,
|
||||||
|
this.dao.availability,
|
||||||
|
`(stake: ${stakeAmount}, duration: ${duration})`,
|
||||||
|
);
|
||||||
|
this.workerId = await this.dao.availability.register(this.reputationPublicKey, {
|
||||||
|
stakeAmount,
|
||||||
|
tokenId: this.tokens[0],
|
||||||
|
duration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAssignedWork() {
|
||||||
|
const requestId = await this.dao.availability.getAssignedWork(this.workerId);
|
||||||
|
const request = await this.dao.business.getRequest(requestId);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitWork(requestId, evidence, { tokenLossRatio, duration }) {
|
||||||
|
await this.actions.submitWork.log(this, this.dao.business);
|
||||||
|
return this.dao.business.submitWork(this.reputationPublicKey, requestId, evidence, { tokenLossRatio, duration });
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Action } from '../display/action.js';
|
||||||
|
import { Actor } from '../display/actor.js';
|
||||||
|
|
||||||
|
export class Public extends Actor {
|
||||||
|
constructor(name, scene) {
|
||||||
|
super(name, scene);
|
||||||
|
this.actions = {
|
||||||
|
submitRequest: new Action('submit work request', scene),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitRequest(business, { fee }, content) {
|
||||||
|
this.actions.submitRequest.log(this, business, `(fee: ${fee})`);
|
||||||
|
return business.submitRequest(fee, content);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { Action } from '../display/action.js';
|
||||||
|
import { Actor } from '../display/actor.js';
|
||||||
|
import { CryptoUtil } from '../supporting/crypto.js';
|
||||||
|
|
||||||
|
class Worker {
|
||||||
|
constructor(reputationPublicKey, tokenId, stakeAmount, duration) {
|
||||||
|
this.reputationPublicKey = reputationPublicKey;
|
||||||
|
this.tokenId = tokenId;
|
||||||
|
this.stakeAmount = stakeAmount;
|
||||||
|
this.duration = duration;
|
||||||
|
this.available = true;
|
||||||
|
this.assignedRequestId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purpose: Enable staking reputation to enter the pool of workers
|
||||||
|
*/
|
||||||
|
export class Availability extends Actor {
|
||||||
|
constructor(dao, name, scene) {
|
||||||
|
super(name, scene);
|
||||||
|
this.dao = dao;
|
||||||
|
|
||||||
|
this.actions = {
|
||||||
|
assignWork: new Action('assign work', scene),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.workers = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
register(reputationPublicKey, { stakeAmount, tokenId, duration }) {
|
||||||
|
// TODO: Should be signed by token owner
|
||||||
|
this.dao.reputation.lock(tokenId, stakeAmount, duration);
|
||||||
|
const workerId = CryptoUtil.randomUUID();
|
||||||
|
this.workers.set(workerId, new Worker(reputationPublicKey, tokenId, stakeAmount, duration));
|
||||||
|
return workerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
get availableWorkers() {
|
||||||
|
return Array.from(this.workers.values()).filter(({ available }) => !!available);
|
||||||
|
}
|
||||||
|
|
||||||
|
async assignWork(requestId) {
|
||||||
|
const totalAmountStaked = this.availableWorkers
|
||||||
|
.reduce((total, { stakeAmount }) => total += stakeAmount, 0);
|
||||||
|
// Imagine all these amounts layed end-to-end along a number line.
|
||||||
|
// To weight choice by amount staked, pick a stake by choosing a number at random
|
||||||
|
// from within that line segment.
|
||||||
|
const randomChoice = Math.random() * totalAmountStaked;
|
||||||
|
let index = 0;
|
||||||
|
let acc = 0;
|
||||||
|
for (const { stakeAmount } of this.workers.values()) {
|
||||||
|
acc += stakeAmount;
|
||||||
|
if (acc >= randomChoice) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
const worker = this.availableWorkers[index];
|
||||||
|
worker.available = false;
|
||||||
|
worker.assignedRequestId = requestId;
|
||||||
|
|
||||||
|
// TODO: Notify assignee
|
||||||
|
return worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAssignedWork(workerId) {
|
||||||
|
const worker = this.workers.get(workerId);
|
||||||
|
return worker.assignedRequestId;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { randomID } from '../../util/helpers.js';
|
||||||
|
import { Action } from '../display/action.js';
|
||||||
|
import { Actor } from '../display/actor.js';
|
||||||
|
import { PostContent } from '../supporting/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(this, {
|
||||||
|
postId,
|
||||||
|
fee: request.fee,
|
||||||
|
reputationPublicKey,
|
||||||
|
}, {
|
||||||
|
duration,
|
||||||
|
tokenLossRatio,
|
||||||
|
});
|
||||||
|
|
||||||
|
await pool.stake(reputationPublicKey, {
|
||||||
|
tokenId: request.worker.tokenId,
|
||||||
|
amount: request.worker.stakeAmount,
|
||||||
|
position: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// When the validation pool concludes,
|
||||||
|
// reputation should be awarded and fees should be distributed.
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export class Client {
|
||||||
|
constructor(dao, expert) {
|
||||||
|
this.dao = dao;
|
||||||
|
this.expert = expert;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { Forum } from './forum.js';
|
||||||
|
import { ReputationTokenContract } from '../reputation/reputation-token.js';
|
||||||
|
import { ValidationPool } from './validation-pool.js';
|
||||||
|
import { Availability } from './availability.js';
|
||||||
|
import { Business } from './business.js';
|
||||||
|
import { Voter } from '../supporting/voter.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, options) {
|
||||||
|
super(name, scene, options);
|
||||||
|
|
||||||
|
/* 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
addVoteRecord(reputationPublicKey, validationPool) {
|
||||||
|
const voter = this.experts.get(reputationPublicKey) ?? new Voter(reputationPublicKey);
|
||||||
|
voter.addVoteRecord(validationPool);
|
||||||
|
this.experts.set(reputationPublicKey, voter);
|
||||||
|
}
|
||||||
|
|
||||||
|
listActiveVoters({ activeVoterThreshold } = {}) {
|
||||||
|
return Array.from(this.experts.values()).filter((voter) => {
|
||||||
|
const hasVoted = !!voter.dateLastVote;
|
||||||
|
const withinThreshold = !activeVoterThreshold
|
||||||
|
|| new Date() - voter.dateLastVote >= 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(fromActor, { postId, reputationPublicKey, fee }, params) {
|
||||||
|
const validationPoolNumber = this.validationPools.size + 1;
|
||||||
|
const name = `Pool${validationPoolNumber}`;
|
||||||
|
const pool = new ValidationPool(this, {
|
||||||
|
postId, reputationPublicKey, fee,
|
||||||
|
}, params, name, this.scene, fromActor);
|
||||||
|
this.validationPools.set(pool.id, pool);
|
||||||
|
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,322 @@
|
||||||
|
import { WeightedDirectedGraph } from '../supporting/wdg.js';
|
||||||
|
import { Action } from '../display/action.js';
|
||||||
|
import { Actor } from '../display/actor.js';
|
||||||
|
import { ReputationHolder } from '../reputation/reputation-holder.js';
|
||||||
|
import { displayNumber } from '../../util/helpers.js';
|
||||||
|
import {
|
||||||
|
EPSILON, INCINERATOR_ADDRESS, EdgeTypes, VertexTypes,
|
||||||
|
} from '../../util/constants.js';
|
||||||
|
|
||||||
|
class Post extends Actor {
|
||||||
|
constructor(forum, senderId, postContent) {
|
||||||
|
const index = forum.graph.countVertices(VertexTypes.POST);
|
||||||
|
const name = `Post${index + 1}`;
|
||||||
|
super(name, forum.scene);
|
||||||
|
this.forum = forum;
|
||||||
|
this.id = postContent.id ?? name;
|
||||||
|
this.senderId = senderId;
|
||||||
|
this.value = 0;
|
||||||
|
this.initialValue = 0;
|
||||||
|
this.authors = postContent.authors;
|
||||||
|
this.citations = postContent.citations;
|
||||||
|
this.title = postContent.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setValue(value) {
|
||||||
|
this.value = value;
|
||||||
|
await this.setDisplayValue('value', value);
|
||||||
|
this.forum.graph.getVertex(this.id).setProperty('value', value).displayVertex();
|
||||||
|
}
|
||||||
|
|
||||||
|
setInitialValue(value) {
|
||||||
|
this.initialValue = value;
|
||||||
|
this.forum.graph.getVertex(this.id).setProperty('initialValue', value).displayVertex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.graph = new WeightedDirectedGraph(scene);
|
||||||
|
this.actions = {
|
||||||
|
propagate: new Action('propagate', scene),
|
||||||
|
confirm: new Action('confirm', scene),
|
||||||
|
transfer: new Action('transfer', scene),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async addPost(senderId, postContent) {
|
||||||
|
console.log('addPost', { senderId, postContent });
|
||||||
|
const post = new Post(this, senderId, postContent);
|
||||||
|
this.graph.addVertex(VertexTypes.POST, post.id, post, post.name);
|
||||||
|
for (const { postId: citedPostId, weight } of post.citations) {
|
||||||
|
// Special case: Incinerator
|
||||||
|
if (citedPostId === INCINERATOR_ADDRESS && !this.graph.getVertex(INCINERATOR_ADDRESS)) {
|
||||||
|
this.graph.addVertex(VertexTypes.POST, INCINERATOR_ADDRESS, { name: 'Incinerator' }, 'Incinerator');
|
||||||
|
}
|
||||||
|
this.graph.addEdge(EdgeTypes.CITATION, post.id, citedPostId, weight);
|
||||||
|
}
|
||||||
|
return post;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPost(postId) {
|
||||||
|
return this.graph.getVertexData(postId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPosts() {
|
||||||
|
return this.graph.getVerticesData();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTotalValue() {
|
||||||
|
return this.getPosts().reduce((total, { value }) => total += value, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLatestContract(type) { }
|
||||||
|
|
||||||
|
// getContract(type) { }
|
||||||
|
|
||||||
|
async onValidate({
|
||||||
|
pool, postId, tokenId, referenceChainLimit, leachingValue,
|
||||||
|
}) {
|
||||||
|
console.log('onValidate', { pool, postId, tokenId });
|
||||||
|
const initialValue = this.dao.reputation.valueOf(tokenId);
|
||||||
|
const postVertex = this.graph.getVertex(postId);
|
||||||
|
const post = postVertex.data;
|
||||||
|
post.setStatus('Validated');
|
||||||
|
post.initialValue = initialValue;
|
||||||
|
|
||||||
|
const addAuthorToGraph = (publicKey, weight, authorTokenId) => {
|
||||||
|
// For graph display purposes, we want to use the existing Expert actors from the current scene.
|
||||||
|
const author = this.scene.findActor(({ reputationPublicKey }) => reputationPublicKey === publicKey);
|
||||||
|
author.setDisplayValue('reputation', () => author.getReputation());
|
||||||
|
const authorVertex = this.graph.getVertex(publicKey)
|
||||||
|
?? this.graph.addVertex(VertexTypes.AUTHOR, publicKey, author, author.name, {
|
||||||
|
hide: author.options.hide,
|
||||||
|
});
|
||||||
|
this.graph.addEdge(
|
||||||
|
EdgeTypes.AUTHOR,
|
||||||
|
postVertex,
|
||||||
|
authorVertex,
|
||||||
|
weight,
|
||||||
|
{ tokenId: authorTokenId },
|
||||||
|
{ hide: author.options.hide },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// In the case of multiple authors, mint additional (empty) tokens.
|
||||||
|
// If no authors are specified, treat the sender as the sole author.
|
||||||
|
// TODO: Verify that cumulative author weight == 1.
|
||||||
|
if (!post.authors?.length) {
|
||||||
|
addAuthorToGraph(post.senderId, 1, tokenId);
|
||||||
|
} else {
|
||||||
|
for (const { publicKey, weight } of post.authors) {
|
||||||
|
// If the sender is also listed among the authors, do not mint them an additional token.
|
||||||
|
const authorTokenId = (publicKey === post.senderId) ? tokenId : this.dao.reputation.mint(this.id, 0);
|
||||||
|
addAuthorToGraph(publicKey, weight, authorTokenId);
|
||||||
|
}
|
||||||
|
// If the sender is not an author, they will end up with the minted token but with zero value.
|
||||||
|
if (!post.authors.find(({ publicKey }) => publicKey === post.senderId)) {
|
||||||
|
addAuthorToGraph(post.senderId, 0, tokenId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rewardsAccumulator = new Map();
|
||||||
|
|
||||||
|
// Compute reputation rewards
|
||||||
|
await this.propagateValue(
|
||||||
|
{ to: postVertex, from: { data: pool } },
|
||||||
|
{
|
||||||
|
rewardsAccumulator,
|
||||||
|
increment: initialValue,
|
||||||
|
referenceChainLimit,
|
||||||
|
leachingValue,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply computed rewards to update values of tokens
|
||||||
|
for (const [authorEdge, amount] of rewardsAccumulator) {
|
||||||
|
const { to: authorVertex, data: { tokenId: authorTokenId } } = authorEdge;
|
||||||
|
const { data: author } = authorVertex;
|
||||||
|
// The primary author gets the validation pool minted token.
|
||||||
|
// So we don't need to transfer any reputation to the primary author.
|
||||||
|
// Their reward will be the remaining balance after all other transfers.
|
||||||
|
if (authorTokenId !== tokenId) {
|
||||||
|
if (amount < 0) {
|
||||||
|
this.dao.reputation.transferValueFrom(authorTokenId, tokenId, -amount);
|
||||||
|
} else {
|
||||||
|
this.dao.reputation.transferValueFrom(tokenId, authorTokenId, amount);
|
||||||
|
}
|
||||||
|
await author.computeDisplayValues((label, value) => authorVertex.setProperty(label, value));
|
||||||
|
authorVertex.displayVertex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const senderVertex = this.graph.getVertex(post.senderId);
|
||||||
|
const { data: sender } = senderVertex;
|
||||||
|
await sender.computeDisplayValues((label, value) => senderVertex.setProperty(label, value));
|
||||||
|
senderVertex.displayVertex();
|
||||||
|
|
||||||
|
// Transfer ownership of the minted tokens to the authors
|
||||||
|
for (const authorEdge of postVertex.getEdges(EdgeTypes.AUTHOR, true)) {
|
||||||
|
const authorVertex = authorEdge.to;
|
||||||
|
const author = authorVertex.data;
|
||||||
|
const { tokenId: authorTokenId } = authorEdge.data;
|
||||||
|
this.dao.reputation.transfer(this.id, author.reputationPublicKey, authorTokenId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Edge} edge
|
||||||
|
* @param {Object} opaqueData
|
||||||
|
*/
|
||||||
|
async propagateValue(edge, {
|
||||||
|
rewardsAccumulator,
|
||||||
|
increment,
|
||||||
|
depth = 0,
|
||||||
|
initialNegative = false,
|
||||||
|
referenceChainLimit,
|
||||||
|
leachingValue,
|
||||||
|
}) {
|
||||||
|
const postVertex = edge.to;
|
||||||
|
const post = postVertex.data;
|
||||||
|
this.actions.propagate.log(edge.from.data, post, `(${increment})`);
|
||||||
|
|
||||||
|
if (!!referenceChainLimit && depth > referenceChainLimit) {
|
||||||
|
this.actions.propagate.log(
|
||||||
|
edge.from.data,
|
||||||
|
post,
|
||||||
|
`referenceChainLimit (${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(EdgeTypes.CITATION, true)
|
||||||
|
.filter(({ weight }) => (positive ? weight > 0 : weight < 0));
|
||||||
|
for (const citationEdge of citationEdges) {
|
||||||
|
const { weight } = citationEdge;
|
||||||
|
let outboundAmount = weight * increment;
|
||||||
|
if (Math.abs(outboundAmount) > EPSILON) {
|
||||||
|
const balanceToOutbound = this.graph.getEdgeWeight(EdgeTypes.BALANCE, citationEdge.from, citationEdge.to)
|
||||||
|
?? 0;
|
||||||
|
let refundFromOutbound = 0;
|
||||||
|
|
||||||
|
// Special case: Incineration.
|
||||||
|
if (citationEdge.to.id === INCINERATOR_ADDRESS) {
|
||||||
|
// Only a positive amount may be incinerated! Otherwise the sink could be used as a source.
|
||||||
|
if (outboundAmount < 0) {
|
||||||
|
this.scene?.flowchart?.log(`style ${citationEdge.from.id} fill:#620000`);
|
||||||
|
this.actions.propagate.log(
|
||||||
|
citationEdge.from.data,
|
||||||
|
{ name: 'Incinerator' },
|
||||||
|
`(${increment})`,
|
||||||
|
undefined,
|
||||||
|
'-x',
|
||||||
|
);
|
||||||
|
throw new Error('Incinerator can only receive positive citations!');
|
||||||
|
}
|
||||||
|
// Reputation sent to the incinerator is burned! This means it is deducted from the sender,
|
||||||
|
// without increasing the value of any other token.
|
||||||
|
this.actions.propagate.log(citationEdge.from.data, { name: 'Incinerator' }, `(${increment})`);
|
||||||
|
} else {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively propagate reputation effects
|
||||||
|
refundFromOutbound = await this.propagateValue(citationEdge, {
|
||||||
|
rewardsAccumulator,
|
||||||
|
increment: outboundAmount,
|
||||||
|
depth: depth + 1,
|
||||||
|
initialNegative: initialNegative || (depth === 0 && outboundAmount < 0),
|
||||||
|
referenceChainLimit,
|
||||||
|
leachingValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Any excess (negative) amount that could not be propagated,
|
||||||
|
// i.e. because a cited post has been reduced to zero value,
|
||||||
|
// is retained by the citing post.
|
||||||
|
outboundAmount -= refundFromOutbound;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep a record of the effect of the reputation transferred along this edge in the graph,
|
||||||
|
// so that later, negative citations can be constrained to at most undo these effects.
|
||||||
|
this.graph.setEdgeWeight(
|
||||||
|
EdgeTypes.BALANCE,
|
||||||
|
citationEdge.from,
|
||||||
|
citationEdge.to,
|
||||||
|
balanceToOutbound + outboundAmount,
|
||||||
|
);
|
||||||
|
totalOutboundAmount += outboundAmount;
|
||||||
|
|
||||||
|
this.actions.confirm.log(
|
||||||
|
citationEdge.to.data,
|
||||||
|
citationEdge.from.data,
|
||||||
|
`(refund: ${displayNumber(refundFromOutbound)}, leach: ${outboundAmount * leachingValue})`,
|
||||||
|
undefined,
|
||||||
|
'-->>',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return totalOutboundAmount;
|
||||||
|
};
|
||||||
|
|
||||||
|
// First, leach value via negative citations
|
||||||
|
const totalLeachingAmount = await propagate(false);
|
||||||
|
increment -= totalLeachingAmount * leachingValue;
|
||||||
|
|
||||||
|
// Now propagate value via positive citations
|
||||||
|
const totalDonationAmount = await propagate(true);
|
||||||
|
increment -= totalDonationAmount * 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;
|
||||||
|
|
||||||
|
// Apply reputation effects to post authors, not to the post directly
|
||||||
|
for (const authorEdge of postVertex.getEdges(EdgeTypes.AUTHOR, true)) {
|
||||||
|
const { weight, to: { data: author } } = authorEdge;
|
||||||
|
const authorIncrement = weight * appliedIncrement;
|
||||||
|
rewardsAccumulator.set(authorEdge, authorIncrement);
|
||||||
|
this.actions.propagate.log(post, author, `(${authorIncrement})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('propagateValue end', {
|
||||||
|
depth,
|
||||||
|
increment,
|
||||||
|
rawNewValue,
|
||||||
|
newValue,
|
||||||
|
appliedIncrement,
|
||||||
|
refundToInbound,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Increment the value of the post
|
||||||
|
await post.setValue(newValue);
|
||||||
|
|
||||||
|
return refundToInbound;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,349 @@
|
||||||
|
import { ReputationHolder } from '../reputation/reputation-holder.js';
|
||||||
|
import { Stake } from '../supporting/stake.js';
|
||||||
|
import { Action } from '../display/action.js';
|
||||||
|
import { displayNumber } from '../../util/helpers.js';
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
fromActor,
|
||||||
|
) {
|
||||||
|
super(name, scene);
|
||||||
|
this.id = this.reputationPublicKey;
|
||||||
|
|
||||||
|
this.actions = {
|
||||||
|
initiate: new Action('initiate validation pool', scene),
|
||||||
|
reward: new Action('reward', scene),
|
||||||
|
transfer: new Action('transfer', scene),
|
||||||
|
mint: new Action('mint', scene),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.actions.initiate.log(fromActor, this, `(fee: ${fee})`);
|
||||||
|
this.activate();
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
const post = this.dao.forum.graph.getVertexData(postId);
|
||||||
|
|
||||||
|
const leachingTotal = post.citations
|
||||||
|
.filter(({ weight }) => weight < 0)
|
||||||
|
.reduce((total, { weight }) => total += -weight, 0);
|
||||||
|
const donationTotal = post.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 (post.citations.some(({ weight }) => Math.abs(weight) > params.revaluationLimit)) {
|
||||||
|
throw new Error(`Each citation magnitude must not exceed revaluation limit ${params.revaluationLimit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (post.authors?.length) {
|
||||||
|
const totalAuthorWeight = post.authors.reduce((total, { weight }) => total += weight, 0);
|
||||||
|
if (totalAuthorWeight !== 1) {
|
||||||
|
throw new Error(`Total author weight ${totalAuthorWeight} !== 1`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
this.dao.addVoteRecord(reputationPublicKey, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
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({ lockingTimeExponent: params.lockingTimeExponent }))
|
||||||
|
.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) {
|
||||||
|
this.dao.addVoteRecord(reputationPublicKey, this);
|
||||||
|
|
||||||
|
// Update computed display values
|
||||||
|
const actor = this.scene?.findActor((a) => a.reputationPublicKey === reputationPublicKey);
|
||||||
|
await actor.computeDisplayValues();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.computeDisplayValues();
|
||||||
|
}
|
||||||
|
await this.dao.computeDisplayValues();
|
||||||
|
|
||||||
|
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({ lockingTimeExponent: params.lockingTimeExponent });
|
||||||
|
const reward = tokensForWinners * (value / totalValueOfStakesForWin);
|
||||||
|
// Also return each winning voter their staked amount
|
||||||
|
const reputationPublicKey = this.dao.reputation.ownerOf(tokenId);
|
||||||
|
console.log(`reward for winning stake by ${reputationPublicKey}: ${reward}`);
|
||||||
|
this.dao.reputation.transferValueFrom(this.tokenId, tokenId, reward + amount);
|
||||||
|
const toActor = this.scene?.findActor((actor) => actor.reputationPublicKey === reputationPublicKey);
|
||||||
|
this.actions.reward.log(this, toActor, `(${displayNumber(reward)})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
referenceChainLimit: params.referenceChainLimit,
|
||||||
|
leachingValue: params.leachingValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('pool complete');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
export class Action {
|
||||||
|
constructor(name, scene) {
|
||||||
|
this.name = name;
|
||||||
|
this.scene = scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param src
|
||||||
|
* @param dest
|
||||||
|
* @param msg
|
||||||
|
* @param obj
|
||||||
|
* @param symbol
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async log(src, dest, msg, obj, symbol = '->>') {
|
||||||
|
await this.scene?.sequence?.log(
|
||||||
|
`${src.name} ${symbol} ${dest.name} : ${this.name} ${msg ?? ''} ${
|
||||||
|
JSON.stringify(obj) ?? ''
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { displayNumber } from '../../util/helpers.js';
|
||||||
|
|
||||||
|
export class Actor {
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @param {Scene} scene
|
||||||
|
* @param {boolean} options.announce
|
||||||
|
* @param {boolean} options.hide
|
||||||
|
*/
|
||||||
|
constructor(name, scene, options = {}) {
|
||||||
|
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;
|
||||||
|
this.options = options;
|
||||||
|
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.computeDisplayValues();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDisplayValue(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {(label: string, value) => {}} cb
|
||||||
|
*/
|
||||||
|
async computeDisplayValues(cb) {
|
||||||
|
for (const [label, fn] of this.valueFunctions.entries()) {
|
||||||
|
const value = fn();
|
||||||
|
await this.setDisplayValue(label, value);
|
||||||
|
if (cb) {
|
||||||
|
cb(label, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getValuesMap() {
|
||||||
|
return new Map(Array.from(this.values.entries())
|
||||||
|
.map(([key, displayValue]) => [key, {
|
||||||
|
name: displayValue.getName(),
|
||||||
|
value: displayValue.get(),
|
||||||
|
}]));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { DisplayValue } from './display-value.js';
|
||||||
|
import { randomID } from '../../util/helpers.js';
|
||||||
|
|
||||||
|
export class Box {
|
||||||
|
constructor(name, parentEl, options = {}) {
|
||||||
|
this.name = name;
|
||||||
|
const { tagName = 'div' } = options;
|
||||||
|
this.el = document.createElement(tagName);
|
||||||
|
this.el.box = this;
|
||||||
|
const id = options.id ?? randomID();
|
||||||
|
this.el.id = `${parentEl.id}_box_${id}`;
|
||||||
|
this.el.classList.add('box');
|
||||||
|
if (name) {
|
||||||
|
this.el.setAttribute('box-name', name);
|
||||||
|
}
|
||||||
|
if (parentEl) {
|
||||||
|
if (options.prepend) {
|
||||||
|
parentEl.prepend(this.el);
|
||||||
|
} else {
|
||||||
|
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) {
|
||||||
|
const box = new Box(name, this.el);
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
addDisplayValue(value) {
|
||||||
|
const box = this.addBox(value.name).flex();
|
||||||
|
return new DisplayValue(value, box);
|
||||||
|
}
|
||||||
|
|
||||||
|
setInnerHTML(html) {
|
||||||
|
this.el.innerHTML = html;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId() {
|
||||||
|
return this.el.id;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { displayNumber } from '../../util/helpers.js';
|
||||||
|
|
||||||
|
export class DisplayValue {
|
||||||
|
constructor(name, box) {
|
||||||
|
this.value = undefined;
|
||||||
|
this.name = name;
|
||||||
|
this.box = box;
|
||||||
|
this.nameBox = this.box.addBox(`${this.name}-name`).addClass('name');
|
||||||
|
this.valueBox = this.box.addBox(`${this.name}-value`).addClass('value');
|
||||||
|
this.nameBox.setInnerHTML(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.valueBox.setInnerHTML(typeof this.value === 'number' ? displayNumber(this.value, 6) : this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
this.value = value;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
get() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { Box } from './box.js';
|
||||||
|
import { Form } from './form.js';
|
||||||
|
|
||||||
|
export class Remark extends Box {
|
||||||
|
constructor(doc, text, opts = {}) {
|
||||||
|
super('Remark', opts.parentEl ?? doc.el, opts);
|
||||||
|
this.setInnerHTML(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const doc = new Document();
|
||||||
|
* const form1 = doc.form();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class Document extends Box {
|
||||||
|
elements = [];
|
||||||
|
|
||||||
|
form(opts) {
|
||||||
|
return this.addElement(new Form(this, opts));
|
||||||
|
}
|
||||||
|
|
||||||
|
remark(text, opts) {
|
||||||
|
return this.addElement(new Remark(this, text, opts));
|
||||||
|
}
|
||||||
|
|
||||||
|
addElement(element) {
|
||||||
|
this.elements.push(element);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.el.innerHTML = '';
|
||||||
|
this.elements = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get lastElement() {
|
||||||
|
if (!this.elements.length) return null;
|
||||||
|
return this.elements[this.elements.length - 1];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { MermaidDiagram } from './mermaid.js';
|
||||||
|
|
||||||
|
export class Flowchart extends MermaidDiagram {
|
||||||
|
constructor(box, logBox, { direction = 'BT' } = {}) {
|
||||||
|
super(box, logBox);
|
||||||
|
this.direction = direction;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.log(`graph ${this.direction}`, false);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
import { randomID } from '../../util/helpers.js';
|
||||||
|
import { Box } from './box.js';
|
||||||
|
|
||||||
|
export class FormElement extends Box {
|
||||||
|
constructor(name, form, opts = {}) {
|
||||||
|
const parentEl = opts.parentEl ?? form.el;
|
||||||
|
super(name, parentEl, opts);
|
||||||
|
this.form = form;
|
||||||
|
this.id = opts.id ?? name;
|
||||||
|
const { cb, cbEventTypes = ['change'], cbOnInit = false } = opts;
|
||||||
|
if (cb) {
|
||||||
|
cbEventTypes.forEach((eventType) => this.el.addEventListener(eventType, () => {
|
||||||
|
cb(this, { initializing: false });
|
||||||
|
}));
|
||||||
|
if (cbOnInit) {
|
||||||
|
cb(this, { initializing: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Button extends FormElement {
|
||||||
|
constructor(name, form, opts) {
|
||||||
|
super(name, form, { ...opts, cbEventTypes: ['click'] });
|
||||||
|
this.button = document.createElement('button');
|
||||||
|
this.button.setAttribute('type', opts.type ?? 'button');
|
||||||
|
this.button.innerHTML = name;
|
||||||
|
this.button.disabled = !!opts.disabled;
|
||||||
|
this.el.appendChild(this.button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TextField extends FormElement {
|
||||||
|
constructor(name, form, opts) {
|
||||||
|
super(name, form, opts);
|
||||||
|
this.label = document.createElement('label');
|
||||||
|
this.labelDiv = document.createElement('div');
|
||||||
|
this.label.appendChild(this.labelDiv);
|
||||||
|
this.labelDiv.innerHTML = name;
|
||||||
|
this.input = document.createElement('input');
|
||||||
|
this.input.disabled = !!opts.disabled;
|
||||||
|
this.input.defaultValue = opts.defaultValue || '';
|
||||||
|
this.label.appendChild(this.input);
|
||||||
|
this.el.appendChild(this.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.input?.value || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TextArea extends FormElement { }
|
||||||
|
|
||||||
|
export class SubFormArray extends FormElement {
|
||||||
|
constructor(name, form, opts) {
|
||||||
|
super(name, form, opts);
|
||||||
|
this.subForms = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.subForms.map((subForm) => subForm.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(subForm) {
|
||||||
|
const idx = this.subForms.findIndex((s) => s === subForm);
|
||||||
|
this.subForms.splice(idx, 1);
|
||||||
|
subForm.el.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SubForm extends FormElement {
|
||||||
|
constructor(name, form, opts) {
|
||||||
|
const parentEl = opts.subFormArray ? opts.subFormArray.el : form.el;
|
||||||
|
const subForm = form.document.form({ name, parentEl, tagName: 'div' }).lastElement;
|
||||||
|
super(name, form, { ...opts, parentEl });
|
||||||
|
this.subForm = subForm;
|
||||||
|
if (opts.subFormArray) {
|
||||||
|
opts.subFormArray.subForms.push(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.subForm.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Form extends Box {
|
||||||
|
constructor(document, opts = {}) {
|
||||||
|
super(opts.name, opts.parentEl || document.el, { tagName: 'form', ...opts });
|
||||||
|
this.document = document;
|
||||||
|
this.items = [];
|
||||||
|
this.id = opts.id ?? `form_${randomID()}`;
|
||||||
|
// Action should be handled by a submit button
|
||||||
|
this.el.onsubmit = () => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
button(opts) {
|
||||||
|
this.items.push(new Button(opts.name, this, opts));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
textField(opts) {
|
||||||
|
this.items.push(new TextField(opts.name, this, opts));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
textArea(opts) {
|
||||||
|
this.items.push(new TextArea(opts.name, this, opts));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
subForm(opts) {
|
||||||
|
this.items.push(new SubForm(opts.name, this, opts));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
subFormArray(opts) {
|
||||||
|
this.items.push(new SubFormArray(opts.name, this, opts));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lastItem() {
|
||||||
|
return this.items[this.items.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.items.reduce((obj, { id, value }) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
obj[id] = value;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
import mermaid from 'https://unpkg.com/mermaid@9.2.2/dist/mermaid.esm.min.mjs';
|
||||||
|
import { debounce } from '../../util/helpers.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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
static initializeAPI() {
|
||||||
|
mermaid.mermaidAPI.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
|
theme: 'base',
|
||||||
|
themeVariables: {
|
||||||
|
darkMode: true,
|
||||||
|
primaryColor: '#2a5b6c',
|
||||||
|
primaryTextColor: '#b6b6b6',
|
||||||
|
lineColor: '#57747d',
|
||||||
|
signalColor: '#57747d',
|
||||||
|
noteBkgColor: '#516f77',
|
||||||
|
noteTextColor: '#cecece',
|
||||||
|
activationBkgColor: '#1d3f49',
|
||||||
|
activationBorderColor: '#569595',
|
||||||
|
},
|
||||||
|
securityLevel: 'loose', // 'loose' so that we can use click events
|
||||||
|
// logLevel: 'debug',
|
||||||
|
useMaxWidth: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
await mermaid.mermaidAPI.render(
|
||||||
|
this.element.getId(),
|
||||||
|
text,
|
||||||
|
(svgCode, bindFunctions) => {
|
||||||
|
this.renderBox.setInnerHTML(svgCode);
|
||||||
|
if (bindFunctions) {
|
||||||
|
bindFunctions(this.renderBox.el);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`render text:\n${text}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.logBoxPre.textContent = '';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
class Button {
|
||||||
|
constructor(innerHTML, onclick) {
|
||||||
|
this.el = document.createElement('button');
|
||||||
|
this.el.innerHTML = innerHTML;
|
||||||
|
this.el.onclick = onclick;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SceneControls {
|
||||||
|
constructor(parentBox) {
|
||||||
|
this.disableAutoplayButton = new Button('Disable Auto-play', () => {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('auto', 'false');
|
||||||
|
window.location.href = url.href;
|
||||||
|
});
|
||||||
|
this.enableAutoplayButton = new Button('Enable Auto-play', () => {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.delete('auto');
|
||||||
|
window.location.href = url.href;
|
||||||
|
});
|
||||||
|
this.stepButton = new Button('Next Action', () => {
|
||||||
|
console.log('Next Action button clicked');
|
||||||
|
document.dispatchEvent(new Event('next-action'));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (window.autoPlay) {
|
||||||
|
parentBox.el.appendChild(this.disableAutoplayButton.el);
|
||||||
|
} else {
|
||||||
|
parentBox.el.appendChild(this.enableAutoplayButton.el);
|
||||||
|
parentBox.el.appendChild(this.stepButton.el);
|
||||||
|
// Disable `stepButton` when test is complete
|
||||||
|
setInterval(() => {
|
||||||
|
this.stepButton.el.disabled = mocha._state !== 'running';
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function delayOrWait(delayMs) {
|
||||||
|
if (window.autoPlay) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, delayMs);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
document.addEventListener('next-action', () => {
|
||||||
|
console.log('next-action event received');
|
||||||
|
resolve();
|
||||||
|
}, { once: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
import { Action } from './action.js';
|
||||||
|
import { CryptoUtil } from '../supporting/crypto.js';
|
||||||
|
import { MermaidDiagram } from './mermaid.js';
|
||||||
|
import { SequenceDiagram } from './sequence.js';
|
||||||
|
import { Table } from './table.js';
|
||||||
|
import { Flowchart } from './flowchart.js';
|
||||||
|
import { SceneControls } from './scene-controls.js';
|
||||||
|
import { Box } from './box.js';
|
||||||
|
import { Document } from './document.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();
|
||||||
|
this.documents = [];
|
||||||
|
|
||||||
|
MermaidDiagram.initializeAPI();
|
||||||
|
|
||||||
|
this.options = {
|
||||||
|
edgeNodeColor: '#4d585c',
|
||||||
|
};
|
||||||
|
|
||||||
|
window.autoPlay = new URL(window.location.href).searchParams.get('auto') !== 'false';
|
||||||
|
|
||||||
|
if (!window.disableSceneControls) {
|
||||||
|
this.topRail = new Box('Top rail', document.body, { prepend: true }).addClass('top-rail');
|
||||||
|
const controlsBox = this.topRail.addBox('SceneControls').addClass('scene-controls');
|
||||||
|
this.controls = new SceneControls(controlsBox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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' } = {}) {
|
||||||
|
this.withSectionFlowchart({ direction, section: this.topSection });
|
||||||
|
this.flowchart = this.lastFlowchart;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
withSectionFlowchart({
|
||||||
|
id, name, direction = 'BT', section,
|
||||||
|
} = {}) {
|
||||||
|
section = section ?? this.middleSection;
|
||||||
|
const index = this.flowcharts.size;
|
||||||
|
name = name ?? `Flowchart ${index}`;
|
||||||
|
id = id ?? `flowchart_${CryptoUtil.randomUUID().slice(0, 4)}`;
|
||||||
|
const container = section.addBox(name).flex();
|
||||||
|
const box = container.addBox('Flowchart').addClass('padded');
|
||||||
|
const logBox = container.addBox('Flowchart text').addClass('dim');
|
||||||
|
const flowchart = new Flowchart(box, logBox, { direction });
|
||||||
|
this.flowcharts.set(id, flowchart);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {(Document): Document} cb
|
||||||
|
* @returns {Scene}
|
||||||
|
*/
|
||||||
|
withDocument(name, cb) {
|
||||||
|
this.documents = this.documents ?? [];
|
||||||
|
const doc = new Document(name, this.middleSection.el);
|
||||||
|
this.documents.push(cb ? cb(doc) : doc);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lastDocument() {
|
||||||
|
if (!this.documents?.length) return null;
|
||||||
|
return this.documents[this.documents.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
getDocument(name) {
|
||||||
|
return this.documents.find((doc) => doc.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerActor(actor) {
|
||||||
|
this.actors.add(actor);
|
||||||
|
if (actor.options.announce) {
|
||||||
|
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) {
|
||||||
|
if (!actor.options.hide) {
|
||||||
|
for (const [aKey, { name, value }] of actor.getValuesMap()) {
|
||||||
|
const key = `${actor.name}:${aKey}`;
|
||||||
|
columns.push({ key, title: name });
|
||||||
|
row.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
columns.push({ key: 'label', title: '' });
|
||||||
|
this.table.setColumns(columns);
|
||||||
|
this.table.addRow(row);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { hexToRGB } from '../../util/helpers.js';
|
||||||
|
import { MermaidDiagram } from './mermaid.js';
|
||||||
|
|
||||||
|
export class SequenceDiagram extends MermaidDiagram {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.activations = [];
|
||||||
|
this.sections = [];
|
||||||
|
|
||||||
|
this.log('sequenceDiagram', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async log(...args) {
|
||||||
|
this.sections.forEach(async (section, index) => {
|
||||||
|
const {
|
||||||
|
empty, r, g, b,
|
||||||
|
} = section;
|
||||||
|
if (empty) {
|
||||||
|
section.empty = false;
|
||||||
|
await super.log(`rect rgb(${r}, ${g}, ${b}) # ${index}`, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return super.log(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
activate(name) {
|
||||||
|
this.log(`activate ${name}`, false);
|
||||||
|
this.activations.push(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deactivate(name) {
|
||||||
|
const index = this.activations.findLastIndex((n) => n === name);
|
||||||
|
if (index === -1) throw new Error(`${name} does not appear to be active!`);
|
||||||
|
this.activations.splice(index, 1);
|
||||||
|
await this.log(`deactivate ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDeactivationsText() {
|
||||||
|
const text = Array.from(this.activations).reverse().reduce((str, name) => str += `deactivate ${name}\n`, '');
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
async startSection(color = '#08252c') {
|
||||||
|
let { r, g, b } = hexToRGB(color);
|
||||||
|
for (let i = 0; i < this.sections.length; i++) {
|
||||||
|
r += (0xff - r) / 16;
|
||||||
|
g += (0xff - g) / 16;
|
||||||
|
b += (0xff - b) / 16;
|
||||||
|
}
|
||||||
|
this.sections.push({
|
||||||
|
empty: true, r, g, b,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async endSection() {
|
||||||
|
const section = this.sections.pop();
|
||||||
|
if (section && !section.empty) {
|
||||||
|
await this.log('end');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSectionEndText() {
|
||||||
|
if (this.sections[this.sections.length - 1]?.empty) {
|
||||||
|
this.sections.pop();
|
||||||
|
}
|
||||||
|
return this.sections.map(() => 'end\n').join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
getText() {
|
||||||
|
let text = super.getText();
|
||||||
|
if (!text.endsWith('\n')) text = `${text}\n`;
|
||||||
|
text += this.getDeactivationsText();
|
||||||
|
text += this.getSectionEndText();
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { displayNumber } from '../../util/helpers.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 ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listUniqueValueKeys() {
|
||||||
|
return this.columns; // TODO
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export class BlockConsensus {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { DAO } from '../actors/dao.js';
|
||||||
|
/**
|
||||||
|
* An Exchange provides conversion between currencies / facilitates such.
|
||||||
|
* That means they carry some of the risk of managing these transactions.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Offer {
|
||||||
|
constructor({
|
||||||
|
_fromType, _toType, _amount, price,
|
||||||
|
}) {
|
||||||
|
this.price = price;
|
||||||
|
// const from =
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Exchange extends DAO {
|
||||||
|
constructor(name, scene) {
|
||||||
|
super(name, scene);
|
||||||
|
this.offersByType = new Map(); // <OfferType, Map<OfferId, Offer>>
|
||||||
|
}
|
||||||
|
|
||||||
|
getOffers(offerType) {
|
||||||
|
return this.buyOffersByType.get(offerType) ?? new Map(); // <
|
||||||
|
}
|
||||||
|
|
||||||
|
addOffer(offerOptions) {
|
||||||
|
const offer = new Offer(offerOptions);
|
||||||
|
const { fromType, toType } = offer;
|
||||||
|
[fromType, toType].forEach((type) => {
|
||||||
|
this.offersByType.s();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
requestTransaction({
|
||||||
|
fromType, toType, amount, price,
|
||||||
|
}) {
|
||||||
|
// this.
|
||||||
|
}
|
||||||
|
|
||||||
|
buy(fromType, toType, _priceParameters) {
|
||||||
|
const buyOffer = {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
/**
|
||||||
|
* Finance does the work of analysis of the dynamics of reputation and currency exchange
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class Finance {
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { randomID } from '../util/util/helpers.js';
|
||||||
|
|
||||||
|
class Pledge {
|
||||||
|
constructor({ stake, duration }) {
|
||||||
|
this.stake = stake;
|
||||||
|
this.duration = duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage work is providing data availability and integrity.
|
||||||
|
* It probably makes sense to manage it in pledges of finite duration.
|
||||||
|
*/
|
||||||
|
export class Storage {
|
||||||
|
constructor() {
|
||||||
|
this.pledges = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
pledge(pledgeOptions) {
|
||||||
|
const id = randomID();
|
||||||
|
this.pledge.set(id, new Pledge(pledgeOptions));
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { randomID } from '../../util/helpers.js';
|
||||||
|
import { Actor } from '../display/actor.js';
|
||||||
|
|
||||||
|
export class ReputationHolder extends Actor {
|
||||||
|
constructor(name, scene, options) {
|
||||||
|
super(name, scene, options);
|
||||||
|
this.reputationPublicKey = `${name}_${randomID()}`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
import { ERC721 } from '../supporting/erc721.js';
|
||||||
|
import { randomID } from '../../util/helpers.js';
|
||||||
|
import { EPSILON } from '../../util/constants.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}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param to
|
||||||
|
* @param value
|
||||||
|
* @param context
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
mint(to, value, context = {}) {
|
||||||
|
const tokenId = `token_${randomID()}`;
|
||||||
|
super.mint(to, tokenId);
|
||||||
|
this.values.set(tokenId, value);
|
||||||
|
this.histories.set(tokenId, [{ increment: value, context }]);
|
||||||
|
return tokenId;
|
||||||
|
}
|
||||||
|
|
||||||
|
incrementValue(tokenId, increment, context) {
|
||||||
|
const value = this.values.get(tokenId);
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error(`Token not found: ${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) {
|
||||||
|
const value = this.values.get(tokenId);
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error(`Token not found: ${tokenId}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
availableValueOf(tokenId) {
|
||||||
|
const amountLocked = Array.from(this.locks.values())
|
||||||
|
.filter(({ tokenId: lockTokenId }) => lockTokenId === tokenId)
|
||||||
|
.filter(({ dateCreated, duration }) => new Date() - dateCreated < duration)
|
||||||
|
.reduce((total, { amount }) => total += amount, 0);
|
||||||
|
|
||||||
|
return this.valueOf(tokenId) - amountLocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
valueOwnedBy(ownerId) {
|
||||||
|
return Array.from(this.owners.entries())
|
||||||
|
.filter(([__, owner]) => owner === ownerId)
|
||||||
|
.map(([tokenId, __]) => this.valueOf(tokenId))
|
||||||
|
.reduce((total, value) => total += value, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
availableValueOwnedBy(ownerId) {
|
||||||
|
return Array.from(this.owners.entries())
|
||||||
|
.filter(([__, owner]) => owner === ownerId)
|
||||||
|
.map(([tokenId, __]) => this.availableValueOf(tokenId))
|
||||||
|
.reduce((total, value) => total += value, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTotal() {
|
||||||
|
return Array.from(this.values.values()).reduce((total, value) => total += value, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTotalAvailable() {
|
||||||
|
const amountLocked = Array.from(this.locks.values())
|
||||||
|
.filter(({ dateCreated, duration }) => new Date() - dateCreated < duration)
|
||||||
|
.reduce((total, { amount }) => total += amount, 0);
|
||||||
|
|
||||||
|
return this.getTotal() - amountLocked;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
export class CryptoUtil {
|
||||||
|
static algorithm = 'RSASSA-PKCS1-v1_5';
|
||||||
|
|
||||||
|
static hash = 'SHA-256';
|
||||||
|
|
||||||
|
static async generateAsymmetricKey() {
|
||||||
|
return window.crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: CryptoUtil.algorithm,
|
||||||
|
hash: CryptoUtil.hash,
|
||||||
|
modulusLength: 2048,
|
||||||
|
publicExponent: new Uint8Array([1, 0, 1]),
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['sign', 'verify'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async sign(content, privateKey) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const encoded = encoder.encode(content);
|
||||||
|
const signature = await window.crypto.subtle.sign(CryptoUtil.algorithm, privateKey, encoded);
|
||||||
|
// Return base64-encoded signature
|
||||||
|
return btoa(String.fromCharCode(...new Uint8Array(signature)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async verify(content, b64publicKey, b64signature) {
|
||||||
|
// Convert base64 javascript web key to CryptoKey
|
||||||
|
const publicKey = await CryptoUtil.importKey(b64publicKey);
|
||||||
|
// Convert base64 signature to an ArrayBuffer
|
||||||
|
const signature = Uint8Array.from(atob(b64signature), (c) => c.charCodeAt(0));
|
||||||
|
// TODO: make a single TextEncoder instance and reuse it
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const encoded = encoder.encode(content);
|
||||||
|
return window.crypto.subtle.verify(CryptoUtil.algorithm, publicKey, signature, encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async exportKey(publicKey) {
|
||||||
|
// Store public key as base64 javascript web key
|
||||||
|
const jwk = await window.crypto.subtle.exportKey('jwk', publicKey);
|
||||||
|
return btoa(JSON.stringify(jwk));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async importKey(b64jwk) {
|
||||||
|
// Convert base64 javascript web key to CryptoKey
|
||||||
|
const jwk = JSON.parse(atob(b64jwk));
|
||||||
|
return window.crypto.subtle.importKey(
|
||||||
|
'jwk',
|
||||||
|
jwk,
|
||||||
|
{
|
||||||
|
name: CryptoUtil.algorithm,
|
||||||
|
hash: CryptoUtil.hash,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['verify'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static randomUUID() {
|
||||||
|
return window.crypto.randomUUID();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
export class Edge {
|
||||||
|
constructor(graph, type, from, to, weight, data, options = {}) {
|
||||||
|
this.graph = graph;
|
||||||
|
this.from = from;
|
||||||
|
this.to = to;
|
||||||
|
this.type = type;
|
||||||
|
this.weight = weight;
|
||||||
|
this.data = data;
|
||||||
|
this.options = options;
|
||||||
|
this.installedClickCallback = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.installedClickCallback = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getKey({
|
||||||
|
from, to, type,
|
||||||
|
}) {
|
||||||
|
return ['edge', from.id, to.id, type].join(':');
|
||||||
|
}
|
||||||
|
|
||||||
|
static getCombinedKey({ from, to }) {
|
||||||
|
return ['edge', from.id, to.id].join(':');
|
||||||
|
}
|
||||||
|
|
||||||
|
getComorphicEdges() {
|
||||||
|
return this.graph.getEdges(null, this.from, this.to);
|
||||||
|
}
|
||||||
|
|
||||||
|
getHtml() {
|
||||||
|
const edges = this.getComorphicEdges();
|
||||||
|
let html = '';
|
||||||
|
html += '<table>';
|
||||||
|
for (const { type, weight } of edges) {
|
||||||
|
html += `<tr><td>${type}</td><td>${weight}</td></tr>`;
|
||||||
|
}
|
||||||
|
html += '</table>';
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFlowchartNode() {
|
||||||
|
return `${Edge.getCombinedKey(this)}("${this.getHtml()}")`;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayEdgeNode() {
|
||||||
|
if (this.options.hide) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.graph.flowchart?.log(this.getFlowchartNode());
|
||||||
|
}
|
||||||
|
|
||||||
|
displayEdge() {
|
||||||
|
if (this.options.hide) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.graph.flowchart?.log(`${this.from.id} --- ${this.getFlowchartNode()} --> ${this.to.id}`);
|
||||||
|
this.graph.flowchart?.log(`class ${Edge.getCombinedKey(this)} edge`);
|
||||||
|
if (this.graph.editable && !this.installedClickCallback) {
|
||||||
|
this.graph.flowchart?.log(`click ${Edge.getCombinedKey(this)} WDGHandler${this.graph.index} \
|
||||||
|
"Edit Edge ${this.from.id} -> ${this.to.id}"`);
|
||||||
|
this.installedClickCallback = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static prepareEditorDocument(graph, doc, from, to) {
|
||||||
|
const form = doc.form({ name: 'editorForm' }).lastElement;
|
||||||
|
doc.remark('<h3>Edit Edge</h3>', { parentEl: form.el });
|
||||||
|
form
|
||||||
|
.textField({
|
||||||
|
id: 'from', name: 'from', defaultValue: from, disabled: true,
|
||||||
|
})
|
||||||
|
.textField({
|
||||||
|
id: 'to', name: 'to', defaultValue: to, disabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.remark('<h4>Edge Types</h4>', { parentEl: form.el });
|
||||||
|
const subFormArray = form.subFormArray({ id: 'edges', name: 'edges' }).lastItem;
|
||||||
|
|
||||||
|
const addEdgeForm = (edge) => {
|
||||||
|
const { subForm } = form.subForm({ name: 'subform', subFormArray }).lastItem;
|
||||||
|
subForm.textField({
|
||||||
|
id: 'type', name: 'type', defaultValue: edge.type, required: true,
|
||||||
|
})
|
||||||
|
.textField({
|
||||||
|
id: 'weight', name: 'weight', defaultValue: edge.weight, required: true,
|
||||||
|
})
|
||||||
|
.button({
|
||||||
|
id: 'remove',
|
||||||
|
name: 'Remove Edge Type',
|
||||||
|
cb: () => subFormArray.remove(subForm),
|
||||||
|
});
|
||||||
|
doc.remark('<br>', { parentEl: subForm.el });
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const edge of graph.getEdges(null, from, to)) {
|
||||||
|
addEdgeForm(edge);
|
||||||
|
}
|
||||||
|
|
||||||
|
form.button({
|
||||||
|
id: 'add',
|
||||||
|
name: 'Add Edge Type',
|
||||||
|
cb: () => addEdgeForm(new Edge(graph, null, graph.getVertex(from), graph.getVertex(to))),
|
||||||
|
})
|
||||||
|
.button({
|
||||||
|
id: 'save',
|
||||||
|
name: 'Save',
|
||||||
|
type: 'submit',
|
||||||
|
cb: ({ form: { value: { edges } } }) => {
|
||||||
|
// Do validation
|
||||||
|
for (const { type, weight } of edges) {
|
||||||
|
if (type === null || weight === null) {
|
||||||
|
graph.errorDoc.remark('<pre>type and weight are required</pre>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle additions and updates
|
||||||
|
for (const { type, weight } of edges) {
|
||||||
|
graph.setEdgeWeight(type, from, to, weight);
|
||||||
|
}
|
||||||
|
// Handle removals
|
||||||
|
for (const edge of graph.getEdges(null, from, to)) {
|
||||||
|
if (!edges.find(({ type }) => type === edge.type)) {
|
||||||
|
graph.deleteEdge(edge.type, from, to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
graph.redraw();
|
||||||
|
graph.errorDoc.clear();
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.button({
|
||||||
|
id: 'cancel',
|
||||||
|
name: 'Cancel',
|
||||||
|
cb: () => graph.resetEditorDocument(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
/**
|
||||||
|
* Note: Copied from openzepplin-contracts/contracts/token/ERC20/ERC20.sol
|
||||||
|
* As of commit d59306b: Improve ERC20.decimals documentation (#3933)
|
||||||
|
* on 2023-02-02
|
||||||
|
* by Ladd Hoffman <laddhoffman@gmail.com>
|
||||||
|
*
|
||||||
|
* @dev Implementation of the {IERC20} interface.
|
||||||
|
*
|
||||||
|
* This implementation is agnostic to the way tokens are created. This means
|
||||||
|
* that a supply mechanism has to be added in a derived contract using {_mint}.
|
||||||
|
* For a generic mechanism see {ERC20PresetMinterPauser}.
|
||||||
|
*
|
||||||
|
* TIP: For a detailed writeup see our guide
|
||||||
|
* https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How
|
||||||
|
* to implement supply mechanisms].
|
||||||
|
*
|
||||||
|
* The default value of {decimals} is 18. To change this, you should override
|
||||||
|
* this function so it returns a different value.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* This Javascript implementation is incomplete. It lacks the following:
|
||||||
|
* - allowance
|
||||||
|
* - transferFrom
|
||||||
|
* - approve
|
||||||
|
* - increaseAllowance
|
||||||
|
* - decreaseAllowance
|
||||||
|
* - _beforeTokenTransfer
|
||||||
|
* - _afterTokenTransfer
|
||||||
|
*/
|
||||||
|
export class ERC20 {
|
||||||
|
/**
|
||||||
|
* @dev Sets the values for {name} and {symbol}.
|
||||||
|
*
|
||||||
|
* All two of these values are immutable: they can only be set once during
|
||||||
|
* construction.
|
||||||
|
* @param {string} name
|
||||||
|
* @param {string} symbol
|
||||||
|
*/
|
||||||
|
constructor(name, symbol) {
|
||||||
|
this.name = name;
|
||||||
|
this.symbol = symbol;
|
||||||
|
this.totalSupply = 0;
|
||||||
|
this.balances = new Map(); // <address, number>
|
||||||
|
this.allowances = new Map(); // <address, number>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Returns the number of decimals used to get its user representation.
|
||||||
|
* For example, if `decimals` equals `2`, a balance of `505` tokens should
|
||||||
|
* be displayed to a user as `5.05` (`505 / 10 ** 2`).
|
||||||
|
*
|
||||||
|
* Tokens usually opt for a value of 18, imitating the relationship between
|
||||||
|
* Ether and Wei. This is the default value returned by this function, unless
|
||||||
|
* it's overridden.
|
||||||
|
*
|
||||||
|
* NOTE: This information is only used for _display_ purposes: it in
|
||||||
|
* no way affects any of the arithmetic of the contract, including
|
||||||
|
* {IERC20-balanceOf} and {IERC20-transfer}.
|
||||||
|
*/
|
||||||
|
static decimals() {
|
||||||
|
return 18;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev See {IERC20-balanceOf}.
|
||||||
|
*/
|
||||||
|
balanceOf(account) {
|
||||||
|
return this.balances.get(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev See {IERC20-transfer}.
|
||||||
|
*
|
||||||
|
* Emits an {Approval} event indicating the updated allowance. This is not
|
||||||
|
* required by the EIP. See the note at the beginning of {ERC20}.
|
||||||
|
*
|
||||||
|
* NOTE: Does not update the allowance if the current allowance
|
||||||
|
* is the maximum `uint256`.
|
||||||
|
*
|
||||||
|
* Requirements:
|
||||||
|
*
|
||||||
|
* - `from` and `to` cannot be the zero address.
|
||||||
|
* - `from` must have a balance of at least `amount`.
|
||||||
|
* - the caller must have allowance for ``from``'s tokens of at least
|
||||||
|
* `amount`.
|
||||||
|
*/
|
||||||
|
transfer(from, to, amount) {
|
||||||
|
if (!from) throw new Error('ERC20: transfer from the zero address');
|
||||||
|
if (!to) throw new Error('ERC20: transfer to the zero address');
|
||||||
|
|
||||||
|
// _beforeTokenTransfer(from, to, amount);
|
||||||
|
|
||||||
|
const fromBalance = this.balances.get(from);
|
||||||
|
if (fromBalance < amount) throw new Error('ERC20: transfer amount exceeds balance');
|
||||||
|
this.balances.set(from, fromBalance - amount);
|
||||||
|
// Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
|
||||||
|
// decrementing then incrementing.
|
||||||
|
this.balances.set(to, this.balances.get(to) + amount);
|
||||||
|
|
||||||
|
// emit Transfer(from, to, amount);
|
||||||
|
|
||||||
|
// _afterTokenTransfer(from, to, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dev Creates `amount` tokens and assigns them to `account`, increasing
|
||||||
|
* the total supply.
|
||||||
|
*
|
||||||
|
* Emits a {Transfer} event with `from` set to the zero address.
|
||||||
|
*
|
||||||
|
* Requirements:
|
||||||
|
*
|
||||||
|
* - `account` cannot be the zero address.
|
||||||
|
*/
|
||||||
|
mint(account, amount) {
|
||||||
|
if (!account) throw new Error('ERC20: mint to the zero address');
|
||||||
|
|
||||||
|
// _beforeTokenTransfer(address(0), account, amount);
|
||||||
|
|
||||||
|
this.totalSupply += amount;
|
||||||
|
// Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
|
||||||
|
this.balances.set(account, this.balances.get(account) + amount);
|
||||||
|
// emit Transfer(address(0), account, amount);
|
||||||
|
|
||||||
|
// _afterTokenTransfer(address(0), account, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Destroys `amount` tokens from `account`, reducing the
|
||||||
|
* total supply.
|
||||||
|
*
|
||||||
|
* Emits a {Transfer} event with `to` set to the zero address.
|
||||||
|
*
|
||||||
|
* Requirements:
|
||||||
|
*
|
||||||
|
* - `account` cannot be the zero address.
|
||||||
|
* - `account` must have at least `amount` tokens.
|
||||||
|
*/
|
||||||
|
burn(account, amount) {
|
||||||
|
if (!account) throw new Error('ERC20: burn from the zero address');
|
||||||
|
|
||||||
|
// _beforeTokenTransfer(account, address(0), amount);
|
||||||
|
|
||||||
|
const accountBalance = this.balances.get(account);
|
||||||
|
if (accountBalance < amount) throw new Error('ERC20: burn amount exceeds balance');
|
||||||
|
this.balances.set(account, accountBalance - amount);
|
||||||
|
// Overflow not possible: amount <= accountBalance <= totalSupply.
|
||||||
|
this.totalSupply -= amount;
|
||||||
|
// emit Transfer(address(0), account, amount);
|
||||||
|
|
||||||
|
// _afterTokenTransfer(address(0), account, amount);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
console.log('ERC721.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) {
|
||||||
|
console.log('ERC721.transfer', { from, to, tokenId });
|
||||||
|
const owner = this.owners.get(tokenId);
|
||||||
|
if (owner !== from) {
|
||||||
|
throw new Error(`ERC721: transfer from incorrect owner ${from}; should be ${owner}`);
|
||||||
|
}
|
||||||
|
this.incrementBalance(from, -1);
|
||||||
|
this.incrementBalance(to, 1);
|
||||||
|
this.owners.set(tokenId, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Enable or disable approval for a third party ("operator") to manage
|
||||||
|
/// all of `msg.sender`'s assets
|
||||||
|
/// @dev Emits the ApprovalForAll event. The contract MUST allow
|
||||||
|
/// multiple operators per owner.
|
||||||
|
/// @param _operator Address to add to the set of authorized operators
|
||||||
|
/// @param _approved True if the operator is approved, false to revoke approval
|
||||||
|
// setApprovalForAll(_operator, _approved) {}
|
||||||
|
|
||||||
|
/// @notice Get the approved address for a single NFT
|
||||||
|
/// @dev Throws if `_tokenId` is not a valid NFT.
|
||||||
|
/// @param _tokenId The NFT to find the approved address for
|
||||||
|
/// @return The approved address for this NFT, or the zero address if there is none
|
||||||
|
// getApproved(_tokenId) {}
|
||||||
|
|
||||||
|
/// @notice Query if an address is an authorized operator for another address
|
||||||
|
/// @param _owner The address that owns the NFTs
|
||||||
|
/// @param _operator The address that acts on behalf of the owner
|
||||||
|
/// @return True if `_operator` is an approved operator for `_owner`, false otherwise
|
||||||
|
// isApprovedForAll(_owner, _operator) {}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
class Author {
|
||||||
|
constructor(publicKey, weight) {
|
||||||
|
this.publicKey = publicKey;
|
||||||
|
this.weight = weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
publicKey: this.publicKey,
|
||||||
|
weight: this.weight,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJSON({ publicKey, weight }) {
|
||||||
|
return new Author(publicKey, weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.authors = [];
|
||||||
|
this.citations = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
addAuthor(authorPublicKey, weight) {
|
||||||
|
const author = new Author(authorPublicKey, weight);
|
||||||
|
this.authors.push(author);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
authors: this.authors.map((author) => author.toJSON()),
|
||||||
|
citations: this.citations.map((citation) => citation.toJSON()),
|
||||||
|
...(this.id ? { id: this.id } : {}),
|
||||||
|
title: this.title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJSON({
|
||||||
|
id, content, authors, citations, title,
|
||||||
|
}) {
|
||||||
|
const post = new PostContent(content);
|
||||||
|
post.authors = authors.map((author) => Author.fromJSON(author));
|
||||||
|
post.citations = citations.map((citation) => Citation.fromJSON(citation));
|
||||||
|
post.id = id;
|
||||||
|
post.title = title;
|
||||||
|
return post;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
export class Stake {
|
||||||
|
constructor({
|
||||||
|
tokenId, position, amount, lockingTime,
|
||||||
|
}) {
|
||||||
|
this.tokenId = tokenId;
|
||||||
|
this.position = position;
|
||||||
|
this.amount = amount;
|
||||||
|
this.lockingTime = lockingTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStakeValue({ lockingTimeExponent } = {}) {
|
||||||
|
return this.amount * this.lockingTime ** lockingTimeExponent;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,159 @@
|
||||||
|
import { displayNumber } from '../../util/helpers.js';
|
||||||
|
|
||||||
|
import { Edge } from './edge.js';
|
||||||
|
|
||||||
|
export class Vertex {
|
||||||
|
constructor(graph, type, id, data, options = {}) {
|
||||||
|
this.graph = graph;
|
||||||
|
this.type = type;
|
||||||
|
this.id = id;
|
||||||
|
this.data = data;
|
||||||
|
this.label = options.label ?? this.id;
|
||||||
|
this.options = options;
|
||||||
|
this.edges = {
|
||||||
|
from: [],
|
||||||
|
to: [],
|
||||||
|
};
|
||||||
|
this.installedClickCallback = false;
|
||||||
|
this.properties = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.installedClickCallback = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEdges(type, away) {
|
||||||
|
return this.edges[away ? 'from' : 'to'].filter(
|
||||||
|
(edge) => edge.type === type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setProperty(key, value) {
|
||||||
|
this.properties.set(key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayVertex() {
|
||||||
|
if (this.options.hide) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
html += `${this.label}`;
|
||||||
|
html += '<table>';
|
||||||
|
for (const [key, value] of this.properties.entries()) {
|
||||||
|
const displayValue = typeof value === 'number' ? displayNumber(value) : value;
|
||||||
|
html += `<tr><td>${key}</td><td>${displayValue}</td></tr>`;
|
||||||
|
}
|
||||||
|
html += '</table>';
|
||||||
|
if (this.id !== this.label) {
|
||||||
|
html += `<span class=small>${this.id}</span><br>`;
|
||||||
|
}
|
||||||
|
html = html.replaceAll(/\n\s*/g, '');
|
||||||
|
this.graph.flowchart?.log(`${this.id}["${html}"]`);
|
||||||
|
|
||||||
|
if (this.graph.editable && !this.installedClickCallback) {
|
||||||
|
this.graph.flowchart?.log(`click ${this.id} WDGHandler${this.graph.index} "Edit Vertex ${this.id}"`);
|
||||||
|
this.installedClickCallback = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static prepareEditorDocument(graph, doc, vertexId) {
|
||||||
|
doc.clear();
|
||||||
|
const vertex = vertexId ? graph.getVertex(vertexId) : undefined;
|
||||||
|
const form = doc.form().lastElement;
|
||||||
|
doc.remark(`<h3>${vertex ? 'Edit' : 'Add'} Vertex</h3>`, { parentEl: form.el });
|
||||||
|
form
|
||||||
|
.textField({
|
||||||
|
id: 'id', name: 'id', defaultValue: vertex?.id,
|
||||||
|
})
|
||||||
|
.textField({ id: 'type', name: 'type', defaultValue: vertex?.type })
|
||||||
|
.textField({ id: 'label', name: 'label', defaultValue: vertex?.label });
|
||||||
|
|
||||||
|
doc.remark('<h4>Properties</h4>', { parentEl: form.el });
|
||||||
|
const subFormArray = form.subFormArray({ id: 'properties', name: 'properties' }).lastItem;
|
||||||
|
const addPropertyForm = (key, value) => {
|
||||||
|
const { subForm } = form.subForm({ name: 'subform', subFormArray }).lastItem;
|
||||||
|
subForm.textField({ id: 'key', name: 'key', defaultValue: key })
|
||||||
|
.textField({ id: 'value', name: 'value', defaultValue: value })
|
||||||
|
.button({
|
||||||
|
id: 'remove',
|
||||||
|
name: 'Remove Property',
|
||||||
|
cb: () => subFormArray.remove(subForm),
|
||||||
|
});
|
||||||
|
doc.remark('<br>', { parentEl: subForm.el });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (vertex) {
|
||||||
|
for (const [key, value] of vertex.properties.entries()) {
|
||||||
|
addPropertyForm(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form.button({
|
||||||
|
id: 'add',
|
||||||
|
name: 'Add Property',
|
||||||
|
cb: () => addPropertyForm('', ''),
|
||||||
|
});
|
||||||
|
|
||||||
|
form.button({
|
||||||
|
id: 'save',
|
||||||
|
name: 'Save',
|
||||||
|
type: 'submit',
|
||||||
|
cb: ({ form: { value: formValue } }) => {
|
||||||
|
let fullRedraw = false;
|
||||||
|
if (vertex && formValue.id !== vertex.id) {
|
||||||
|
fullRedraw = true;
|
||||||
|
}
|
||||||
|
// TODO: preserve data types of properties
|
||||||
|
formValue.properties = new Map(formValue.properties.map(({ key, value }) => [key, value]));
|
||||||
|
if (vertex) {
|
||||||
|
Object.assign(vertex, formValue);
|
||||||
|
vertex.displayVertex();
|
||||||
|
} else {
|
||||||
|
const newVertex = graph.addVertex(formValue.type, formValue.id, null, formValue.label);
|
||||||
|
Object.assign(newVertex, formValue);
|
||||||
|
doc.clear();
|
||||||
|
Vertex.prepareEditorDocument(graph, doc, newVertex.id);
|
||||||
|
}
|
||||||
|
if (fullRedraw) {
|
||||||
|
graph.redraw();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (vertex) {
|
||||||
|
form.button({
|
||||||
|
id: 'delete',
|
||||||
|
name: 'Delete Vertex',
|
||||||
|
cb: () => {
|
||||||
|
graph.deleteVertex(vertex.id);
|
||||||
|
graph.redraw();
|
||||||
|
graph.resetEditorDocument();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.remark('<h3>New Edge</h3>', { parentEl: form.el });
|
||||||
|
const { subForm } = form.subForm({ name: 'newEdge' }).lastItem;
|
||||||
|
subForm.textField({ name: 'to' });
|
||||||
|
subForm.textField({ name: 'type' });
|
||||||
|
subForm.textField({ name: 'weight' });
|
||||||
|
subForm.button({
|
||||||
|
name: 'Save',
|
||||||
|
cb: ({ form: { value: { to, type, weight } } }) => {
|
||||||
|
graph.addEdge(type, vertex, to, weight, null);
|
||||||
|
doc.clear();
|
||||||
|
Edge.prepareEditorDocument(graph, doc, vertex.id, to);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
form.button({
|
||||||
|
id: 'cancel',
|
||||||
|
name: 'Cancel',
|
||||||
|
cb: () => graph.resetEditorDocument(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { Action } from '../display/action.js';
|
||||||
|
|
||||||
|
class ContractRecord {
|
||||||
|
constructor(id, instance) {
|
||||||
|
this.id = id;
|
||||||
|
this.instance = instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VMHandle {
|
||||||
|
constructor(vm, sender) {
|
||||||
|
this.vm = vm;
|
||||||
|
this.sender = sender;
|
||||||
|
this.actions = {
|
||||||
|
call: new Action('call', vm.scene),
|
||||||
|
return: new Action('return', vm.scene),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} id Contract ID
|
||||||
|
* @param {string} method
|
||||||
|
*/
|
||||||
|
async callContract(id, method, ...args) {
|
||||||
|
const instance = this.vm.getContractInstance(id);
|
||||||
|
const fn = instance[method];
|
||||||
|
if (!fn) throw new Error(`Contract ${id} method ${method} not found!`);
|
||||||
|
await this.actions.call.log(this.sender, instance, method);
|
||||||
|
const result = await fn.call(instance, this.sender, ...args);
|
||||||
|
await this.actions.return.log(instance, this.sender, undefined, undefined, '-->>');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VM {
|
||||||
|
constructor(scene) {
|
||||||
|
this.scene = scene;
|
||||||
|
this.contracts = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @param {class} ContractClass
|
||||||
|
* @param {any[]} ...args Passed to contractClass constructor after `vm`
|
||||||
|
*/
|
||||||
|
addContract(id, ContractClass, ...args) {
|
||||||
|
const instance = new ContractClass(this, ...args);
|
||||||
|
const contract = new ContractRecord(id, instance);
|
||||||
|
this.contracts.set(id, contract);
|
||||||
|
}
|
||||||
|
|
||||||
|
getHandle(sender) {
|
||||||
|
return new VMHandle(this, sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
*/
|
||||||
|
getContract(id) {
|
||||||
|
return this.contracts.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
*/
|
||||||
|
getContractInstance(id) {
|
||||||
|
return this.getContract(id)?.instance;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
export class Voter {
|
||||||
|
constructor(reputationPublicKey) {
|
||||||
|
this.reputationPublicKey = reputationPublicKey;
|
||||||
|
this.voteHistory = [];
|
||||||
|
this.dateLastVote = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
addVoteRecord(stake) {
|
||||||
|
this.voteHistory.push(stake);
|
||||||
|
if (!this.dateLastVote || stake.dateStart > this.dateLastVote) {
|
||||||
|
this.dateLastVote = stake.dateStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,231 @@
|
||||||
|
import { Vertex } from './vertex.js';
|
||||||
|
import { Edge } from './edge.js';
|
||||||
|
import { Document } from '../display/document.js';
|
||||||
|
|
||||||
|
const allGraphs = [];
|
||||||
|
|
||||||
|
const makeWDGHandler = (graphIndex) => (vertexId) => {
|
||||||
|
const graph = allGraphs[graphIndex];
|
||||||
|
// We want a document for editing this node, which may be a vertex or an edge
|
||||||
|
const { editorDoc } = graph;
|
||||||
|
editorDoc.clear();
|
||||||
|
if (vertexId.startsWith('edge:')) {
|
||||||
|
const [, from, to] = vertexId.split(':');
|
||||||
|
Edge.prepareEditorDocument(graph, editorDoc, from, to);
|
||||||
|
} else {
|
||||||
|
Vertex.prepareEditorDocument(graph, editorDoc, vertexId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export class WeightedDirectedGraph {
|
||||||
|
constructor(scene, options = {}) {
|
||||||
|
this.scene = scene;
|
||||||
|
this.vertices = new Map();
|
||||||
|
this.edgeTypes = new Map();
|
||||||
|
this.nextVertexId = 0;
|
||||||
|
this.flowchart = scene?.flowchart;
|
||||||
|
this.editable = options.editable;
|
||||||
|
|
||||||
|
// Mermaid supports a click callback, but we can't customize arguments; we just get the vertex ID.
|
||||||
|
// In order to provide the appropriate graph context for each callback, we create a separate callback
|
||||||
|
// function for each graph.
|
||||||
|
this.index = allGraphs.length;
|
||||||
|
allGraphs.push(this);
|
||||||
|
window[`WDGHandler${this.index}`] = makeWDGHandler(this.index);
|
||||||
|
|
||||||
|
// TODO: Populate history
|
||||||
|
this.history = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
getHistory() {
|
||||||
|
// record operations that modify the graph
|
||||||
|
return this.history;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
vertices: Array.from(this.vertices.values()),
|
||||||
|
edgeTypes: Array.from(this.edgeTypes.keys()),
|
||||||
|
edges: Array.from(this.edgeTypes.values()).flatMap((edges) => Array.from(edges.values())),
|
||||||
|
history: this.getHistory(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
redraw() {
|
||||||
|
// Call .reset() on all vertices and edges
|
||||||
|
for (const vertex of this.vertices.values()) {
|
||||||
|
vertex.reset();
|
||||||
|
}
|
||||||
|
for (const edges of this.edgeTypes.values()) {
|
||||||
|
for (const edge of edges.values()) {
|
||||||
|
edge.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the target div
|
||||||
|
this.flowchart?.reset();
|
||||||
|
this.flowchart?.init();
|
||||||
|
|
||||||
|
// Draw all vertices and edges
|
||||||
|
for (const vertex of this.vertices.values()) {
|
||||||
|
vertex.displayVertex();
|
||||||
|
}
|
||||||
|
// Let's flatmap and dedupe by [from, to] since each edge
|
||||||
|
// renders all comorphic edges as well.
|
||||||
|
const edgesFrom = new Map(); // edgeMap[from][to] = edge
|
||||||
|
for (const edges of this.edgeTypes.values()) {
|
||||||
|
for (const edge of edges.values()) {
|
||||||
|
const edgesTo = edgesFrom.get(edge.from) || new Map();
|
||||||
|
edgesTo.set(edge.to, edge);
|
||||||
|
edgesFrom.set(edge.from, edgesTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const edgesTo of edgesFrom.values()) {
|
||||||
|
for (const edge of edgesTo.values()) {
|
||||||
|
edge.displayEdge();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure rerender
|
||||||
|
this.flowchart?.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
withFlowchart() {
|
||||||
|
this.scene?.withSectionFlowchart();
|
||||||
|
this.flowchart = this.scene?.lastFlowchart;
|
||||||
|
if (this.editable) {
|
||||||
|
this.editorDoc = new Document('WDGControls', this.flowchart.box.el);
|
||||||
|
this.resetEditorDocument();
|
||||||
|
}
|
||||||
|
this.errorDoc = new Document('WDGErrors', this.flowchart.box.el);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetEditorDocument() {
|
||||||
|
this.editorDoc.clear();
|
||||||
|
Vertex.prepareEditorDocument(this, this.editorDoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
addVertex(type, id, data, label, options) {
|
||||||
|
// Supports simple case of auto-incremented numeric ids
|
||||||
|
if (typeof id === 'object') {
|
||||||
|
data = id;
|
||||||
|
id = this.nextVertexId++;
|
||||||
|
}
|
||||||
|
id = (typeof id === 'number') ? id.toString(10) : id;
|
||||||
|
if (this.vertices.has(id)) {
|
||||||
|
throw new Error(`Vertex already exists with id: ${id}`);
|
||||||
|
}
|
||||||
|
const vertex = new Vertex(this, type, id, data, { ...options, label });
|
||||||
|
this.vertices.set(id, vertex);
|
||||||
|
vertex.displayVertex();
|
||||||
|
return vertex;
|
||||||
|
}
|
||||||
|
|
||||||
|
getVertex(id) {
|
||||||
|
id = (typeof id === 'number') ? id.toString(10) : id;
|
||||||
|
return this.vertices.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
getVertexData(id) {
|
||||||
|
return this.getVertex(id)?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
getVerticesData() {
|
||||||
|
return Array.from(this.vertices.values()).map(({ data }) => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
getEdge(type, 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.edgeTypes.get(type);
|
||||||
|
const edgeKey = Edge.getKey({ from, to, type });
|
||||||
|
return edges?.get(edgeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
getEdgeWeight(type, from, to) {
|
||||||
|
return this.getEdge(type, from, to)?.weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEdgeWeight(type, from, to, weight, data, options) {
|
||||||
|
from = from instanceof Vertex ? from : this.getVertex(from);
|
||||||
|
to = to instanceof Vertex ? to : this.getVertex(to);
|
||||||
|
const edge = new Edge(this, type, from, to, weight, data, options);
|
||||||
|
let edges = this.edgeTypes.get(type);
|
||||||
|
if (!edges) {
|
||||||
|
edges = new Map();
|
||||||
|
this.edgeTypes.set(type, edges);
|
||||||
|
}
|
||||||
|
const edgeKey = Edge.getKey(edge);
|
||||||
|
edges.set(edgeKey, edge);
|
||||||
|
edge.displayEdgeNode();
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEdge(type, from, to, weight, data, options) {
|
||||||
|
from = from instanceof Vertex ? from : this.getVertex(from);
|
||||||
|
to = to instanceof Vertex ? to : this.getVertex(to);
|
||||||
|
const existingEdges = this.getEdges(type, from, to);
|
||||||
|
if (this.getEdge(type, from, to)) {
|
||||||
|
throw new Error(`Edge ${type} from ${from.id} to ${to.id} already exists`);
|
||||||
|
}
|
||||||
|
const edge = this.setEdgeWeight(type, from, to, weight, data, options);
|
||||||
|
from.edges.from.push(edge);
|
||||||
|
to.edges.to.push(edge);
|
||||||
|
if (existingEdges.length) {
|
||||||
|
edge.displayEdgeNode();
|
||||||
|
} else {
|
||||||
|
edge.displayEdge();
|
||||||
|
}
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEdges(type, from, to) {
|
||||||
|
from = from instanceof Vertex ? from : this.getVertex(from);
|
||||||
|
to = to instanceof Vertex ? to : this.getVertex(to);
|
||||||
|
const edgeTypes = type ? [type] : Array.from(this.edgeTypes.keys());
|
||||||
|
return edgeTypes.flatMap((edgeType) => {
|
||||||
|
const edges = this.edgeTypes.get(edgeType);
|
||||||
|
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(type) {
|
||||||
|
if (!type) {
|
||||||
|
return this.vertices.size;
|
||||||
|
}
|
||||||
|
return Array.from(this.vertices.values()).filter((vertex) => vertex.type === type).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteEdge(type, from, to) {
|
||||||
|
from = from instanceof Vertex ? from : this.getVertex(from);
|
||||||
|
to = to instanceof Vertex ? to : this.getVertex(to);
|
||||||
|
const edges = this.edgeTypes.get(type);
|
||||||
|
const edgeKey = Edge.getKey({ type, from, to });
|
||||||
|
if (!edges) return;
|
||||||
|
const edge = edges.get(edgeKey);
|
||||||
|
if (!edge) return;
|
||||||
|
to.edges.from.forEach((x, i) => (x === edge) && to.edges.from.splice(i, 1));
|
||||||
|
from.edges.to.forEach((x, i) => (x === edge) && from.edges.to.splice(i, 1));
|
||||||
|
edges.delete(edgeKey);
|
||||||
|
if (edges.size === 0) {
|
||||||
|
this.edgeTypes.delete(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteVertex(id) {
|
||||||
|
const vertex = this.getVertex(id);
|
||||||
|
for (const { type, from, to } of [...vertex.edges.to, ...vertex.edges.from]) {
|
||||||
|
this.deleteEdge(type, from, to);
|
||||||
|
}
|
||||||
|
this.vertices.delete(id);
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
|
@ -0,0 +1,94 @@
|
||||||
|
body {
|
||||||
|
background-color: #09343f;
|
||||||
|
color: #b6b6b6;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12pt;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
.dim {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
.padded {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.top-rail {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
.scene-controls {
|
||||||
|
position: relative;
|
||||||
|
left: 150px;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
width: 800px;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
background-color: #0c2025;
|
||||||
|
}
|
||||||
|
.edge > rect {
|
||||||
|
fill: #216262 !important;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
margin: 5px;
|
||||||
|
margin-top: 1em;
|
||||||
|
background-color: #c6f4ff;
|
||||||
|
border-color: #b6b6b6;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
button:disabled {
|
||||||
|
background-color: #2a535e;
|
||||||
|
color: #919191;
|
||||||
|
}
|
||||||
|
label > input {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
font-family: monospace;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: smaller;
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
label > div {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
min-width: 20em;
|
||||||
|
}
|
||||||
|
span.small {
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>DGF Tests</title>
|
||||||
|
<link type="text/css" rel="stylesheet" href="./index.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Decentralized Governance Framework</h1>
|
||||||
|
<p>
|
||||||
|
We are building a system to enable experts to collaborate and self-govern in accordance with their values.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For more information please see the <a href="https://daogovernanceframework.com/wiki/DAO_Governance_Framework">DGF
|
||||||
|
Wiki</a>.
|
||||||
|
</p>
|
||||||
|
<h2>Javascript Prototype: Example Scenarios</h2>
|
||||||
|
<p>
|
||||||
|
Below are example scenarios with various assertions covering features of our reputation system.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The code for this site is available in <a
|
||||||
|
href="https://gitlab.com/dao-governance-framework/science-publishing-dao/-/tree/main/forum-network/src">GitLab</a>.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="./tests/validation-pool.test.html">Validation Pool</a></li>
|
||||||
|
<li><a href="./tests/availability.test.html">Availability + Business</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<h3>Forum</h3>
|
||||||
|
<ol>
|
||||||
|
<li><a href="./tests/forum1.test.html">Negative citation of a negative citation</a></li>
|
||||||
|
<li><a href="./tests/forum2.test.html">Negative citation of a weaker negative citation</a></li>
|
||||||
|
<li><a href="./tests/forum3.test.html">Redistribute power</a></li>
|
||||||
|
<li><a href="./tests/forum4.test.html">Redistribute power through subsequent support</a></li>
|
||||||
|
<li><a href="./tests/forum5.test.html">Destroy a post after it has received positive citations</a></li>
|
||||||
|
<li><a href="./tests/forum6.test.html">Initially zero-valued posts later receive citations</a></li>
|
||||||
|
<li><a href="./tests/forum7.test.html">Negatively cite a zero-valued post</a></li>
|
||||||
|
<li><a href="./tests/forum8.test.html">Incinerate reputation</a></li>
|
||||||
|
<li><a href="./tests/forum9.test.html">Use incineration to achieve more balanced reweighting</a></li>
|
||||||
|
<li><a href="./tests/forum10.test.html">Post with multiple authors</a></li>
|
||||||
|
<li><a href="./tests/forum11.test.html">Multiple posts with overlapping authors</a></li>
|
||||||
|
</ol>
|
||||||
|
</ul>
|
||||||
|
<ul>
|
||||||
|
<h3>Client</h3>
|
||||||
|
<ol>
|
||||||
|
<li><a href="./tests/client1.test.html">Expert can run a client</a></li>
|
||||||
|
</ol>
|
||||||
|
</ul>
|
||||||
|
<ul>
|
||||||
|
<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/wdg.test.html">Weighted Directed Graph</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>
|
||||||
|
<li><a href="./tests/input.test.html">Input</a></li>
|
||||||
|
<li><a href="./tests/document.test.html">Document</a></li>
|
||||||
|
</ul>
|
||||||
|
<ul>
|
||||||
|
<h4><a href="./tests/all.test.html">All</a></h4>
|
||||||
|
</ul>
|
||||||
|
</body>
|
|
@ -0,0 +1,47 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>All Tests</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>
|
||||||
|
window.disableSceneControls = true;
|
||||||
|
</script>
|
||||||
|
<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/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/wdg.test.js"></script>
|
||||||
|
<script type="module" src="./scripts/forum/forum1.test.js"></script>
|
||||||
|
<script type="module" src="./scripts/forum/forum2.test.js"></script>
|
||||||
|
<script type="module" src="./scripts/forum/forum3.test.js"></script>
|
||||||
|
<script type="module" src="./scripts/forum/forum4.test.js"></script>
|
||||||
|
<script type="module" src="./scripts/forum/forum5.test.js"></script>
|
||||||
|
<script type="module" src="./scripts/forum/forum6.test.js"></script>
|
||||||
|
<script type="module" src="./scripts/forum/forum7.test.js"></script>
|
||||||
|
<script type="module" src="./scripts/forum/forum8.test.js"></script>
|
||||||
|
<script type="module" src="./scripts/forum/forum9.test.js"></script>
|
||||||
|
<script type="module" src="./scripts/forum/forum10.test.js"></script>
|
||||||
|
<script type="module" src="./scripts/forum/forum11.test.js"></script>
|
||||||
|
<script type="module" src="./scripts/input.test.js"></script>
|
||||||
|
<script type="module" src="./scripts/document.test.js"></script>
|
||||||
|
<script defer class="mocha-init">
|
||||||
|
mocha.setup({
|
||||||
|
ui: 'bdd',
|
||||||
|
});
|
||||||
|
window.should = chai.should();
|
||||||
|
</script>
|
|
@ -0,0 +1,31 @@
|
||||||
|
<!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',
|
||||||
|
});
|
||||||
|
window.should = chai.should();
|
||||||
|
</script>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Forum Network</title>
|
||||||
|
<link type="text/css" rel="stylesheet" href="../index.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2><a href="../">DGF Tests</a></h2>
|
||||||
|
<div id="basic"></div>
|
||||||
|
</body>
|
||||||
|
<script type="module" src="./scripts/basic.test.js">
|
||||||
|
</script>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Forum Network</title>
|
||||||
|
<link type="text/css" rel="stylesheet" href="../index.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2><a href="../">DGF Tests</a></h2>
|
||||||
|
<div id="basic"></div>
|
||||||
|
</body>
|
||||||
|
<script type="module" src="./scripts/basic2.test.js">
|
||||||
|
</script>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<!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://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-tfLUmTr4u39/6Pykb8v/LjLaQ9u/uSgbHtZXFCtT9bOsZd1ZPZabIrwhif/YzashftTOhwwQUC0cQyrnIC1vEQ=="
|
||||||
|
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<script type="module" src="./scripts/business.test.js"></script>
|
||||||
|
<script defer class="mocha-init">
|
||||||
|
mocha.setup({
|
||||||
|
ui: 'bdd',
|
||||||
|
});
|
||||||
|
window.should = chai.should();
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Client 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/client/client1.test.js"></script>
|
||||||
|
<script defer class="mocha-init">
|
||||||
|
mocha.setup({
|
||||||
|
ui: 'bdd',
|
||||||
|
});
|
||||||
|
chai.should();
|
||||||
|
</script>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<!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',
|
||||||
|
});
|
||||||
|
window.should = chai.should();
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Document</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/document.test.js"></script>
|
||||||
|
<script defer class="mocha-init">
|
||||||
|
mocha.setup({
|
||||||
|
ui: 'bdd',
|
||||||
|
});
|
||||||
|
window.should = chai.should();
|
||||||
|
</script>
|
|
@ -0,0 +1,41 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Flowchart test</title>
|
||||||
|
<link type="text/css" rel="stylesheet" href="../index.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2><a href="../">DGF Tests</a></h2>
|
||||||
|
<div id="flowchart-test"></div>
|
||||||
|
</body>
|
||||||
|
<script type="module">
|
||||||
|
import { Box } from '../classes/display/box.js';
|
||||||
|
import { Scene } from '../classes/display/scene.js';
|
||||||
|
import { Actor } from '../classes/display/actor.js';
|
||||||
|
import { Action } from '../classes/display/action.js';
|
||||||
|
import { delay } from '../util/helpers.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.setDisplayValue('value', 1);
|
||||||
|
|
||||||
|
await scene.withFlowchart();
|
||||||
|
await scene.flowchart.log('A --> B');
|
||||||
|
|
||||||
|
await delay(DEFAULT_DELAY_INTERVAL);
|
||||||
|
action1.log(actor1, actor2);
|
||||||
|
|
||||||
|
await delay(DEFAULT_DELAY_INTERVAL);
|
||||||
|
await scene.flowchart.log('A --> C');
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!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',
|
||||||
|
});
|
||||||
|
chai.should();
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Forum test 10</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/forum10.test.js"></script>
|
||||||
|
<script defer class="mocha-init">
|
||||||
|
mocha.setup({
|
||||||
|
ui: 'bdd',
|
||||||
|
});
|
||||||
|
chai.should();
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Forum test 11</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/forum11.test.js"></script>
|
||||||
|
<script defer class="mocha-init">
|
||||||
|
mocha.setup({
|
||||||
|
ui: 'bdd',
|
||||||
|
});
|
||||||
|
chai.should();
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!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',
|
||||||
|
});
|
||||||
|
chai.should();
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!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',
|
||||||
|
});
|
||||||
|
chai.should();
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!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',
|
||||||
|
});
|
||||||
|
chai.should();
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!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',
|
||||||
|
});
|
||||||
|
chai.should();
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Forum test 6</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css" />
|
||||||
|
<link type="text/css" rel="stylesheet" href="../index.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2><a href="../">DGF Tests</a></h2>
|
||||||
|
<div id="mocha"></div>
|
||||||
|
<div id="scene"></div>
|
||||||
|
</body>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/radash/10.7.0/radash.js"
|
||||||
|
integrity="sha512-S207zKWG3iqXqe6msO7/Mr8X3DzzF4u8meFlokHjGtBPTGUhgzVo0lpcqEy0GoiMUdcoct+H+SqzoLsxXbynzg=="
|
||||||
|
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<script src="https://unpkg.com/mocha/mocha.js"></script>
|
||||||
|
<script src="https://unpkg.com/chai/chai.js"></script>
|
||||||
|
<script type="module" src="./scripts/forum/forum6.test.js"></script>
|
||||||
|
<script defer class="mocha-init">
|
||||||
|
mocha.setup({
|
||||||
|
ui: 'bdd',
|
||||||
|
});
|
||||||
|
chai.should();
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Forum test 7</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/forum7.test.js"></script>
|
||||||
|
<script defer class="mocha-init">
|
||||||
|
mocha.setup({
|
||||||
|
ui: 'bdd',
|
||||||
|
});
|
||||||
|
chai.should();
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Forum test 8</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/forum8.test.js"></script>
|
||||||
|
<script defer class="mocha-init">
|
||||||
|
mocha.setup({
|
||||||
|
ui: 'bdd',
|
||||||
|
});
|
||||||
|
chai.should();
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Forum test 9</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/forum9.test.js"></script>
|
||||||
|
<script defer class="mocha-init">
|
||||||
|
mocha.setup({
|
||||||
|
ui: 'bdd',
|
||||||
|
});
|
||||||
|
chai.should();
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Input</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/input.test.js"></script>
|
||||||
|
<script defer class="mocha-init">
|
||||||
|
mocha.setup({
|
||||||
|
ui: 'bdd',
|
||||||
|
});
|
||||||
|
window.should = chai.should();
|
||||||
|
</script>
|
|
@ -0,0 +1,31 @@
|
||||||
|
<!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',
|
||||||
|
});
|
||||||
|
chai.should();
|
||||||
|
</script>
|
||||||
|
<script src="./scripts/mocha.test.js"></script>
|
||||||
|
<script class="mocha-exec">
|
||||||
|
mocha.run();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,35 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Reputation test</title>
|
||||||
|
<link type="text/css" rel="stylesheet" href="../index.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2><a href="../">DGF Tests</a></h2>
|
||||||
|
<div id="scene"></div>
|
||||||
|
</body>
|
||||||
|
<script type="module">
|
||||||
|
import { Box } from '../classes/display/box.js';
|
||||||
|
import { Scene } from '../classes/display/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/helpers.js';
|
||||||
|
|
||||||
|
const DEFAULT_DELAY_INTERVAL = 500;
|
||||||
|
|
||||||
|
const rootElement = document.getElementById('scene');
|
||||||
|
const rootBox = new Box('rootBox', rootElement).flex();
|
||||||
|
|
||||||
|
const scene = (window.scene = new Scene('Reputation test', rootBox));
|
||||||
|
scene.withSequenceDiagram();
|
||||||
|
scene.withFlowchart();
|
||||||
|
|
||||||
|
// const pool = new ValidationPool();
|
||||||
|
// const repToken = new ReputationToken();
|
||||||
|
|
||||||
|
// const tokenMinter = new TokenHolder('TokenMinter', scene);
|
||||||
|
|
||||||
|
await delay(DEFAULT_DELAY_INTERVAL);
|
||||||
|
</script>
|
|
@ -0,0 +1,174 @@
|
||||||
|
import { Box } from '../../classes/display/box.js';
|
||||||
|
import { Scene } from '../../classes/display/scene.js';
|
||||||
|
import { Expert } from '../../classes/actors/expert.js';
|
||||||
|
import { DAO } from '../../classes/dao/dao.js';
|
||||||
|
import { Public } from '../../classes/actors/public.js';
|
||||||
|
import { PostContent } from '../../classes/supporting/post-content.js';
|
||||||
|
import { delayOrWait } from '../../classes/display/scene-controls.js';
|
||||||
|
import { mochaRun } from '../../util/helpers.js';
|
||||||
|
|
||||||
|
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();
|
||||||
|
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.setDisplayValue('total rep', () => dao.reputation.getTotal());
|
||||||
|
|
||||||
|
experts = [];
|
||||||
|
|
||||||
|
await newExpert();
|
||||||
|
await newExpert();
|
||||||
|
requestor = new Public('Public', scene);
|
||||||
|
|
||||||
|
await delayOrWait(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 delayOrWait(POOL_DURATION);
|
||||||
|
|
||||||
|
await pool1.evaluateWinningConditions();
|
||||||
|
await delayOrWait(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 delayOrWait(POOL_DURATION);
|
||||||
|
|
||||||
|
await pool2.evaluateWinningConditions();
|
||||||
|
await delayOrWait(DELAY_INTERVAL);
|
||||||
|
|
||||||
|
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(15);
|
||||||
|
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(5);
|
||||||
|
};
|
||||||
|
|
||||||
|
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', function tests() {
|
||||||
|
this.timeout(0);
|
||||||
|
|
||||||
|
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 delayOrWait(DELAY_INTERVAL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Public can submit a work request', async () => {
|
||||||
|
await requestor.submitRequest(
|
||||||
|
dao.business,
|
||||||
|
{ fee: 100 },
|
||||||
|
{ please: 'do some work' },
|
||||||
|
);
|
||||||
|
await delayOrWait(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 delayOrWait(POOL_DURATION);
|
||||||
|
|
||||||
|
// Distribute reputation awards and fees
|
||||||
|
await pool3.evaluateWinningConditions();
|
||||||
|
await delayOrWait(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);
|
||||||
|
});
|
||||||
|
|
||||||
|
mochaRun();
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { Box } from '../../classes/display/box.js';
|
||||||
|
import { Scene } from '../../classes/display/scene.js';
|
||||||
|
import { Actor } from '../../classes/display/actor.js';
|
||||||
|
import { Action } from '../../classes/display/action.js';
|
||||||
|
|
||||||
|
const rootElement = document.getElementById('basic');
|
||||||
|
const rootBox = new Box('rootBox', rootElement).flex();
|
||||||
|
|
||||||
|
function randomDelay(min, max) {
|
||||||
|
const delayMs = min + Math.random() * max;
|
||||||
|
return delayMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
(function run() {
|
||||||
|
const scene = new Scene('Scene 1', rootBox).withSequenceDiagram();
|
||||||
|
const webClientStatus = scene.addDisplayValue('WebClient Status');
|
||||||
|
const node1Status = scene.addDisplayValue('Node 1 Status');
|
||||||
|
const blockchainStatus = scene.addDisplayValue('Blockchain Status');
|
||||||
|
|
||||||
|
const webClient = new Actor('web client', scene);
|
||||||
|
const node1 = new Actor('node 1', scene);
|
||||||
|
const blockchain = new Actor('blockchain', scene);
|
||||||
|
const requestForumPage = new Action('requestForumPage', scene);
|
||||||
|
const readBlockchainData = new Action('readBlockchainData', scene);
|
||||||
|
const blockchainData = new Action('blockchainData', scene);
|
||||||
|
const forumPage = new Action('forumPage', scene);
|
||||||
|
|
||||||
|
webClientStatus.set('Initialized');
|
||||||
|
node1Status.set('Idle');
|
||||||
|
blockchainStatus.set('Idle');
|
||||||
|
|
||||||
|
node1.on(requestForumPage, (src, detail) => {
|
||||||
|
node1Status.set('Processing request');
|
||||||
|
node1.on(blockchainData, (_src, data) => {
|
||||||
|
node1Status.set('Processing response');
|
||||||
|
setTimeout(() => {
|
||||||
|
node1.send(src, forumPage, data);
|
||||||
|
node1Status.set('Idle');
|
||||||
|
}, randomDelay(500, 1000));
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
node1.send(blockchain, readBlockchainData, detail);
|
||||||
|
}, randomDelay(500, 1500));
|
||||||
|
});
|
||||||
|
|
||||||
|
blockchain.on(readBlockchainData, (src, _detail) => {
|
||||||
|
blockchainStatus.set('Processing request');
|
||||||
|
setTimeout(() => {
|
||||||
|
blockchain.send(src, blockchainData, {});
|
||||||
|
blockchainStatus.set('Idle');
|
||||||
|
}, randomDelay(500, 1500));
|
||||||
|
});
|
||||||
|
|
||||||
|
webClient.on(forumPage, (_src, _detail) => {
|
||||||
|
webClientStatus.set('Received forum page');
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
webClient.send(node1, requestForumPage);
|
||||||
|
webClientStatus.set('Requested forum page');
|
||||||
|
}, randomDelay(500, 1500));
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
webClient.send(node1, requestForumPage);
|
||||||
|
webClientStatus.set('Requested forum page');
|
||||||
|
}, randomDelay(6000, 12000));
|
||||||
|
}());
|
|
@ -0,0 +1,139 @@
|
||||||
|
import { Box } from '../../classes/display/box.js';
|
||||||
|
import { Scene } from '../../classes/display/scene.js';
|
||||||
|
import { Actor } from '../../classes/display/actor.js';
|
||||||
|
|
||||||
|
const rootElement = document.getElementById('basic');
|
||||||
|
const rootBox = new Box('rootBox', rootElement).flex();
|
||||||
|
|
||||||
|
function delay(min, max = min) {
|
||||||
|
const delayMs = min + Math.random() * (max - min);
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, delayMs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
(async function run() {
|
||||||
|
const scene = new Scene('Scene 2', rootBox).withSequenceDiagram();
|
||||||
|
|
||||||
|
const webClient = new Actor('webClient', scene);
|
||||||
|
|
||||||
|
const nodes = [];
|
||||||
|
const memories = [];
|
||||||
|
const storages = [];
|
||||||
|
|
||||||
|
async function addNode() {
|
||||||
|
const idx = nodes.length;
|
||||||
|
const node = new Actor(`node${idx}`, scene);
|
||||||
|
const memory = new Actor(`memory${idx}`, scene);
|
||||||
|
const storage = new Actor(`storage${idx}`, scene);
|
||||||
|
node.memory = memory;
|
||||||
|
node.storage = storage;
|
||||||
|
nodes.push(node);
|
||||||
|
memories.push(memory);
|
||||||
|
storages.push(storage);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPeer(node) {
|
||||||
|
const peers = nodes.filter((peer) => peer !== node);
|
||||||
|
const idx = Math.floor(Math.random() * peers.length);
|
||||||
|
return peers[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
await addNode();
|
||||||
|
await addNode();
|
||||||
|
|
||||||
|
const [
|
||||||
|
seekTruth,
|
||||||
|
considerInfo,
|
||||||
|
evaluateConfidence,
|
||||||
|
chooseResponse,
|
||||||
|
qualifiedOpinions,
|
||||||
|
requestMemoryData,
|
||||||
|
memoryData,
|
||||||
|
requestStorageData,
|
||||||
|
storageData,
|
||||||
|
] = [
|
||||||
|
'seek truth',
|
||||||
|
'consider available information',
|
||||||
|
'evaluate confidence',
|
||||||
|
'choose response',
|
||||||
|
'qualified opinions',
|
||||||
|
'request in-memory data',
|
||||||
|
'in-memory data',
|
||||||
|
'request storage data',
|
||||||
|
'storage data',
|
||||||
|
].map((name) => scene.addAction(name));
|
||||||
|
|
||||||
|
memories.forEach((memory) => {
|
||||||
|
memory.setStatus('Idle');
|
||||||
|
memory.on(requestMemoryData, async (src, _detail) => {
|
||||||
|
memory.setStatus('Retrieving data');
|
||||||
|
await delay(1000);
|
||||||
|
memory.send(src, memoryData, {});
|
||||||
|
memory.setStatus('Idle');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
storages.forEach((storage) => {
|
||||||
|
storage.setStatus('Idle');
|
||||||
|
storage.on(requestStorageData, async (src, _detail) => {
|
||||||
|
storage.setStatus('Retrieving data');
|
||||||
|
await delay(1000);
|
||||||
|
storage.send(src, storageData, {});
|
||||||
|
storage.setStatus('Idle');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
node.setStatus('Idle');
|
||||||
|
node.on(seekTruth, async (seeker, detail) => {
|
||||||
|
node.setStatus('Processing request');
|
||||||
|
|
||||||
|
node.on(chooseResponse, async (_src, _info) => {
|
||||||
|
node.setStatus('Choosing response');
|
||||||
|
await delay(1000);
|
||||||
|
node.send(seeker, qualifiedOpinions, {});
|
||||||
|
node.setStatus('Idle');
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on(evaluateConfidence, async (_src, _info) => {
|
||||||
|
node.setStatus('Evaluating confidence');
|
||||||
|
await delay(1000);
|
||||||
|
node.send(node, chooseResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on(considerInfo, async (_src, _info) => {
|
||||||
|
node.setStatus('Considering info');
|
||||||
|
await delay(1000);
|
||||||
|
node.send(node, evaluateConfidence);
|
||||||
|
});
|
||||||
|
|
||||||
|
node.on(memoryData, (_src, _data) => {
|
||||||
|
node.on(storageData, (__src, __data) => {
|
||||||
|
if (detail?.readConcern === 'single') {
|
||||||
|
node.send(node, considerInfo, {});
|
||||||
|
} else {
|
||||||
|
const peer = getPeer(node);
|
||||||
|
node.on(qualifiedOpinions, (___src, info) => {
|
||||||
|
node.send(node, considerInfo, info);
|
||||||
|
});
|
||||||
|
node.send(peer, seekTruth, { readConcern: 'single' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
node.send(node.storage, requestStorageData);
|
||||||
|
});
|
||||||
|
|
||||||
|
await delay(1000);
|
||||||
|
node.send(node.memory, requestMemoryData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
webClient.on(qualifiedOpinions, (_src, _detail) => {
|
||||||
|
webClient.setStatus('Received opinions and qualifications');
|
||||||
|
});
|
||||||
|
|
||||||
|
await delay(1000);
|
||||||
|
webClient.setStatus('Seek truth');
|
||||||
|
webClient.send(nodes[0], seekTruth);
|
||||||
|
}());
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Business } from '../../classes/dao/business.js';
|
||||||
|
import { Scene } from '../../classes/display/scene.js';
|
||||||
|
import { Box } from '../../classes/display/box.js';
|
||||||
|
import { mochaRun } from '../../util/helpers.js';
|
||||||
|
|
||||||
|
describe('Business', function tests() {
|
||||||
|
this.timeout(0);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mochaRun();
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { mochaRun } from '../../../util/helpers.js';
|
||||||
|
import { ForumTest } from '../forum.test-util.js';
|
||||||
|
import { Client } from '../../../classes/dao/client.js';
|
||||||
|
|
||||||
|
describe('Forum', function tests() {
|
||||||
|
this.timeout(0);
|
||||||
|
|
||||||
|
const forumTest = new ForumTest({ displayAuthors: false });
|
||||||
|
let client;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
await forumTest.setup();
|
||||||
|
await forumTest.newExpert();
|
||||||
|
await forumTest.newExpert();
|
||||||
|
|
||||||
|
client = new Client(forumTest.dao, forumTest.experts[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Expert can run a client', async () => {
|
||||||
|
client.should.not.be.undefined;
|
||||||
|
client.dao.should.equal(forumTest.dao);
|
||||||
|
client.expert.should.equal(forumTest.experts[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mochaRun();
|
|
@ -0,0 +1,74 @@
|
||||||
|
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, mochaRun } from '../../util/helpers.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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mochaRun();
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Box } from '../../classes/display/box.js';
|
||||||
|
// import { Document } from '../../classes/display/document.js';
|
||||||
|
import { Scene } from '../../classes/display/scene.js';
|
||||||
|
import { mochaRun } from '../../util/helpers.js';
|
||||||
|
|
||||||
|
const rootElement = document.getElementById('scene');
|
||||||
|
const rootBox = new Box('rootBox', rootElement).flex();
|
||||||
|
const scene = window.scene = new Scene('Document test', rootBox);
|
||||||
|
|
||||||
|
scene.withDocument();
|
||||||
|
|
||||||
|
describe('Document', () => {
|
||||||
|
describe('remark', () => {
|
||||||
|
it('can exist', () => {
|
||||||
|
const docFunction = (doc) => doc.remark('Hello');
|
||||||
|
scene.withDocument('Document', docFunction);
|
||||||
|
});
|
||||||
|
it.skip('can include handlebars expressions', () => { });
|
||||||
|
it.skip('updates rendered output when input changes', () => { });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mochaRun();
|
|
@ -0,0 +1,91 @@
|
||||||
|
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/supporting/post-content.js';
|
||||||
|
import { DAO } from '../../classes/dao/dao.js';
|
||||||
|
import { delayOrWait } from '../../classes/display/scene-controls.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(authors, fee, citations = []) {
|
||||||
|
const postIndex = this.posts.length;
|
||||||
|
const title = `posts[${postIndex}]`;
|
||||||
|
await this.scene.sequence.startSection();
|
||||||
|
|
||||||
|
const postContent = new PostContent().setTitle(title);
|
||||||
|
|
||||||
|
const submitter = Array.isArray(authors) ? authors[0].author : authors;
|
||||||
|
|
||||||
|
if (Array.isArray(authors)) {
|
||||||
|
for (const { author, weight } of authors) {
|
||||||
|
console.log('author', { author, weight });
|
||||||
|
postContent.addAuthor(author.reputationPublicKey, weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { postId, weight } of citations) {
|
||||||
|
postContent.addCitation(postId, weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pool, postId } = await submitter.submitPostWithFee(
|
||||||
|
postContent,
|
||||||
|
{
|
||||||
|
fee,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
duration: this.options.poolDurationMs,
|
||||||
|
tokenLossRatio: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
this.posts.push(postId);
|
||||||
|
await delayOrWait(this.options.poolDurationMs);
|
||||||
|
await pool.evaluateWinningConditions();
|
||||||
|
await this.scene.sequence.endSection();
|
||||||
|
await delayOrWait(this.options.defaultDelayMs);
|
||||||
|
return postId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async newExpert() {
|
||||||
|
// Hide by default, for simplicity of rendering first 9 forum tests
|
||||||
|
const options = {
|
||||||
|
hide: !this.options.displayAuthors,
|
||||||
|
announce: this.options.displayAuthors,
|
||||||
|
};
|
||||||
|
const index = this.experts.length;
|
||||||
|
const name = `Expert${index + 1}`;
|
||||||
|
const expert = await new Expert(this.dao, name, this.scene, options).initialize();
|
||||||
|
this.experts.push(expert);
|
||||||
|
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();
|
||||||
|
|
||||||
|
// If we're going to announce experts, announce the DAO so it appears first.
|
||||||
|
this.dao = new DAO('DAO', scene, { announce: this.options.displayAuthors });
|
||||||
|
this.forum = this.dao.forum;
|
||||||
|
this.experts = [];
|
||||||
|
this.posts = [];
|
||||||
|
|
||||||
|
await this.newExpert();
|
||||||
|
|
||||||
|
await this.dao.addComputedValue('total value', () => this.dao.reputation.getTotal());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { mochaRun } from '../../../util/helpers.js';
|
||||||
|
import { ForumTest } from '../forum.test-util.js';
|
||||||
|
|
||||||
|
describe('Forum', function tests() {
|
||||||
|
this.timeout(0);
|
||||||
|
|
||||||
|
const forumTest = new ForumTest({ displayAuthors: false });
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
await forumTest.setup();
|
||||||
|
await forumTest.newExpert();
|
||||||
|
await forumTest.newExpert();
|
||||||
|
});
|
||||||
|
|
||||||
|
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[1], 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[2], 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mochaRun();
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { mochaRun } from '../../../util/helpers.js';
|
||||||
|
import { ForumTest } from '../forum.test-util.js';
|
||||||
|
|
||||||
|
describe('Forum', function tests() {
|
||||||
|
this.timeout(0);
|
||||||
|
|
||||||
|
const forumTest = new ForumTest({ displayAuthors: true });
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
await forumTest.setup();
|
||||||
|
await forumTest.newExpert();
|
||||||
|
await forumTest.newExpert();
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Post with multiple authors', async () => {
|
||||||
|
let forum;
|
||||||
|
let experts;
|
||||||
|
let posts;
|
||||||
|
let dao;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
forum = forumTest.forum;
|
||||||
|
experts = forumTest.experts;
|
||||||
|
posts = forumTest.posts;
|
||||||
|
dao = forumTest.dao;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Post1 has three authors and reputation is distributed among them', async () => {
|
||||||
|
const authors = [
|
||||||
|
{ author: experts[0], weight: 0.5 },
|
||||||
|
{ author: experts[1], weight: 0.25 },
|
||||||
|
{ author: experts[2], weight: 0.25 },
|
||||||
|
];
|
||||||
|
await forumTest.addPost(authors, 10);
|
||||||
|
forum.getPost(posts[0]).value.should.equal(10);
|
||||||
|
|
||||||
|
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(5);
|
||||||
|
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(2.5);
|
||||||
|
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey).should.equal(2.5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mochaRun();
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { mochaRun } from '../../../util/helpers.js';
|
||||||
|
import { ForumTest } from '../forum.test-util.js';
|
||||||
|
|
||||||
|
describe('Forum', function tests() {
|
||||||
|
this.timeout(0);
|
||||||
|
|
||||||
|
const forumTest = new ForumTest({ displayAuthors: true });
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
await forumTest.setup();
|
||||||
|
await forumTest.newExpert();
|
||||||
|
await forumTest.newExpert();
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Multiple posts with overlapping authors', async () => {
|
||||||
|
let forum;
|
||||||
|
let experts;
|
||||||
|
let posts;
|
||||||
|
let dao;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
forum = forumTest.forum;
|
||||||
|
experts = forumTest.experts;
|
||||||
|
posts = forumTest.posts;
|
||||||
|
dao = forumTest.dao;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Post1 with two authors', async () => {
|
||||||
|
const authors = [
|
||||||
|
{ author: experts[0], weight: 0.5 },
|
||||||
|
{ author: experts[1], weight: 0.5 },
|
||||||
|
];
|
||||||
|
await forumTest.addPost(authors, 10);
|
||||||
|
forum.getPost(posts[0]).value.should.equal(10);
|
||||||
|
|
||||||
|
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(5);
|
||||||
|
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(5);
|
||||||
|
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey).should.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Post2 with two authors, one shared with Post1', async () => {
|
||||||
|
const authors = [
|
||||||
|
{ author: experts[1], weight: 0.5 },
|
||||||
|
{ author: experts[2], weight: 0.5 },
|
||||||
|
];
|
||||||
|
await forumTest.addPost(authors, 10);
|
||||||
|
forum.getPost(posts[0]).value.should.equal(10);
|
||||||
|
|
||||||
|
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(5);
|
||||||
|
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(10);
|
||||||
|
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey).should.equal(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mochaRun();
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue