├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── 99999_addresses.txt ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── contracts ├── Airdrop.sol ├── BalanceVerifier.sol ├── DemoToken.sol ├── Migrations.sol ├── Monoplasma.sol └── Ownable.sol ├── copy-abi-to-ui.js ├── defaultServers.json ├── demo ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── gatsby-config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── src │ ├── components │ │ ├── Button │ │ │ ├── button.module.css │ │ │ └── index.jsx │ │ ├── Clock │ │ │ └── index.jsx │ │ ├── Container │ │ │ ├── container.module.css │ │ │ └── index.jsx │ │ ├── Home │ │ │ ├── About │ │ │ │ ├── about.module.css │ │ │ │ └── index.jsx │ │ │ ├── Blocks │ │ │ │ ├── blocks.module.css │ │ │ │ └── index.jsx │ │ │ ├── Headline │ │ │ │ ├── headline.module.css │ │ │ │ └── index.jsx │ │ │ ├── Hero │ │ │ │ ├── hero.module.css │ │ │ │ └── index.jsx │ │ │ ├── Management │ │ │ │ ├── index.jsx │ │ │ │ └── management.module.css │ │ │ ├── RevenuePoolActions │ │ │ │ ├── index.jsx │ │ │ │ └── revenuePoolActions.module.css │ │ │ ├── Section │ │ │ │ └── index.jsx │ │ │ ├── Settings │ │ │ │ ├── index.jsx │ │ │ │ └── settings.module.css │ │ │ ├── Stats │ │ │ │ ├── Stat │ │ │ │ │ ├── index.jsx │ │ │ │ │ └── stat.module.css │ │ │ │ ├── index.jsx │ │ │ │ └── stats.module.css │ │ │ ├── UserActions │ │ │ │ ├── index.jsx │ │ │ │ └── userActions.module.css │ │ │ ├── home.module.css │ │ │ └── index.jsx │ │ ├── Input │ │ │ ├── index.js │ │ │ └── input.module.css │ │ ├── Layout │ │ │ ├── index.jsx │ │ │ └── layout.css │ │ ├── Notification │ │ │ ├── index.jsx │ │ │ └── notification.module.css │ │ └── Tooltip │ │ │ ├── index.jsx │ │ │ └── tooltip.module.css │ ├── containers │ │ ├── Home │ │ │ └── index.jsx │ │ └── Wallet │ │ │ └── index.jsx │ ├── contexts │ │ ├── Home │ │ │ └── index.js │ │ └── Wallet │ │ │ └── index.js │ ├── pages │ │ └── index.jsx │ ├── stylesheets │ │ └── variables.css │ └── utils │ │ ├── formatFixedDecimal.js │ │ ├── monoplasmaAbi.json │ │ ├── proxyRouter.js │ │ └── tokenAbi.json └── static │ └── favicon.ico ├── flatten ├── index.js ├── migrations └── 1_initial_migration.js ├── package-lock.json ├── package.json ├── src ├── fileStore.js ├── joinPartChannel.js ├── member.js ├── merkletree.js ├── operator.js ├── routers │ ├── admin.js │ ├── member.js │ └── revenueDemo.js ├── state.js ├── utils │ ├── checkArguments.js │ ├── deployDemoToken.js │ ├── events.js │ ├── formatDecimals.js │ ├── now.js │ ├── partitionArray.js │ └── startGanache.js ├── validator.js └── watcher.js ├── start_operator.js ├── start_validator.js ├── strings ├── BalanceVerifier.json ├── Monoplasma.json └── Ownable.json ├── test ├── .eslintrc.js ├── e2e │ └── revenue-sharing-demo.js ├── mocha │ ├── fileStore.js │ ├── joinPartChannel.js │ ├── member.js │ ├── merkletree.js │ ├── routers │ │ └── member.js │ ├── state.js │ ├── test-utils.js │ ├── utils │ │ ├── events.js │ │ ├── formatDecimals.js │ │ ├── now.js │ │ └── partitionArray.js │ ├── validator.js │ └── watcher.js ├── truffle │ ├── .eslintrc.js │ ├── BalanceVerifier.js │ ├── FailToken.json │ ├── FailToken.sol.txt │ ├── Monoplasma.js │ ├── Ownable.js │ └── increaseTime.js └── utils │ ├── await-until.js │ ├── increaseTime.js │ ├── joinPartChannel-client.js │ ├── joinPartChannel-server.js │ ├── mockChannel.js │ ├── mockStore.js │ ├── mockWeb3.js │ ├── operatorApi.js │ ├── sleep-promise.js │ └── web3Assert.js └── truffle-config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaVersion": 2017 9 | }, 10 | "rules": { 11 | "indent": [ 12 | "error", 13 | 4, 14 | { 15 | "SwitchCase": 1, 16 | "flatTernaryExpressions": true 17 | }, 18 | ], 19 | "linebreak-style": [ 20 | "error", 21 | "unix" 22 | ], 23 | "quotes": [ 24 | "error", 25 | "double" 26 | ], 27 | "semi": [ 28 | "error", 29 | "never" 30 | ], 31 | "no-console": "warn", 32 | "comma-spacing": ["error", { "before": false, "after": true }] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea/workspace.xml 3 | build 4 | static_web/airdrop/0x* 5 | test/e2e/test-store-* 6 | coverage.json 7 | coverage 8 | monoplasma.iml 9 | .idea 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10.14.0" 4 | before_install: 5 | - "npm i -g npm@6.4.1" 6 | branches: 7 | only: 8 | - master 9 | - /^v\d+\.\d+(\.\d+)?(-\S*)?$/ 10 | jobs: 11 | include: 12 | - script: 13 | - "npm run lint" 14 | - "npm run test" 15 | stage: "lint & tests" 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Revenue sharing demo web page", 9 | "type": "chrome", 10 | "request": "launch", 11 | "url": "http://localhost/index.html", 12 | "webRoot": "${workspaceFolder}/static_web" 13 | }, 14 | { 15 | "name": "Revenue sharing demo web page", 16 | "type": "chrome", 17 | "request": "launch", 18 | "file": "${workspaceFolder}/static_web/index.html" 19 | }, 20 | { 21 | "type": "node", 22 | "request": "launch", 23 | "name": "Truffle test current file", 24 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/truffle", 25 | "runtimeArgs": [ 26 | "test", 27 | "--inspect-brk", 28 | "${file}" 29 | ], 30 | "internalConsoleOptions": "openOnSessionStart" 31 | }, { 32 | "type": "node", 33 | "request": "launch", 34 | "name": "Mocha test current file", 35 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/mocha", 36 | "runtimeArgs": [ 37 | "${file}" 38 | ], 39 | "internalConsoleOptions": "openOnSessionStart" 40 | }, { 41 | "type": "node", 42 | "request": "launch", 43 | "name": "Run this file", 44 | "program": "${file}", 45 | }, { 46 | "type": "node", 47 | "request": "launch", 48 | "name": "Truffle test all", 49 | "program": "${workspaceFolder}/node_modules/.bin/truffle", 50 | "args": [ 51 | "test", 52 | "${workspaceFolder}/test/truffle/" 53 | ], 54 | "internalConsoleOptions": "openOnSessionStart" 55 | }, { 56 | "type": "node", 57 | "request": "launch", 58 | "name": "Mocha test all", 59 | "program": "${workspaceFolder}/node_modules/.bin/mocha", 60 | "args": [ 61 | "${workspaceFolder}/test/mocha/" 62 | ], 63 | "internalConsoleOptions": "openOnSessionStart" 64 | }, { 65 | "type": "node", 66 | "request": "launch", 67 | "name": "operator (Ganache)", 68 | "program": "${workspaceFolder}/start_operator.js" 69 | }, { 70 | "type": "node", 71 | "request": "launch", 72 | "name": "operator (localhost:8545 + web:8080)", 73 | "program": "${workspaceFolder}/start_operator.js", 74 | "env": { 75 | "EXTERNAL_UI_SERVER": "http://localhost:8080/", 76 | "ETHEREUM_SERVER": "ws://localhost:8545/", 77 | "ETHEREUM_PRIVATE_KEY": "0x5e98cce00cff5dea6b454889f359a4ec06b9fa6b88e9d69b86de8e1c81887da0" 78 | } 79 | }, { 80 | "type": "node", 81 | "request": "launch", 82 | "name": "operator (rinkeby infura)", 83 | "program": "${workspaceFolder}/start_operator.js", 84 | "env": { 85 | "ETHEREUM_NETWORK_ID": "4", 86 | "ETHEREUM_PRIVATE_KEY": "0x6E340F41A1C6E03E6E0A4E9805D1CEA342F6A299E7C931D6F3DA6DD34CB6E17D", 87 | "__ADDRESS_FOR_THE_ABOVE_KEY": "0xb3428050eA2448eD2E4409bE47E1a50EBac0B2d2", 88 | "TOKEN_ADDRESS": "0x0c89795f75eEA923Cd1faEea50777c1B87787A06", 89 | "CONTRACT_ADDRESS": "0xC2fA5C809C293aFE830F20037eE33d5521e9e036" 90 | } 91 | }, { 92 | "type": "node", 93 | "request": "launch", 94 | "name": "Start Monoplasma validator", 95 | "program": "${workspaceFolder}/start_validator.js", 96 | "env": { 97 | "WATCHED_ACCOUNTS": "0x41ad2327e5910dcca156dc17d0908b690e2f1f7c,0x0e7a1cf7cf69299c20af39056af232fde05b5204,0x297f393328243147e5fd08bd4b8b3786150635cd,0x1612801262e358bdc6031136c0c2104e7d3bd78e,0x2526401999f6502058d57dee734d2277d8669ce7" 98 | } 99 | } 100 | ] 101 | } 102 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.implicitProjectConfig.checkJs": true, 3 | 4 | "solidity.compileUsingRemoteVersion": "0.5.16+commit.9c3226c" 5 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at contact@streamr.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | == How to contribute == 2 | 3 | * Check out the repo 4 | * Run the demo using [instructions in the README](README.md) 5 | * If you bump into problems: 6 | * Fix them, and open a pull request :) 7 | * Contact us in [Telegram](https://t.me/streamrdata) 8 | 9 | Thanks, happy hacking! 10 | -------------------------------------------------------------------------------- /contracts/Airdrop.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "openzeppelin-solidity/contracts/math/SafeMath.sol"; 4 | import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; 5 | 6 | import "./BalanceVerifier.sol"; 7 | import "./Ownable.sol"; 8 | 9 | /** 10 | * Hypothetical "continuous airdrop" where recipients can withdraw tokens allocated off-chain. 11 | * Simplest root chain contract implementation. 12 | * For purposes of illustrating implementing the BalanceVerifier, and mainly for testing it. 13 | */ 14 | contract Airdrop is BalanceVerifier, Ownable { 15 | using SafeMath for uint256; 16 | 17 | IERC20 public token; 18 | mapping (address => uint) public withdrawn; 19 | 20 | constructor(address tokenAddress) Ownable() public { 21 | token = IERC20(tokenAddress); 22 | } 23 | 24 | /** 25 | * Owner commits the balances 26 | */ 27 | function onCommit(uint, bytes32, string memory) internal { 28 | require(msg.sender == owner, "error_notPermitted"); 29 | } 30 | 31 | /** 32 | * Called from BalanceVerifier.prove, perform payout directly (ignoring re-entrancy problems for simplicity) 33 | */ 34 | function onVerifySuccess(uint, address account, uint balance) internal { 35 | require(withdrawn[account] < balance, "error_oldEarnings"); 36 | uint withdrawable = balance.sub(withdrawn[account]); 37 | withdrawn[account] = balance; 38 | require(token.transfer(account, withdrawable), "error_transfer"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contracts/BalanceVerifier.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | /** 4 | * Abstract contract, requires implementation to specify who can publish commits and what 5 | * happens when a successful proof is presented 6 | * Verifies Merkle-tree inclusion proofs that show that certain address has 7 | * certain earnings balance, according to hash published ("signed") by a 8 | * sidechain operator or similar authority 9 | * 10 | * ABOUT Merkle-tree inclusion proof: Merkle-tree inclusion proof is an algorithm to prove memebership 11 | * in a set using minimal [ie log(N)] inputs. The hashes of the items are arranged by hash value in a binary Merkle tree where 12 | * each node contains a hash of the hashes of nodes below. The root node (ie "root hash") contains hash information 13 | * about the entire set, and that is the data that BalanceVerifier posts to the blockchain. To prove membership, you walk up the 14 | * tree from the node in question, and use the supplied hashes (the "proof") to fill in the hashes from the adjacent nodes. The proof 15 | * succeeds iff you end up with the known root hash when you get to the top of the tree. 16 | * See https://medium.com/crypto-0-nite/merkle-proofs-explained-6dd429623dc5 17 | * 18 | * Merkle-tree inclusion proof is a related concept to the blockchain Merkle tree, but a somewhat different application. 19 | * BalanceVerifier posts the root hash of the current ledger only, and this does not depend on the hash of previous ledgers. 20 | * This is different from the blockchain, where each block contains the hash of the previous block. 21 | * 22 | * TODO: see if it could be turned into a library, so many contracts could use it 23 | */ 24 | contract BalanceVerifier { 25 | event NewCommit(uint blockNumber, bytes32 rootHash, string ipfsHash); 26 | 27 | /** 28 | * Root hashes of merkle-trees constructed from its balances 29 | * @param uint root-chain block number after which the balances were committed 30 | * @return bytes32 root of the balances merkle-tree at that time 31 | */ 32 | mapping (uint => bytes32) public committedHash; 33 | 34 | /** 35 | * Handler for proof of off-chain balances 36 | * It is up to the implementing contract to actually distribute out the balances 37 | * @param blockNumber the block whose hash was used for verification 38 | * @param account whose balances were successfully verified 39 | * @param balance the off-chain account balance 40 | */ 41 | function onVerifySuccess(uint blockNumber, address account, uint balance) internal; 42 | 43 | /** 44 | * Implementing contract should should do access controls for committing 45 | */ 46 | function onCommit(uint blockNumber, bytes32 rootHash, string memory ipfsHash) internal; 47 | 48 | /** 49 | * Monoplasma operator submits commitments to root-chain. 50 | * For convenience, also publish the ipfsHash of the balance book JSON object 51 | * @param blockNumber the root-chain block after which the balances were recorded 52 | * @param rootHash root of the balances merkle-tree 53 | * @param ipfsHash where the whole balances object can be retrieved in JSON format 54 | */ 55 | function commit(uint blockNumber, bytes32 rootHash, string calldata ipfsHash) external { 56 | require(committedHash[blockNumber] == 0x0, "error_overwrite"); 57 | string memory _hash = ipfsHash; 58 | onCommit(blockNumber, rootHash, _hash); // Access control delegated to implementing class 59 | committedHash[blockNumber] = rootHash; 60 | emit NewCommit(blockNumber, rootHash, _hash); 61 | } 62 | 63 | /** 64 | * Proving can be used to record the sidechain balances permanently into root chain 65 | * @param blockNumber the block after which the balances were recorded 66 | * @param account whose balances will be verified 67 | * @param balance off-chain account balance 68 | * @param proof list of hashes to prove the totalEarnings 69 | */ 70 | function prove(uint blockNumber, address account, uint balance, bytes32[] memory proof) public { 71 | require(proofIsCorrect(blockNumber, account, balance, proof), "error_proof"); 72 | onVerifySuccess(blockNumber, account, balance); 73 | } 74 | 75 | /** 76 | * Check the merkle proof of balance in the given commit (after blockNumber in root-chain) for given account 77 | * @param blockNumber the block after which the balances were recorded 78 | * @param account whose balances will be verified 79 | * @param balance off-chain account balance 80 | * @param proof list of hashes to prove the totalEarnings 81 | */ 82 | function proofIsCorrect(uint blockNumber, address account, uint balance, bytes32[] memory proof) public view returns(bool) { 83 | bytes32 leafHash = keccak256(abi.encodePacked(account, balance, blockNumber)); 84 | bytes32 rootHash = committedHash[blockNumber]; 85 | require(rootHash != 0x0, "error_blockNotFound"); 86 | return rootHash == calculateRootHash(leafHash, proof); 87 | } 88 | 89 | /** 90 | * Calculate root hash of a Merkle tree, given 91 | * @param leafHash of the member whose balances are being be verified 92 | * @param others list of hashes of "other" branches 93 | */ 94 | function calculateRootHash(bytes32 leafHash, bytes32[] memory others) public pure returns (bytes32 root) { 95 | root = leafHash; 96 | for (uint8 i = 0; i < others.length; i++) { 97 | bytes32 other = others[i]; 98 | if (root < other) { 99 | // TODO: consider hashing in i to defend from https://en.wikipedia.org/wiki/Merkle_tree#Second_preimage_attack 100 | root = keccak256(abi.encodePacked(root, other)); 101 | } else { 102 | root = keccak256(abi.encodePacked(other, root)); 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /contracts/DemoToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "openzeppelin-solidity/contracts/token/ERC20/ERC20Mintable.sol"; 4 | import "openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol"; 5 | 6 | /** 7 | * ERC20 token for demo purposes 8 | * Creator starts with 1 million tokens 9 | */ 10 | contract DemoToken is ERC20Mintable, ERC20Detailed { 11 | constructor(string memory name, string memory symbol) ERC20Detailed(name, symbol, 18) public { 12 | mint(msg.sender, 10**24); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | constructor() public { 8 | owner = msg.sender; 9 | } 10 | 11 | modifier restricted() { 12 | if (msg.sender == owner) _; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/Ownable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | /** 4 | * @title Ownable 5 | * @dev The Ownable contract has an owner address, and provides basic authorization control 6 | * functions, this simplifies the implementation of "user permissions". 7 | */ 8 | contract Ownable { 9 | address public owner; 10 | address public pendingOwner; 11 | 12 | event OwnershipTransferred( 13 | address indexed previousOwner, 14 | address indexed newOwner 15 | ); 16 | 17 | /** 18 | * @dev The Ownable constructor sets the original `owner` of the contract to the sender 19 | * account. 20 | */ 21 | constructor() public { 22 | owner = msg.sender; 23 | } 24 | 25 | /** 26 | * @dev Throws if called by any account other than the owner. 27 | */ 28 | modifier onlyOwner() { 29 | require(msg.sender == owner, "error_onlyOwner"); 30 | _; 31 | } 32 | 33 | /** 34 | * @dev Allows the current owner to set the pendingOwner address. 35 | * @param newOwner The address to transfer ownership to. 36 | */ 37 | function transferOwnership(address newOwner) public onlyOwner { 38 | pendingOwner = newOwner; 39 | } 40 | 41 | /** 42 | * @dev Allows the pendingOwner address to finalize the transfer. 43 | */ 44 | function claimOwnership() public { 45 | require(msg.sender == pendingOwner, "error_onlyPendingOwner"); 46 | emit OwnershipTransferred(owner, pendingOwner); 47 | owner = pendingOwner; 48 | pendingOwner = address(0); 49 | } 50 | } -------------------------------------------------------------------------------- /copy-abi-to-ui.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require("fs") 4 | 5 | const tokenAbi = JSON.parse(fs.readFileSync("build/contracts/DemoToken.json")).abi 6 | fs.writeFileSync("demo/src/utils/tokenAbi.json", JSON.stringify(tokenAbi)) 7 | 8 | let monoplasmaAbi = JSON.parse(fs.readFileSync("build/contracts/Monoplasma.json")).abi 9 | fs.writeFileSync("demo/src/utils/monoplasmaAbi.json", JSON.stringify(monoplasmaAbi)) 10 | -------------------------------------------------------------------------------- /defaultServers.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": "wss://mainnet.infura.io/ws", 3 | "3": "wss://ropsten.infura.io/ws", 4 | "4": "wss://rinkeby.infura.io/ws" 5 | } -------------------------------------------------------------------------------- /demo/.eslintignore: -------------------------------------------------------------------------------- 1 | static/* -------------------------------------------------------------------------------- /demo/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "streamr" 3 | } 4 | -------------------------------------------------------------------------------- /demo/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [options] 8 | module.name_mapper.extension='css' -> 'empty/object' 9 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variables file 55 | .env 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | -------------------------------------------------------------------------------- /demo/gatsby-config.js: -------------------------------------------------------------------------------- 1 | const proxyRouter = require('./src/utils/proxyRouter') 2 | 3 | module.exports = { 4 | plugins: [ 5 | 'gatsby-plugin-eslint', 6 | 'gatsby-plugin-flow', 7 | 'gatsby-plugin-postcss', 8 | 'gatsby-plugin-react-helmet', 9 | ], 10 | developMiddleware: (app) => { 11 | app.use(proxyRouter) 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@streamr/revenue-sharing-demo", 3 | "private": true, 4 | "description": "Revenue sharing demo.", 5 | "version": "0.1.0", 6 | "license": "UNLICENSED", 7 | "scripts": { 8 | "build": "gatsby build", 9 | "develop": "gatsby develop -H 0.0.0.0", 10 | "start": "npm run develop", 11 | "serve": "gatsby serve", 12 | "test": "echo \"Write tests! -> https://gatsby.app/unit-testing\"" 13 | }, 14 | "dependencies": { 15 | "bn.js": "^4.11.8", 16 | "classnames": "^2.2.6", 17 | "empty": "^0.10.1", 18 | "eslint": "^5.13.0", 19 | "eslint-config-airbnb": "^17.1.0", 20 | "eslint-config-streamr": "^1.1.8", 21 | "eslint-loader": "^2.1.1", 22 | "ethjs": "^0.4.0", 23 | "express": "^4.16.4", 24 | "flow-bin": "^0.92.0", 25 | "gatsby": "^2.0.111", 26 | "gatsby-plugin-eslint": "^2.0.4", 27 | "gatsby-plugin-flow": "^1.0.2", 28 | "gatsby-plugin-postcss": "^2.0.5", 29 | "gatsby-plugin-react-helmet": "^3.0.6", 30 | "http-proxy-middleware": "^0.19.1", 31 | "normalize.css": "^8.0.1", 32 | "precss": "^4.0.0", 33 | "react": "^16.8.1", 34 | "react-dom": "^16.8.1", 35 | "react-helmet": "^5.2.0", 36 | "time-ago": "^0.2.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /demo/postcss.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const precss = require('precss') 3 | 4 | module.exports = { 5 | plugins: [ 6 | precss({ 7 | importFrom: [ 8 | path.resolve(__dirname, 'src/stylesheets/variables.css'), 9 | ], 10 | }), 11 | ], 12 | } 13 | -------------------------------------------------------------------------------- /demo/src/components/Button/button.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | position: relative; 3 | } 4 | 5 | .inner { 6 | border-radius: 4px; 7 | color: white; 8 | cursor: pointer; 9 | font-family: var(--mono); 10 | font-size: 1em; 11 | font-weight: var(--semiBold); 12 | height: 4em; 13 | letter-spacing: 1.33px; 14 | line-height: 1em; 15 | padding: 0; 16 | text-transform: uppercase; 17 | transition: 150ms ease-in; 18 | transition-property: transform, background-color, border-color, opacity; 19 | width: 100%; 20 | 21 | &:--interact { 22 | background-color: #0324ff; 23 | border-color: #0324ff; 24 | outline: 0; 25 | transform: scale(1.05); 26 | transition-duration: 75ms; 27 | transition-time-function: ease-out; 28 | } 29 | 30 | &:--disabled, 31 | &:--idle { 32 | transform: scale(1); 33 | } 34 | 35 | &, 36 | &:active { 37 | background-color: #2037ce; 38 | border: 1px solid #2037ce; 39 | } 40 | 41 | &:--disabled { 42 | background-color: #2037ce; 43 | cursor: default; 44 | border: 1px solid #2037ce; 45 | opacity: 0.5; 46 | } 47 | 48 | &:--interact + .tooltip { 49 | opacity: 1; 50 | transform: translateY(0.5rem); 51 | visibility: visible; 52 | } 53 | 54 | &:--disabled + .tooltip { 55 | opacity: 0; 56 | transform: translateY(0.5rem); 57 | visibility: hidden; 58 | } 59 | 60 | &:--disabled + .tooltip, 61 | &:--interact + .tooltip { 62 | transition-delay: 0s, 0s, 0s; 63 | } 64 | } 65 | 66 | .edge, 67 | .redEdge { 68 | &, 69 | &:--idle, 70 | &:--disabled, 71 | &:--interact { 72 | background-color: transparent; 73 | } 74 | } 75 | 76 | .edge { 77 | &:--interact { 78 | color: #0324ff; 79 | } 80 | 81 | &, 82 | &:--disabled, 83 | &:active { 84 | color: #2037ce; 85 | } 86 | } 87 | 88 | .redEdge { 89 | &:--interact { 90 | border-color: #ff0f2d; 91 | color: #ff0f2d; 92 | } 93 | 94 | &, 95 | &:--disabled, 96 | &:active { 97 | border-color: #d71c34; 98 | color: #d71c34; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /demo/src/components/Button/index.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { type Node } from 'react' 4 | import cx from 'classnames' 5 | import Tooltip from '../Tooltip' 6 | 7 | import styles from './button.module.css' 8 | 9 | type Props = { 10 | children: Node, 11 | className?: string, 12 | theme?: 'default' | 'edge' | 'red-edge', 13 | tooltip?: string, 14 | } 15 | 16 | const Button = ({ 17 | children, 18 | className, 19 | theme, 20 | tooltip, 21 | ...props 22 | }: Props) => ( 23 |
24 | 34 | {tooltip && ( 35 | 36 | {tooltip} 37 | 38 | )} 39 |
40 | ) 41 | 42 | export default Button 43 | -------------------------------------------------------------------------------- /demo/src/components/Clock/index.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { useState, type Node, useEffect } from 'react' 4 | 5 | type Props = { 6 | children: (number) => Node, 7 | } 8 | 9 | const Clock = ({ children }: Props) => { 10 | const [timestamp, setTimestamp] = useState(new Date().getTime()) 11 | 12 | useEffect(() => { 13 | const interval: IntervalID = setInterval(() => { 14 | setTimestamp(new Date().getTime()) 15 | }, 1000) 16 | 17 | return () => { 18 | clearInterval(interval) 19 | } 20 | }) 21 | 22 | return children(timestamp) 23 | } 24 | 25 | export default Clock 26 | -------------------------------------------------------------------------------- /demo/src/components/Container/container.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | margin: 0 auto; 3 | position: relative; 4 | width: 896px; 5 | } 6 | -------------------------------------------------------------------------------- /demo/src/components/Container/index.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { type Node } from 'react' 4 | import cx from 'classnames' 5 | 6 | import styles from './container.module.css' 7 | 8 | type Props = { 9 | children: Node, 10 | className?: string, 11 | } 12 | 13 | const Container = ({ children, className }: Props) => ( 14 |
15 | {children} 16 |
17 | ) 18 | 19 | export default Container 20 | -------------------------------------------------------------------------------- /demo/src/components/Home/About/about.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | line-height: 1.75em; 3 | } 4 | 5 | .root + .root { 6 | margin-top: 6rem; 7 | } 8 | 9 | .root p, 10 | .root ul { 11 | margin: 0; 12 | 13 | + p, 14 | + ul { 15 | margin-top: 1.75em; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demo/src/components/Home/About/index.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import Section from '../Section' 5 | 6 | import styles from './about.module.css' 7 | 8 | const About = () => ( 9 |
10 |

11 | This is a demonstration of how Monoplasma contracts can be used to implement basic ERC-20 token revenue 12 | sharing. There are three main roles in the system: 13 |

14 | 32 |

33 | The Operator 34 |
35 | Normally the operator publishes commits when the first revenue transaction arrives after 36 | a cooldown period. This cooldown period plus the freeze period is the time it takes for 37 | a member to be able to withdraw tokens after the token transaction where they earned them. The point 38 | of the cooldown period is to not unnecessarily pay for commit transaction if revenue arrives rapidly. 39 |

40 |

41 | All tokens earned during this period are in the hands of the operator (similar to tokens on an 42 | exchange). The member can lose only those tokens in case of total breakdown of the operator. 43 | By pressing the Force Publish button you can ask the operator to send a commit transaction even during 44 | the cooldown period or if no revenue has been added. 45 |

46 |
47 | ) 48 | 49 | About.styles = styles 50 | 51 | export default About 52 | -------------------------------------------------------------------------------- /demo/src/components/Home/Blocks/blocks.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | border: solid #cdcdcd; 3 | border-width: 1px 0; 4 | font-family: var(--mono); 5 | font-size: 0.75rem; 6 | line-height: 1em; 7 | } 8 | 9 | .row { 10 | display: grid; 11 | grid-template-columns: 1fr 1fr 1fr 1fr; 12 | position: relative; 13 | 14 | > div { 15 | padding: 1em 2rem; 16 | } 17 | 18 | > div + div { 19 | border-left: 1px solid #cdcdcd; 20 | } 21 | 22 | &:--even { 23 | background-color: #ececec; 24 | } 25 | 26 | &.frozen { 27 | background-color: #eaf1f3; 28 | } 29 | 30 | &.frozen:--even { 31 | background-color: #e5ecee; 32 | } 33 | } 34 | 35 | .columnNames { 36 | border-bottom: 1px solid #cdcdcd; 37 | font-weight: var(--medium); 38 | letter-spacing: 1.5px; 39 | text-transform: uppercase; 40 | 41 | > div { 42 | padding-bottom: 1.5em; 43 | padding-top: 1.5em; 44 | } 45 | } 46 | 47 | .flake { 48 | left: 0.5rem; 49 | position: absolute; 50 | top: 50%; 51 | transform: translateY(-50%); 52 | width: 14px; 53 | } 54 | -------------------------------------------------------------------------------- /demo/src/components/Home/Blocks/index.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import cx from 'classnames' 5 | import BN from 'bn.js' 6 | import ta from 'time-ago' 7 | import formatFixedDecimal from '../../../utils/formatFixedDecimal' 8 | import Clock from '../../Clock' 9 | 10 | import styles from './blocks.module.css' 11 | 12 | export type Block = { 13 | blockNumber: number, 14 | timestamp: number, 15 | memberCount: number, 16 | totalEarnings: BN, 17 | frozen?: boolean, 18 | } 19 | 20 | type Props = { 21 | className?: string, 22 | items: Array, 23 | freezePeriodSeconds: string | number, 24 | } 25 | 26 | const Blocks = ({ items, className, freezePeriodSeconds }: Props) => ( 27 |
28 |
29 |
Block #
30 |
Timestamp
31 |
Members
32 |
Earnings
33 |
34 |
35 | {items.map((block) => { 36 | // initial empty blocks are marked with numbers 37 | if (typeof block === 'number') { 38 | return ( 39 |
40 |
41 |
42 |
43 |
44 |
45 | ) 46 | } 47 | return ( 48 | 51 | {(timestamp) => { 52 | const threshold = typeof freezePeriodSeconds === 'number' ? freezePeriodSeconds : Number.parseInt(freezePeriodSeconds, 10) 53 | const frozen = Math.floor((timestamp / 1000) - block.timestamp) < threshold 54 | 55 | return ( 56 |
61 |
62 | {frozen && ( 63 | 64 | 65 | 68 | 74 | 75 | 76 | )} 77 | {block.blockNumber} 78 |
79 |
{`> ${ta.ago(block.timestamp * 1000)}`}
80 |
{block.memberCount}
81 |
{formatFixedDecimal(block.totalEarnings)}
82 |
83 | ) 84 | }} 85 |
86 | ) 87 | })} 88 |
89 |
90 | ) 91 | 92 | export default Blocks 93 | -------------------------------------------------------------------------------- /demo/src/components/Home/Headline/headline.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | border-bottom: 1px solid #cdcdcd; 3 | font-size: 1em; 4 | font-weight: var(--medium); 5 | letter-spacing: 2px; 6 | margin: 0 0 2em; 7 | padding-bottom: 1.25em; 8 | text-transform: uppercase; 9 | } 10 | -------------------------------------------------------------------------------- /demo/src/components/Home/Headline/index.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { type Node } from 'react' 4 | 5 | import styles from './headline.module.css' 6 | 7 | type Props = { 8 | children: Node, 9 | } 10 | 11 | const Headline = ({ children }: Props) => ( 12 |

13 | {children} 14 |

15 | ) 16 | 17 | export default Headline 18 | -------------------------------------------------------------------------------- /demo/src/components/Home/Hero/hero.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | background-color: #f8f8f8; 3 | padding: 10em 0; 4 | text-align: center; 5 | } 6 | 7 | .root h1 { 8 | font-size: 2.5em; 9 | font-weight: var(--regular); 10 | line-height: 1em; 11 | margin: 0 0 0.75em; 12 | } 13 | 14 | .root p { 15 | color: #a3a3a3; 16 | font-size: 0.75em; 17 | font-weight: var(--medium); 18 | line-height: 2em; 19 | letter-spacing: 2px; 20 | margin: 0 auto; 21 | text-transform: uppercase; 22 | width: 75%; 23 | } 24 | -------------------------------------------------------------------------------- /demo/src/components/Home/Hero/index.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import Container from '../../Container' 5 | 6 | import styles from './hero.module.css' 7 | 8 | const Hero = () => ( 9 |
10 | 11 |

Monoplasma Revenue Sharing Demo

12 |

This is a demonstration of how Monoplasma contracts can be used to implement basic ERC-20 token revenue sharing

13 |
14 |
15 | ) 16 | 17 | export default Hero 18 | -------------------------------------------------------------------------------- /demo/src/components/Home/Management/index.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { Component } from 'react' 4 | import Button from '../../Button' 5 | import Input from '../../Input' 6 | import Section from '../Section' 7 | 8 | import styles from './management.module.css' 9 | 10 | type Props = { 11 | onAddUsersClick: (Array) => void, 12 | onMintClick: () => void, 13 | onStealClick: () => void, 14 | } 15 | 16 | type State = { 17 | addresses: string, 18 | } 19 | 20 | class Management extends Component { 21 | state = { 22 | addresses: '', 23 | } 24 | 25 | onAddressesChange = ({ target: { value: addresses } }: SyntheticInputEvent) => { 26 | this.setState({ 27 | addresses, 28 | }) 29 | } 30 | 31 | onAddUsersClick = () => { 32 | const { onAddUsersClick } = this.props 33 | const { addresses } = this.state 34 | 35 | onAddUsersClick(addresses.split(/[\r\n]/m).filter(Boolean)) 36 | this.setState({ 37 | addresses: '', 38 | }) 39 | } 40 | 41 | render() { 42 | const { addresses } = this.state 43 | const { onMintClick, onStealClick } = this.props 44 | 45 | return ( 46 |
47 |
48 |
49 |