├── .babelrc ├── .coveralls.yml ├── .esdoc.js ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── deploy_docs.sh ├── jest.config.js ├── package.json ├── src ├── constants │ ├── arbitrator.js │ ├── contract.js │ ├── dispute.js │ ├── error.js │ ├── eth.js │ └── notification.js ├── contracts │ ├── AbstractContract.js │ ├── ContractImplementation.js │ ├── abstractions │ │ ├── Arbitrable.js │ │ ├── Arbitrator.js │ │ ├── MultipleArbitrable.js │ │ └── index.js │ ├── implementations │ │ ├── PNK │ │ │ ├── MiniMePinakion.js │ │ │ ├── PinakionPOC.js │ │ │ ├── TokenFactory.js │ │ │ └── index.js │ │ ├── RNG │ │ │ ├── BlockHashRNG.js │ │ │ └── index.js │ │ ├── arbitrable │ │ │ ├── Arbitrable.js │ │ │ ├── ArbitrablePermissionList.js │ │ │ ├── ArbitrableTransaction.js │ │ │ ├── MultipleArbitrableTransaction.js │ │ │ └── index.js │ │ ├── arbitrator │ │ │ ├── Kleros.js │ │ │ ├── KlerosPOC.js │ │ │ └── index.js │ │ └── index.js │ └── index.js ├── index.js ├── kleros.js ├── resources │ ├── Auth.js │ ├── Disputes.js │ ├── Notifications.js │ └── index.js └── utils │ ├── EventListener.js │ ├── PromiseQueue.js │ ├── StoreProviderWrapper.js │ ├── Web3Wrapper.js │ ├── delegateCalls.js │ ├── deployContractAsync.js │ ├── getContractAddress.js │ ├── httpRequest.js │ └── isRequired.js ├── tests ├── helpers │ ├── asyncMockResponse.js │ ├── delaySecond.js │ ├── setUpContracts.js │ └── waitNotifications.js ├── integration │ ├── EventListener.test.js │ ├── contracts.test.js │ ├── disputeResolution.test.js │ └── kleros.test.js └── unit │ ├── contracts │ └── abstractions │ │ └── Arbitrator.test.js │ ├── resources │ └── Disputes.test.js │ └── utils │ └── PromiseQueue.test.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["env", { "modules": false }], "stage-2"], 3 | "plugins": ["transform-runtime"], 4 | "env": { 5 | "test": { 6 | "presets": ["env", "stage-2"] 7 | }, 8 | "commonjs": { 9 | "presets": ["env", "stage-2"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: $COVERALLS_REPO_TOKEN 2 | -------------------------------------------------------------------------------- /.esdoc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | source: './src', 3 | destination: './docs', 4 | excludes: ['constants/'], 5 | plugins: [ 6 | { name: 'esdoc-standard-plugin' }, 7 | { name: 'esdoc-ecmascript-proposal-plugin', option: { all: true } } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: './node_modules/kleros-scripts/.eslintrc.js', 3 | rules: { 4 | 'unicorn/filename-case': 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | /node_modules/ 5 | 6 | # Testing 7 | /coverage/ 8 | 9 | # Documentation 10 | /docs/ 11 | 12 | # Production 13 | /umd/ 14 | /es/ 15 | /lib/ 16 | 17 | # Logs 18 | /yarn-debug.log* 19 | /yarn-error.log* 20 | 21 | # Editors 22 | /.vscode/ 23 | /.idea/* 24 | 25 | # Misc 26 | /.DS_Store 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /yarn.lock 3 | 4 | # Configuration 5 | .* 6 | config.json 7 | webpack.config.js 8 | 9 | # Documentation 10 | /docs/ 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - v9.4.0 4 | cache: 5 | directories: 6 | - node_modules 7 | yarn: true 8 | addons: 9 | apt: 10 | sources: 11 | - ubuntu-toolchain-r-test 12 | install: yarn install --pure-lockfile 13 | script: 14 | - yarn run lint 15 | - nohup yarn run ganache & 16 | - yarn test 17 | - if [ -n "$COVERALLS_REPO_TOKEN" ]; then yarn run test:coveralls; fi 18 | - yarn run build 19 | notifications: 20 | slack: kleros:Ub8n81EgKJ3iRrMDyWyQIVJp 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | See [kleros.md](https://kleros.gitbooks.io/kleros-md). 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Smart Contract Solutions, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included 14 | in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kleros API 2 | 3 |

4 | NPM Version 5 | Build Status 6 | Coverage Status 7 | Dependencies 8 | Dev Dependencies 9 | Tested with Jest 10 | JavaScript Style Guide 11 | Styled with Prettier 12 | Conventional Commits 13 | Commitizen Friendly 14 |

15 | 16 | > This repository contains a Javascript library that provides methods to interact with Kleros arbitrator 17 | > and Arbitrable contracts. It can be used to develop Relayers or DApps that use Kleros smart contracts. 18 | 19 | ## Installation 20 | 21 | ``` 22 | yarn add kleros-api 23 | ``` 24 | 25 | ## Basic Usage 26 | 27 | See the full API docs [here](https://kleros.io/kleros-api/). 28 | 29 | The base Kleros object initializes all of the different kleros api's with the contract 30 | addresses you pass. This object is useful if your application interacts with both arbitrators, 31 | arbitrable contracts and uses an off chain store to provide metadata on the different disputes 32 | for the UI. 33 | 34 | ``` 35 | // pay arbitration fee. 36 | import Kleros from 'kleros-api' 37 | 38 | const KlerosInstance = new Kleros( 39 | ETH_PROVIDER, // ethereum provider object 40 | KLEROS_STORE_URI, // uri of off chain storage e.g. https://kleros.in 41 | ARITRATOR_CONTRACT_ADDRESS, // address of a deployed Kleros arbitrator contract 42 | ARBITRABLE_CONTRACT_ADDRESS // address of a deployed arbitrable transaction contract 43 | ) 44 | 45 | KlerosInstance.arbitrable.payArbitrationFeeByPartyA() // pay arbitration fee for an arbitrable contract 46 | ``` 47 | 48 | You can also use the specific api that best suits your needs. 49 | 50 | ``` 51 | // deploy a new contract and pay the arbitration fee. 52 | import ArbitrableTransaction from 'kleros-api/contracts/implementations/arbitrable/ArbitrableTransaction' 53 | 54 | // deploy methods are static 55 | const contractInstance = ArbitrableTransaction.deploy( 56 | "0x67a3f2BB8B4B2060875Bd6543156401B817bEd22", // users address 57 | 0.15, // amount of ETH to escrow 58 | "0x0", // hash of the off chain contract 59 | "0x3af76ef44932695a33ba2af52018cd24a74c904f", // arbitrator address 60 | 3600, // number of seconds until there is a timeout 61 | "0x0474b723bd4986808366E0DcC2E301515Aa742B4", // the other party in the contract 62 | "0x0", // extra data in bytes. This can be used to interact with the arbitrator contract 63 | ETH_PROVIDER, // provider object to be used to interact with the network 64 | ) 65 | 66 | const address = contractInstance.address // get the address of your newly created contract 67 | 68 | const ArbitrableTransactionInstance = new ArbitrableTransaction(address) // instantiate instance of the api 69 | 70 | ArbitrableTransactionInstance.payArbitrationFeeByPartyA() // pay arbitration fee 71 | ``` 72 | 73 | ## Development 74 | 75 | If you want to contribute to our api or modify it for your usage 76 | 77 | ## Setup 78 | 79 | We assume that you have node and yarn installed. 80 | 81 | ```sh 82 | yarn install 83 | ``` 84 | 85 | ## Test 86 | 87 | ```sh 88 | yarn ganache 89 | yarn test 90 | ``` 91 | 92 | ## Build 93 | 94 | ```sh 95 | yarn run build 96 | ``` 97 | -------------------------------------------------------------------------------- /deploy_docs.sh: -------------------------------------------------------------------------------- 1 | # Remove old docs dir 2 | rm -rf docs || exit 0; 3 | # Build docs 4 | yarn run docs; 5 | 6 | # Set up new directory 7 | mkdir ../esdocs; 8 | cp -r docs/* ../esdocs/; 9 | 10 | # github pages. must be run by user with ssh write access to kleros-api 11 | cd ../esdocs; 12 | git init; 13 | git add .; 14 | git commit -m "Deploy to GitHub Pages"; 15 | git push --force --quiet "git@github.com:kleros/kleros-api.git" master:gh-pages; 16 | 17 | # cleanup 18 | rm -rf ../esdocs; 19 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | collectCoverage: true, 4 | collectCoverageFrom: ['src/**/*.js', '!**/node_modules/**'] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kleros-api-2", 3 | "version": "0.18.12", 4 | "description": "A Javascript library that makes it easy to build relayers and other DApps that use the Kleros protocol.", 5 | "keywords": [ 6 | "Blockchain", 7 | "Ethereum", 8 | "Kleros" 9 | ], 10 | "main": "lib/index.js", 11 | "module": "es/index.js", 12 | "files": [ 13 | "es", 14 | "lib", 15 | "umd" 16 | ], 17 | "homepage": "https://kleros.io", 18 | "repository": "github:kleros/kleros-api", 19 | "bugs": "https://github.com/kleros/kleros-api/issues", 20 | "author": "Kleros Team (https://github.com/kleros)", 21 | "contributors": [ 22 | "Wagner Nicolas (https://github.com/n1c01a5)", 23 | "Sam Vitello (https://github.com/satello)", 24 | "Enrique Piqueras (https://github.com/epiqueras)" 25 | ], 26 | "license": "MIT", 27 | "private": false, 28 | "scripts": { 29 | "docs": "esdoc", 30 | "prettify": "kleros-scripts prettify", 31 | "lint": "kleros-scripts lint:js --config ./.eslintrc.js", 32 | "ganache": "ganache-cli -a 15 -s 1", 33 | "test": "GAS=3000000 jest --config ./jest.config.js", 34 | "test:coveralls": "coveralls < ./coverage/lcov.info", 35 | "commitmsg": "kleros-scripts commitmsg", 36 | "cz": "kleros-scripts cz", 37 | "start": "babel src --out-dir ./es --watch --source-maps", 38 | "build": "rimraf ./umd ./es ./lib && webpack --env.NODE_ENV=production -p && babel src --out-dir ./es --source-maps && cross-env BABEL_ENV=commonjs babel src --out-dir ./lib --source-maps" 39 | }, 40 | "commitlint": { 41 | "extends": [ 42 | "@commitlint/config-conventional" 43 | ] 44 | }, 45 | "devDependencies": { 46 | "babel-cli": "^6.26.0", 47 | "babel-core": "^6.26.0", 48 | "babel-jest": "^22.4.1", 49 | "babel-loader": "^7.1.3", 50 | "babel-plugin-transform-runtime": "^6.23.0", 51 | "babel-preset-env": "^1.6.1", 52 | "babel-preset-stage-2": "^6.24.1", 53 | "coveralls": "^3.0.0", 54 | "cross-env": "^5.1.4", 55 | "esdoc": "^1.0.4", 56 | "esdoc-ecmascript-proposal-plugin": "^1.0.0", 57 | "esdoc-standard-plugin": "^1.0.0", 58 | "ganache-cli": "^6.1.8", 59 | "husky": "^0.14.3", 60 | "jest": "^22.4.2", 61 | "kleros-scripts": "^0.4.0", 62 | "rimraf": "^2.6.2", 63 | "standard-version": "^4.3.0", 64 | "webpack": "^4.0.1", 65 | "webpack-cli": "^2.0.9" 66 | }, 67 | "dependencies": { 68 | "babel-runtime": "^6.26.0", 69 | "eth-sig-util": "^1.4.2", 70 | "ethereumjs-util": "^5.2.0", 71 | "ethjs": "^0.4.0", 72 | "kleros": "^0.0.6", 73 | "kleros-interaction": "^0.1.1", 74 | "lodash": "^4.17.4", 75 | "minimetoken": "^0.2.0", 76 | "truffle-contract": "^2.0.5", 77 | "web3": "^0.20.1", 78 | "web3-eth-personal": "^1.0.0-beta.34" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/constants/arbitrator.js: -------------------------------------------------------------------------------- 1 | export const DEFAULT_ARBITRATION_FEE = 0.15 2 | 3 | export const PERIOD = { 4 | ACTIVATION: 0, 5 | DRAW: 1, 6 | VOTE: 2, 7 | APPEAL: 3, 8 | EXECUTE: 4 9 | } 10 | -------------------------------------------------------------------------------- /src/constants/contract.js: -------------------------------------------------------------------------------- 1 | export const STATUS = { 2 | NO_DISPUTE: 0, 3 | WAITING_SELLER: 1, 4 | WAITING_BUYER: 2, 5 | DISPUTE_CREATED: 3, 6 | RESOLVED: 4 7 | } 8 | -------------------------------------------------------------------------------- /src/constants/dispute.js: -------------------------------------------------------------------------------- 1 | export const STATE = { 2 | OPEN: 0, 3 | RESOLVING: 1, 4 | EXECUTABLE: 2, 5 | RESOLVED: 3 6 | } 7 | 8 | export const DISPUTE_CACHE_URI = 9 | 'https://plkzpnwz7e.execute-api.us-east-2.amazonaws.com/session' 10 | -------------------------------------------------------------------------------- /src/constants/error.js: -------------------------------------------------------------------------------- 1 | // Kleros 2 | export const MISSING_PARAMETERS = name => `Missing required parameter: ${name}` 3 | 4 | // StoreProviderWrapper 5 | export const PROFILE_NOT_FOUND = user => `No profile found for user: ${user}.` 6 | export const NOTIFICATION_NOT_FOUND = txHash => 7 | `No notification with txHash ${txHash} exists.` 8 | export const REQUEST_FAILED = error => 9 | `Request returned an error response: ${error}` 10 | 11 | // Contracts 12 | export const UNABLE_TO_DEPLOY_CONTRACT = 13 | 'Unable to deploy contract, are you sure the contract artifact is correct?' 14 | export const CONTRACT_NOT_DEPLOYED = 15 | 'Unable to load contract. Are you sure the contract is deployed and you are on the right network?' 16 | export const UNABLE_TO_LOAD_CONTRACT = 17 | 'Unable to load contract. Are you sure the contract artifact is correct?' 18 | export const MISSING_CONTRACT_PARAMETERS = 19 | 'Unable to load contract. Missing contractAddress or Artifact. Please call setContractInstance' 20 | 21 | // Implementation 22 | export const CONTRACT_INSTANCE_NOT_SET = 23 | 'No contract instance. Use setContractInstance' 24 | 25 | // PinakionPOC 26 | export const UNABLE_TO_SET_KLEROS = 'Unable to set Kleros.' 27 | export const UNABLE_TO_TRANSFER_OWNERSHIP = 'Unable to transfer ownership.' 28 | 29 | // KlerosPOC 30 | export const UNABLE_TO_BUY_PNK = 31 | 'Unable to buy PNK, are you sure you have enough ETH?' 32 | export const UNABLE_TO_ACTIVATE_PNK = 33 | 'Unable to activate PNK, are you sure you have enough?' 34 | export const UNABLE_TO_FETCH_ARBITRATION_COST = 35 | 'Unable to fetch arbitration cost.' 36 | export const UNABLE_TO_FETCH_APPEAL_COST = 'Unable to fetch appeal cost.' 37 | export const UNABLE_TO_FETCH_TIME_PER_PERIOD = 'Unable to fetch time per period' 38 | export const UNABLE_TO_PASS_PERIOD = 'Unable to pass period.' 39 | export const UNABLE_TO_FETCH_DISPUTE = 'Unable to fetch dispute.' 40 | export const UNABLE_TO_FETCH_AMOUNT_OF_JURORS = 41 | 'Unable to fetch amount of jurors' 42 | export const UNABLE_TO_SUBMIT_VOTES = 'Unable to submit votes.' 43 | export const UNABLE_TO_APPEAL = 'Unable to appeal.' 44 | export const UNABLE_TO_REPARTITION_TOKENS = 'Unable to repartition tokens.' 45 | export const UNABLE_TO_EXECUTE_RULING = 'Unable to execute ruling.' 46 | export const ACCOUNT_NOT_A_JUROR_FOR_CONTRACT = (account, contractAddress) => 47 | `${account} is not a juror for contract ${contractAddress}` 48 | export const PERIOD_OUT_OF_RANGE = periodNumber => 49 | `Period ${periodNumber} does not have a time associated with it.` 50 | export const DISPUTE_DOES_NOT_EXIST = disputeID => 51 | `Dispute ${disputeID} does not exist` 52 | 53 | // ArbitrableTransaction 54 | export const UNABLE_TO_CREATE_TRANSACTION = 55 | 'Unable to create a new transaction.' 56 | export const UNABLE_TO_PAY_ARBITRATION_FEE = 57 | 'Unable to pay fee, are you sure you have enough PNK?' 58 | export const UNABLE_TO_RAISE_AN_APPEAL = 59 | 'Unable to raise appeal, are you sure you are in the appeal period?' 60 | export const UNABLE_TO_PAY_SELLER = 61 | 'Unable to pay the seller, are you sure you have enough ETH?' 62 | export const UNABLE_TO_REIMBURSE_BUYER = 'Unable to reimburse the buyer.' 63 | export const UNABLE_TO_CALL_TIMEOUT = 'Unable to call timeout.' 64 | export const CONTRACT_IS_NOT_WAITING_ON_OTHER_PARTY = 65 | 'Unable to call timeout, because the contract is not waiting on the other party.' 66 | export const TIMEOUT_NOT_REACHED = 67 | 'Unable to call timeout, because it has not been reached yet.' 68 | 69 | // AbstractContract 70 | export const NO_ARBITRATOR_IMPLEMENTATION_SPECIFIED = 71 | 'No Arbitrator Contract Implementation specified. Please call setArbitrator.' 72 | export const NO_ARBITRABLE_IMPLEMENTATION_SPECIFIED = 73 | 'No Arbitrable Contract Implementation specified. Please call setArbitrable.' 74 | export const NO_STORE_PROVIDER_SPECIFIED = 75 | 'No Store Provider Specified. Please call setStoreProvider' 76 | 77 | // Disputes 78 | export const NO_STORE_DATA_FOR_DISPUTE = account => 79 | `Account ${account} does not have store data for dispute` 80 | 81 | // Notifications 82 | export const MISSING_STORE_PROVIDER = 83 | 'This method requires the use of an off chain store. Please call setStoreProviderInstance.' 84 | 85 | // Event Listener 86 | export const MISSING_CONTRACT_INSTANCE = contractAddress => 87 | `No contract instance stored for ${contractAddress}. Please call addContractInstance.` 88 | export const ERROR_FETCHING_EVENTS = error => `Unable to fetch events: ${error}` 89 | 90 | // Auth 91 | export const UNABLE_TO_SIGN_TOKEN = `There was an error signing the auth token. Please try again` 92 | export const INVALID_AUTH_TOKEN = msg => 93 | `Authorization Token is invalid: ${msg}` 94 | -------------------------------------------------------------------------------- /src/constants/eth.js: -------------------------------------------------------------------------------- 1 | export const LOCALHOST_ETH_PROVIDER = 'http://localhost:8545' 2 | export const LOCALHOST_STORE_PROVIDER = 'https://kleros.in' 3 | // export const LOCALHOST_STORE_PROVIDER = 'http://localhost:3001' 4 | 5 | export const NULL_ADDRESS = '0x' 6 | 7 | export const TRANSACTION = { 8 | GAS: 6400000, 9 | VALUE: 0 10 | } 11 | -------------------------------------------------------------------------------- /src/constants/notification.js: -------------------------------------------------------------------------------- 1 | export const TYPE = { 2 | DISPUTE_CREATED: 0, 3 | APPEAL_POSSIBLE: 1, 4 | RULING_APPEALED: 2, 5 | TOKEN_SHIFT: 3, 6 | ARBITRATION_REWARD: 4, 7 | CAN_ACTIVATE: 5, 8 | CAN_REPARTITION: 6, 9 | CAN_EXECUTE: 7, 10 | CAN_PAY_FEE: 8 11 | } 12 | -------------------------------------------------------------------------------- /src/contracts/AbstractContract.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | import isRequired from '../utils/isRequired' 4 | import delegateCalls from '../utils/delegateCalls' 5 | 6 | /** 7 | * Abstract Contract holds a contract implementation to make calls to the blockchain but 8 | * also includes methods that interact with the off chain store. NOTE all methods that 9 | * the underlying contract implementation expose can be called directly from an Abstract contract. 10 | */ 11 | class AbstractContract { 12 | /** 13 | * AbstractContract wraps an implementation instance to provide access to higher level 14 | * services such as an off chain store, as well as the functionality of the underlying 15 | * implementation. 16 | * @param {object} implementationInstance - Contract Implementation object to extend 17 | * @param {object} storeProviderInstance - StoreProvider wrapper object. 18 | */ 19 | constructor( 20 | implementationInstance = isRequired('implementationInstance'), 21 | storeProviderInstance = isRequired('storeProviderInstance') 22 | ) { 23 | this._StoreProvider = storeProviderInstance 24 | this._contractImplementation = implementationInstance 25 | delegateCalls(this, implementationInstance) 26 | } 27 | 28 | /** 29 | * Set store provider instance. 30 | * @param {object} storeProviderInstance - instance of store provider wrapper. 31 | */ 32 | setStoreProviderInstance = storeProviderInstance => { 33 | this._StoreProvider = storeProviderInstance 34 | } 35 | } 36 | 37 | export default AbstractContract 38 | -------------------------------------------------------------------------------- /src/contracts/ContractImplementation.js: -------------------------------------------------------------------------------- 1 | import contract from 'truffle-contract' 2 | import _ from 'lodash' 3 | 4 | import isRequired from '../utils/isRequired' 5 | import * as errorConstants from '../constants/error' 6 | import Web3Wrapper from '../utils/Web3Wrapper' 7 | 8 | /** 9 | * ContractImplementation is a parent class for on chain contracts. It loads the contract from the 10 | * blockchain and exposes the contract instance for use by the child. 11 | */ 12 | class ContractImplementation { 13 | constructor( 14 | web3Provider = isRequired('web3Provider'), 15 | artifact = isRequired('artifact'), 16 | contractAddress 17 | ) { 18 | this.contractAddress = contractAddress 19 | this.artifact = artifact 20 | this.contractInstance = null 21 | this._Web3Wrapper = new Web3Wrapper(web3Provider) 22 | // loading params 23 | this._contractLoadedResolver = null 24 | this._contractLoadedRejecter = null 25 | this._loadingContractInstance = null 26 | this.isLoading = false 27 | } 28 | 29 | /** 30 | * Load contract instance if not yet initialized. Returns loading promise 31 | * @returns {Promise} resolves to contractInstance 32 | */ 33 | loadContract = async () => { 34 | if (this.isLoading) return this._loadingContractInstance 35 | if (this.contractInstance) return this.contractInstance 36 | 37 | if (!this.contractAddress || !this.artifact) 38 | throw new Error(errorConstants.CONTRACT_INSTANCE_NOT_SET) 39 | 40 | const newLoadingPromise = this._newLoadingPromise() 41 | this._loadingContractInstance = newLoadingPromise 42 | this._load() 43 | return newLoadingPromise 44 | } 45 | 46 | /** 47 | * Get the web3 provider object from the initialized web3 instance 48 | * @returns {object} web3 provider object 49 | */ 50 | getWeb3Provider = () => this._Web3Wrapper.getProvider() 51 | 52 | /** 53 | * MetaMask safe get block data by blockNumber 54 | * @param {Int} blockNumber - Block number 55 | * @returns {Promise} block object 56 | */ 57 | getBlock = async blockNumber => this._Web3Wrapper.getBlock(blockNumber) 58 | 59 | /** 60 | * Set a new contract instance 61 | * @param {string} contractAddress - The address of the contract 62 | * @param {object} artifact - Contract artifact to use to load contract 63 | * @returns {object} contractInstance object 64 | */ 65 | setContractInstance = async ( 66 | contractAddress = this.contractAddress, 67 | artifact = this.artifact 68 | ) => { 69 | this.contractAddress = contractAddress 70 | this.artifact = artifact 71 | this.contractInstance = null 72 | return this.loadContract() 73 | } 74 | 75 | /** 76 | * Load an existing contract from the current artifact and address 77 | */ 78 | _load = async () => { 79 | this.isLoading = true 80 | try { 81 | this.contractInstance = await this._instantiateContractIfExistsAsync( 82 | this.artifact, 83 | this.contractAddress 84 | ) 85 | 86 | this.isLoading = false 87 | this._contractLoadedResolver(this.contractInstance) 88 | } catch (err) { 89 | this.isLoading = false 90 | this._contractLoadedRejecter(err) 91 | } 92 | } 93 | 94 | /** 95 | * Instantiate contract. 96 | * @private 97 | * @param {object} artifact - The contract artifact. 98 | * @param {string} address - The hex encoded contract Ethereum address 99 | * @returns {object} - The contract instance. 100 | */ 101 | _instantiateContractIfExistsAsync = async (artifact, address) => { 102 | try { 103 | const c = await contract(artifact) 104 | await c.setProvider(await this._Web3Wrapper.getProvider()) 105 | const contractInstance = _.isUndefined(address) 106 | ? await c.deployed() 107 | : await c.at(address) 108 | 109 | // Estimate gas before sending transactions 110 | for (const funcABI of contractInstance.abi) { 111 | // Check for non-constant functions 112 | if (funcABI.type === 'function' && funcABI.constant === false) { 113 | const func = contractInstance[funcABI.name] 114 | 115 | // eslint-disable-next-line no-loop-func 116 | contractInstance[funcABI.name] = async (...args) => { 117 | await func.estimateGas(...args) // Estimate gas (also checks for possible failures) 118 | return func(...args) // Call original function 119 | } 120 | 121 | // Keep reference to the original function for special cases 122 | contractInstance[funcABI.name].original = func 123 | 124 | // Forward other accessors to the original function 125 | Object.setPrototypeOf(contractInstance[funcABI.name], func) 126 | } 127 | } 128 | 129 | return contractInstance 130 | } catch (err) { 131 | console.error(err) 132 | 133 | if (_.includes(err.message, 'not been deployed to detected network')) 134 | throw new Error(errorConstants.CONTRACT_NOT_DEPLOYED) 135 | 136 | throw new Error(errorConstants.UNABLE_TO_LOAD_CONTRACT) 137 | } 138 | } 139 | 140 | /** 141 | * Create a new Promise to be used in loading the contract. 142 | * @returns {Promise} - Resolves to contract instance. 143 | */ 144 | _newLoadingPromise = () => 145 | new Promise((resolve, reject) => { 146 | this._contractLoadedResolver = resolve 147 | this._contractLoadedRejecter = reject 148 | }) 149 | 150 | /** 151 | * Get the contract address for the currently instantiated contract. 152 | * @returns {string} - The address of the contract. 153 | */ 154 | getContractAddress = () => this.contractAddress 155 | 156 | /** 157 | * Returns the web3 wrapper 158 | * @returns {object} - Web3 Wrapper 159 | */ 160 | getWeb3Wrapper = () => this._Web3Wrapper 161 | } 162 | 163 | export default ContractImplementation 164 | -------------------------------------------------------------------------------- /src/contracts/abstractions/Arbitrable.js: -------------------------------------------------------------------------------- 1 | import Eth from 'ethjs' 2 | 3 | import getContractAddress from '../../utils/getContractAddress' 4 | import AbstractContract from '../AbstractContract' 5 | 6 | /** 7 | * Arbitrable Abstract Contarct API. This wraps an arbitrable contract. It provides 8 | * interaction with both the off chain store as well as the arbitrable instance. All 9 | * arbitrable methods from the supplied contract implementation can be called from this 10 | * object. 11 | */ 12 | class ArbitrableContract extends AbstractContract { 13 | /** 14 | * Deploy a contract and add to the Store. 15 | * @param {string} account - Ethereum address. 16 | * @param {int} value - funds to be placed in contract. 17 | * @param {string} arbitratorAddress - The address of the arbitrator contract. 18 | * @param {int} timeout - Time after which a party automatically loose a dispute. 19 | * @param {string} partyB - Ethereum address of the other party in the contract. 20 | * @param {bytes} arbitratorExtraData - Extra data for the arbitrator. 21 | * @param {string} email - Email address of the contract creator (default empty string). 22 | * @param {object} metaEvidence - The metaEvidence object associated with the Arbitarble Contract. 23 | * @param {...any} args - Extra arguments for the contract. 24 | * @returns {object | Error} - The contract object or an error. 25 | */ 26 | deploy = async ( 27 | account, 28 | value, 29 | arbitratorAddress, 30 | timeout, 31 | partyB, 32 | arbitratorExtraData = '', 33 | email = '', 34 | metaEvidence = {}, 35 | ...args 36 | ) => { 37 | const web3Provider = this._contractImplementation.getWeb3Provider() 38 | const eth = new Eth(web3Provider) 39 | const txCount = await eth.getTransactionCount(account) 40 | // determine the contract address WARNING if the nonce changes this will produce a different address 41 | const contractAddress = getContractAddress(account, txCount) 42 | const metaEvidenceUri = this._StoreProvider.getMetaEvidenceUri( 43 | account, 44 | contractAddress 45 | ) 46 | const contractInstance = await this._contractImplementation.constructor.deploy( 47 | account, 48 | value, 49 | arbitratorAddress, 50 | timeout, 51 | partyB, 52 | arbitratorExtraData, 53 | metaEvidenceUri, 54 | web3Provider, 55 | ...args 56 | ) 57 | 58 | if (contractInstance.address !== contractAddress) 59 | throw new Error('Contract address does not match meta-evidence uri') 60 | 61 | const newContract = await this._StoreProvider.updateContract( 62 | account, 63 | contractInstance.address, 64 | { 65 | partyA: account, 66 | partyB, 67 | email, 68 | metaEvidence 69 | } 70 | ) 71 | 72 | return newContract 73 | } 74 | 75 | /** 76 | * Submit evidence. FIXME should we determine the hash for the user? 77 | * @param {string} account - ETH address of user. 78 | * @param {string} name - Name of evidence. 79 | * @param {string} description - Description of evidence. 80 | * @param {string} url - A link to an evidence using its URI. 81 | * @param {string} hash - A hash of the evidence at the URI. No hash if content is dynamic 82 | * @returns {string} - txHash Hash transaction. 83 | */ 84 | submitEvidence = async (account, name, description, url, hash) => { 85 | const contractAddress = this._contractImplementation.contractAddress 86 | // get the index of the new evidence 87 | const evidenceIndex = await this._StoreProvider.addEvidenceContract( 88 | contractAddress, 89 | account, 90 | name, 91 | description, 92 | url, 93 | hash 94 | ) 95 | // construct the unique URI 96 | const evidenceUri = this._StoreProvider.getEvidenceUri( 97 | account, 98 | contractAddress, 99 | evidenceIndex 100 | ) 101 | const txHash = await this._contractImplementation.submitEvidence( 102 | account, 103 | evidenceUri 104 | ) 105 | 106 | return txHash 107 | } 108 | 109 | /** 110 | * Get all contracts TODO do we need to get contract data from blockchain? 111 | * @param {string} account - Address of user. 112 | * @returns {object[]} - Contract data from store. 113 | */ 114 | getContractsForUser = async account => { 115 | // fetch user profile 116 | const userProfile = await this._StoreProvider.newUserProfile(account) 117 | 118 | return userProfile.contracts 119 | } 120 | 121 | /** 122 | * Fetch all data from the store on the current contract. 123 | * @returns {object} - Store data for contract. 124 | */ 125 | getDataFromStore = async () => { 126 | const contractInstance = await this._contractImplementation.loadContract() 127 | const partyA = await contractInstance.partyA() 128 | 129 | return this._StoreProvider.getContractByAddress( 130 | partyA, 131 | this._contractImplementation.contractAddress 132 | ) 133 | } 134 | } 135 | 136 | export default ArbitrableContract 137 | -------------------------------------------------------------------------------- /src/contracts/abstractions/Arbitrator.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | import * as arbitratorConstants from '../../constants/arbitrator' 4 | import AbstractContract from '../AbstractContract' 5 | import httpRequest from '../../utils/httpRequest' 6 | import { DISPUTE_CACHE_URI } from '../../constants/dispute' 7 | /** 8 | * Arbitrator Abstract Contarct API. This wraps an arbitrator contract. It provides 9 | * interaction with both the off chain store as well as the arbitrator instance. All 10 | * arbitrator methods from the supplied contract implementation can be called from this 11 | * object. 12 | */ 13 | class Arbitrator extends AbstractContract { 14 | /** 15 | * Get disputes for user with extra data from arbitrated transaction and store 16 | * @param {string} account Address of user 17 | * @param {bool} allowOffChainCache Should open disputes be pulled from off chain cache. 18 | * The cache is maintained by Kleros for performance. To pull open disputes from the blockchain 19 | * set false 20 | * @returns {object[]} dispute data objects for user 21 | */ 22 | getDisputesForUser = async (account, allowOffChainCache = true) => { 23 | // contract data 24 | const [period, currentSession] = await Promise.all([ 25 | this._contractImplementation.getPeriod(), 26 | this._contractImplementation.getSession() 27 | ]) 28 | 29 | // new jurors have not been chosen yet. don't update 30 | if (period !== arbitratorConstants.PERIOD.VOTE) { 31 | return this.getDisputesForUserFromStore(account) 32 | } 33 | 34 | let profile = await this._StoreProvider.newUserProfile(account) 35 | if (currentSession !== profile.session) { 36 | // pull open disputes from off chain cache 37 | let cachedDisputes = null 38 | if (allowOffChainCache) { 39 | const cachedDisputesResponse = await httpRequest( 40 | 'GET', 41 | `${DISPUTE_CACHE_URI}/${currentSession}` 42 | ) 43 | 44 | if ( 45 | cachedDisputesResponse.body && 46 | cachedDisputesResponse.body.open_disputes 47 | ) { 48 | cachedDisputes = await Promise.all( 49 | cachedDisputesResponse.body.open_disputes.map(disputeIDString => 50 | this._contractImplementation.getDispute(Number(disputeIDString)) 51 | ) 52 | ) 53 | } 54 | } 55 | 56 | // get disputes for juror 57 | const myDisputes = await this._contractImplementation.getDisputesForJuror( 58 | account, 59 | cachedDisputes 60 | ) 61 | 62 | // update user profile for each dispute 63 | await Promise.all( 64 | myDisputes.map(async dispute => { 65 | if (dispute.appealDraws[dispute.numberOfAppeals].length > 0) { 66 | const disputeCreationLog = await this._contractImplementation.getDisputeCreationEvent( 67 | dispute.disputeID 68 | ) 69 | 70 | if (!disputeCreationLog) 71 | throw new Error('Could not fetch dispute creation event log') 72 | // update profile for account 73 | await this._StoreProvider.updateDisputeProfile( 74 | account, 75 | dispute.arbitratorAddress, 76 | dispute.disputeID, 77 | { 78 | blockNumber: disputeCreationLog.blockNumber 79 | } 80 | ) 81 | // add draws separately for appeals 82 | await this._StoreProvider.addNewDrawsDisputeProfile( 83 | account, 84 | dispute.arbitratorAddress, 85 | dispute.disputeID, 86 | dispute.appealDraws[dispute.numberOfAppeals], 87 | dispute.numberOfAppeals 88 | ) 89 | } 90 | }) 91 | ) 92 | 93 | // FIXME do we want to store session? 94 | this._StoreProvider.updateUserSession(account, currentSession) 95 | } 96 | 97 | return this.getDisputesForUserFromStore(account) 98 | } 99 | 100 | /** 101 | * Get disputes from the store. 102 | * @param {string} account - The users account. 103 | * @returns {object[]} The dispute objects. 104 | */ 105 | getDisputesForUserFromStore = async account => { 106 | const aribtratorAddress = this._contractImplementation.getContractAddress() 107 | return Promise.all( 108 | (await this._StoreProvider.getDisputes(account)) 109 | .filter(dispute => dispute.arbitratorAddress === aribtratorAddress) 110 | .map(dispute => 111 | this._contractImplementation.getDispute(dispute.disputeId) 112 | ) 113 | ) 114 | } 115 | 116 | /** 117 | * Buy PNK. 118 | * @param {number} amount - Number of pinakion to buy. 119 | * @param {string} account - Address of user. 120 | * @returns {object[]} - Balance of user. 121 | */ 122 | buyPNK = async (amount, account) => { 123 | await this._contractImplementation.buyPNK(amount, account) 124 | return this._contractImplementation.getPNKBalance(account) 125 | } 126 | } 127 | 128 | export default Arbitrator 129 | -------------------------------------------------------------------------------- /src/contracts/abstractions/MultipleArbitrable.js: -------------------------------------------------------------------------------- 1 | import AbstractContract from '../AbstractContract' 2 | 3 | /** 4 | * Arbitrable Abstract Contarct API. This wraps an arbitrable contract. It provides 5 | * interaction with both the off chain store as well as the arbitrable instance. All 6 | * arbitrable methods from the supplied contract implementation can be called from this 7 | * object. 8 | */ 9 | class ArbitrableContract extends AbstractContract { 10 | /** 11 | * Submit evidence. FIXME should we determine the hash for the user? 12 | * @param {string} account - ETH address of user. 13 | * @param {string} arbitrableTransactionId - Id of the arbitrable transaction. 14 | * @param {string} name - Name of evidence. 15 | * @param {string} description - Description of evidence. 16 | * @param {string} url - A link to an evidence using its URI. 17 | * @param {string} hash - A hash of the evidence at the URI. No hash if content is dynamic 18 | * @returns {string} - txHash Hash transaction. 19 | */ 20 | submitEvidence = async ( 21 | account, 22 | arbitrableTransactionId, 23 | name, 24 | description, 25 | url, 26 | hash 27 | ) => { 28 | const contractAddress = this._contractImplementation.contractAddress 29 | 30 | // get the index of the new evidence 31 | const evidenceIndex = await this._StoreProvider.addEvidenceContract( 32 | contractAddress, 33 | arbitrableTransactionId, 34 | account, 35 | name, 36 | description, 37 | url, 38 | hash 39 | ) 40 | 41 | // construct the unique URI 42 | const evidenceUri = this._StoreProvider.getEvidenceUri( 43 | account, 44 | contractAddress, 45 | arbitrableTransactionId, 46 | evidenceIndex 47 | ) 48 | 49 | const txHash = await this._contractImplementation.submitEvidence( 50 | account, 51 | arbitrableTransactionId, 52 | evidenceUri 53 | ) 54 | 55 | return txHash 56 | } 57 | } 58 | 59 | export default ArbitrableContract 60 | -------------------------------------------------------------------------------- /src/contracts/abstractions/index.js: -------------------------------------------------------------------------------- 1 | import Arbitrator from './Arbitrator' 2 | import Arbitrable from './Arbitrable' 3 | 4 | export { Arbitrator, Arbitrable } 5 | -------------------------------------------------------------------------------- /src/contracts/implementations/PNK/MiniMePinakion.js: -------------------------------------------------------------------------------- 1 | import PinakionPOCArtifact from 'kleros-interaction/build/contracts/MiniMeTokenERC20' 2 | import _ from 'lodash' 3 | 4 | import * as errorConstants from '../../../constants/error' 5 | import ContractImplementation from '../../ContractImplementation' 6 | import deployContractAsync from '../../../utils/deployContractAsync' 7 | import isRequired from '../../../utils/isRequired' 8 | 9 | /** 10 | * Provides interaction with a PinakionPOC contract deployed on the blockchain. 11 | */ 12 | class MiniMePinakion extends ContractImplementation { 13 | /** 14 | * Constructor PinakionPOC. 15 | * @param {object} web3Provider - web3 instance. 16 | * @param {string} contractAddress - of the contract (optionnal). 17 | */ 18 | constructor(web3Provider, contractAddress) { 19 | super(web3Provider, PinakionPOCArtifact, contractAddress) 20 | } 21 | 22 | /** 23 | * Deploy a new instance of PinakionPOC. 24 | * @param {string} account - account of user 25 | * @param {object} web3Provider - web3 provider object 26 | * @param {string} tokenFactoryAddress - The address of the MiniMeTokenFactory contract that will create the Clone token contracts 27 | * @param {string} parentTokenAddress - Address of the parent token, set to 0x0 if it is a new token 28 | * @param {number} parentSnapshotBlock - Block of the parent token that will determine the initial distribution of the clone token, set to 0 if it is a new token 29 | * @param {string} tokenName - Name of the token. 30 | * @param {number} decimalUnits - Number of decimal units token is divisible by. 31 | * @param {string} tokenSymbol - Abreviated symbol to represent the token. 32 | * @param {bool} transfersEnabled - If users can transfer tokens. 33 | * @returns {object} - 'truffle-contract' Object | err The contract object or error deploy. 34 | */ 35 | static deploy = async ( 36 | account = isRequired('account'), 37 | web3Provider = isRequired('web3Provider'), 38 | tokenFactoryAddress = isRequired('tokenFactoryAddress'), 39 | parentTokenAddress = '0x0', 40 | parentSnapshotBlock = 0, 41 | tokenName = 'Pinakion', 42 | decimalUnits = 18, 43 | tokenSymbol = 'PNK', 44 | transfersEnabled = true 45 | ) => { 46 | const contractDeployed = await deployContractAsync( 47 | account, 48 | 0, // value 49 | PinakionPOCArtifact, 50 | web3Provider, 51 | tokenFactoryAddress, // args 52 | parentTokenAddress, 53 | parentSnapshotBlock, 54 | tokenName, 55 | decimalUnits, 56 | tokenSymbol, 57 | transfersEnabled 58 | ) 59 | 60 | return contractDeployed 61 | } 62 | 63 | /** 64 | * Transfer ownership of the PNK contract to the kleros POC contract. 65 | * @param {string} newControllerAddress - Address of the new controller. 66 | * @param {string} controllerAccount - Address of the current controller. (They must sign the tx) 67 | * @returns {object} - The result transaction object. 68 | */ 69 | changeController = async (newControllerAddress, controllerAccount) => { 70 | await this.loadContract() 71 | 72 | try { 73 | return this.contractInstance.changeController(newControllerAddress, { 74 | from: controllerAccount 75 | }) 76 | } catch (err) { 77 | console.error(err) 78 | throw new Error(errorConstants.UNABLE_TO_TRANSFER_OWNERSHIP) 79 | } 80 | } 81 | 82 | /** 83 | * Approve the arbitrator contract to transfer PNK to the contract and call the arbitrators 84 | * receiveApproval() 85 | * @param {string} arbitratorAddress - The address of the arbitrator contract. 86 | * @param {number} amount - The amount of PNK to transfer in wei. 87 | * @param {string} account - The users account. 88 | * @returns {bool} If the transfer succeeded or not 89 | */ 90 | approveAndCall = async (arbitratorAddress, amount, account) => { 91 | await this.loadContract() 92 | 93 | return this.contractInstance.approveAndCall( 94 | arbitratorAddress, 95 | amount, 96 | '0x0', 97 | { 98 | from: account 99 | } 100 | ) 101 | } 102 | 103 | /** 104 | * Get the token balance for an account 105 | * @param {string} account - The users account. 106 | * @returns {number} the amount of tokens. 107 | */ 108 | getTokenBalance = async account => { 109 | await this.loadContract() 110 | 111 | return this.contractInstance.balanceOf(account) 112 | } 113 | 114 | /** 115 | * Fetch the controller of the contract. 116 | * @returns {string} The ETH address of the controller. 117 | */ 118 | getController = async () => { 119 | await this.loadContract() 120 | 121 | return this.contractInstance.controller() 122 | } 123 | } 124 | 125 | export default MiniMePinakion 126 | -------------------------------------------------------------------------------- /src/contracts/implementations/PNK/PinakionPOC.js: -------------------------------------------------------------------------------- 1 | import PinakionPOCArtifact from 'kleros/build/contracts/PinakionPOC' // FIXME: mock 2 | import _ from 'lodash' 3 | 4 | import * as ethConstants from '../../../constants/eth' 5 | import * as errorConstants from '../../../constants/error' 6 | import ContractImplementation from '../../ContractImplementation' 7 | import deployContractAsync from '../../../utils/deployContractAsync' 8 | 9 | /** 10 | * Provides interaction with a PinakionPOC contract deployed on the blockchain. 11 | */ 12 | class PinakionPOC extends ContractImplementation { 13 | /** 14 | * Constructor PinakionPOC. 15 | * @param {object} web3Provider - web3 instance. 16 | * @param {string} contractAddress - of the contract (optionnal). 17 | */ 18 | constructor(web3Provider, contractAddress) { 19 | super(web3Provider, PinakionPOCArtifact, contractAddress) 20 | } 21 | 22 | /** 23 | * Deploy a new instance of PinakionPOC. 24 | * @param {string} account - account of user 25 | * @param {object} web3Provider - web3 provider object 26 | * @returns {object} - 'truffle-contract' Object | err The contract object or error deploy. 27 | */ 28 | static deploy = async (account, web3Provider) => { 29 | const contractDeployed = await deployContractAsync( 30 | account, 31 | ethConstants.TRANSACTION.VALUE, 32 | PinakionPOCArtifact, 33 | web3Provider 34 | ) 35 | 36 | return contractDeployed 37 | } 38 | 39 | /** 40 | * Change the kleros contract variable in instance of PinakionPOC. 41 | * @param {string} klerosAddress - Address of Kleros POC contract. 42 | * @param {string} account - Address of user. 43 | * @returns {object} - The result transaction object. 44 | */ 45 | setKleros = async (klerosAddress, account) => { 46 | await this.loadContract() 47 | 48 | try { 49 | return this.contractInstance.setKleros(klerosAddress, { 50 | from: account 51 | }) 52 | } catch (err) { 53 | console.error(err) 54 | throw new Error(errorConstants.UNABLE_TO_SET_KLEROS) 55 | } 56 | } 57 | 58 | /** 59 | * Transfer ownership of the PNK contract to the kleros POC contract. 60 | * @param {string} klerosAddress - Address of Kleros POC contract. 61 | * @param {string} account - Address of user. 62 | * @returns {object} - The result transaction object. 63 | */ 64 | transferOwnership = async (klerosAddress, account) => { 65 | await this.loadContract() 66 | 67 | try { 68 | return this.contractInstance.transferOwnership(klerosAddress, { 69 | from: account 70 | }) 71 | } catch (err) { 72 | console.error(err) 73 | throw new Error(errorConstants.UNABLE_TO_TRANSFER_OWNERSHIP) 74 | } 75 | } 76 | 77 | /** 78 | * Get data from PNK contract. 79 | * @returns {object} - Data from PNK contract. 80 | */ 81 | getData = async () => { 82 | await this.loadContract() 83 | const [owner, kleros] = await Promise.all([ 84 | this.contractInstance.owner(), 85 | this.contractInstance.kleros() 86 | ]) 87 | 88 | return { 89 | owner, 90 | kleros 91 | } 92 | } 93 | } 94 | 95 | export default PinakionPOC 96 | -------------------------------------------------------------------------------- /src/contracts/implementations/PNK/TokenFactory.js: -------------------------------------------------------------------------------- 1 | import { 2 | MiniMeTokenFactoryAbi, 3 | MiniMeTokenFactoryByteCode 4 | } from 'minimetoken/build/MiniMeToken.sol' 5 | import _ from 'lodash' 6 | 7 | import ContractImplementation from '../../ContractImplementation' 8 | import deployContractAsync from '../../../utils/deployContractAsync' 9 | 10 | /** 11 | * Provides interaction with a PinakionPOC contract deployed on the blockchain. 12 | */ 13 | class TokenFactory extends ContractImplementation { 14 | /** 15 | * Constructor PinakionPOC. 16 | * @param {object} web3Provider - web3 instance. 17 | * @param {string} contractAddress - of the contract (optionnal). 18 | */ 19 | constructor(web3Provider, contractAddress) { 20 | const artifact = { 21 | abi: MiniMeTokenFactoryAbi, 22 | bytecode: MiniMeTokenFactoryByteCode 23 | } 24 | super(web3Provider, artifact, contractAddress) 25 | } 26 | 27 | /** 28 | * Deploy a new instance of PinakionPOC. 29 | * @param {string} account - account of user 30 | * @param {object} web3Provider - web3 provider object 31 | * @returns {object} - 'truffle-contract' Object | err The contract object or error deploy. 32 | */ 33 | static deploy = async (account, web3Provider) => { 34 | const artifact = { 35 | abi: MiniMeTokenFactoryAbi, 36 | bytecode: MiniMeTokenFactoryByteCode 37 | } 38 | 39 | const contractDeployed = await deployContractAsync( 40 | account, // account 41 | 0, // value 42 | artifact, // artifact 43 | web3Provider // web3 44 | ) 45 | 46 | return contractDeployed 47 | } 48 | } 49 | 50 | export default TokenFactory 51 | -------------------------------------------------------------------------------- /src/contracts/implementations/PNK/index.js: -------------------------------------------------------------------------------- 1 | import PinakionPOC from './PinakionPOC' 2 | import MiniMePinakion from './MiniMePinakion' 3 | import TokenFactory from './TokenFactory' 4 | 5 | export { PinakionPOC, MiniMePinakion, TokenFactory } 6 | -------------------------------------------------------------------------------- /src/contracts/implementations/RNG/BlockHashRNG.js: -------------------------------------------------------------------------------- 1 | import BlockHashRNGArtifact from 'kleros-interaction/build/contracts/BlockHashRNGFallback' 2 | import _ from 'lodash' 3 | 4 | import ContractImplementation from '../../ContractImplementation' 5 | import deployContractAsync from '../../../utils/deployContractAsync' 6 | 7 | /** 8 | * Provides interaction with an instance of BlockHashRNG. 9 | */ 10 | class BlockHashRNG extends ContractImplementation { 11 | /** 12 | * Constructor BlockHashRNG. 13 | * @param {object} web3Provider - instance 14 | * @param {string} contractAddress - of the contract (optionnal) 15 | */ 16 | constructor(web3Provider, contractAddress) { 17 | super(web3Provider, BlockHashRNGArtifact, contractAddress) 18 | } 19 | 20 | /** 21 | * BlockHashRNG deploy. 22 | * @param {string} account - users account 23 | * @param {object} web3Provider - web3 provider object 24 | * @returns {object} - truffle-contract Object | err The contract object or error deploy 25 | */ 26 | static deploy = async (account, web3Provider) => { 27 | const contractDeployed = await deployContractAsync( 28 | account, 29 | 0, 30 | BlockHashRNGArtifact, 31 | web3Provider 32 | ) 33 | 34 | return contractDeployed 35 | } 36 | } 37 | 38 | export default BlockHashRNG 39 | -------------------------------------------------------------------------------- /src/contracts/implementations/RNG/index.js: -------------------------------------------------------------------------------- 1 | import BlockHashRNG from './BlockHashRNG' 2 | 3 | export { BlockHashRNG } 4 | -------------------------------------------------------------------------------- /src/contracts/implementations/arbitrable/Arbitrable.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | import ContractImplementation from '../../ContractImplementation' 4 | import EventListener from '../../../utils/EventListener' 5 | import httpRequest from '../../../utils/httpRequest' 6 | 7 | /** 8 | * Provides interaction with standard Arbitrable contracts 9 | */ 10 | class Arbitrable extends ContractImplementation { 11 | /** 12 | * Constructor ArbitrableTransaction. 13 | * @param {object} web3Provider instance. 14 | * @param {object} contractArtifact The abi JSON of the arbitrable contract. 15 | * @param {string} contractAddress of the contract. 16 | */ 17 | constructor(web3Provider, contractArtifact, contractAddress) { 18 | super(web3Provider, contractArtifact, contractAddress) 19 | 20 | this.metaEvidenceCache = {} 21 | } 22 | 23 | /** 24 | * Get the meta evidence for the contract. Arbitrable Transaction can only have 25 | * one meta-evidence that is submitted on contract creation. Look up meta-evidence event 26 | * and make an http request to the resource. 27 | * @returns {object} The metaEvidence object 28 | */ 29 | getMetaEvidence = async (metaEvidenceID = 0) => { 30 | if (this.metaEvidenceCache[metaEvidenceID]) 31 | return this.metaEvidenceCache[metaEvidenceID] 32 | 33 | const metaEvidenceLog = await EventListener.getEventLogs( 34 | this, 35 | 'MetaEvidence', 36 | 0, 37 | 'latest', 38 | { _metaEvidenceID: metaEvidenceID } 39 | ) 40 | 41 | if (!metaEvidenceLog[0]) return {} // NOTE better to throw errors for missing meta-evidence? 42 | 43 | const metaEvidenceUri = metaEvidenceLog[0].args._evidence 44 | // FIXME caching issue need a query param to fetch from AWS 45 | const metaEvidenceResponse = await httpRequest('GET', metaEvidenceUri) 46 | 47 | if (metaEvidenceResponse.status >= 400) 48 | throw new Error(`Unable to fetch meta-evidence at ${metaEvidenceUri}`) 49 | 50 | this.metaEvidenceCache[metaEvidenceID] = 51 | metaEvidenceResponse.body || metaEvidenceResponse 52 | return metaEvidenceResponse.body || metaEvidenceResponse 53 | } 54 | 55 | /** 56 | * Get the evidence submitted in a dispute. 57 | * @returns {object[]} An array of evidence objects. 58 | */ 59 | getEvidence = async (arbitratorAddress, disputeID = 0) => { 60 | const evidenceLogs = await EventListener.getEventLogs( 61 | this, 62 | 'Evidence', 63 | 0, 64 | 'latest', 65 | { _disputeID: disputeID, _arbitrator: arbitratorAddress } 66 | ) 67 | 68 | // TODO verify hash and data are valid if hash exists 69 | return Promise.all( 70 | evidenceLogs.map(async evidenceLog => { 71 | const evidenceURI = evidenceLog.args._evidence 72 | const evidence = await httpRequest('GET', evidenceURI) 73 | const submittedAt = (await this._Web3Wrapper.getBlock( 74 | evidenceLog.blockNumber 75 | )).timestamp 76 | return { 77 | ...evidence.body, 78 | ...{ submittedBy: evidenceLog.args._party, submittedAt } 79 | } 80 | }) 81 | ) 82 | } 83 | 84 | /** 85 | * Fetch all standard contract data. 86 | */ 87 | getContractData = async () => { 88 | await this.loadContract() 89 | 90 | const [metaEvidence] = await Promise.all([this.getMetaEvidence()]) 91 | 92 | return { 93 | metaEvidence 94 | } 95 | } 96 | } 97 | 98 | export default Arbitrable 99 | -------------------------------------------------------------------------------- /src/contracts/implementations/arbitrable/ArbitrablePermissionList.js: -------------------------------------------------------------------------------- 1 | import ArbitrablePermissionListArtifact from 'kleros-interaction/build/contracts/ArbitrablePermissionList' 2 | 3 | import Arbitrable from './Arbitrable' 4 | 5 | /** 6 | * Provides interaction with an Arbitrable Transaction contract deployed on the blockchain. 7 | */ 8 | class ArbitrablePermissionList extends Arbitrable { 9 | /** 10 | * Constructor ArbitrableTransaction. 11 | * @param {object} web3Provider instance 12 | * @param {string} contractAddress of the contract 13 | */ 14 | constructor(web3Provider, contractAddress) { 15 | super(web3Provider, ArbitrablePermissionListArtifact, contractAddress) 16 | } 17 | 18 | /** 19 | * Fetch an item hash by disputeID 20 | * @param {number} disputeID The index of the dispute. 21 | * @returns {string} The item hash. 22 | */ 23 | getItemByDisputeId = async disputeID => { 24 | await this.loadContract() 25 | 26 | return this.contractInstance.disputeIDToItem(disputeID) 27 | } 28 | } 29 | 30 | export default ArbitrablePermissionList 31 | -------------------------------------------------------------------------------- /src/contracts/implementations/arbitrable/ArbitrableTransaction.js: -------------------------------------------------------------------------------- 1 | import arbitrableTransactionArtifact from 'kleros-interaction/build/contracts/ArbitrableTransaction' 2 | import _ from 'lodash' 3 | 4 | import * as ethConstants from '../../../constants/eth' 5 | import * as contractConstants from '../../../constants/contract' 6 | import * as errorConstants from '../../../constants/error' 7 | import deployContractAsync from '../../../utils/deployContractAsync' 8 | 9 | import Arbitrable from './Arbitrable' 10 | 11 | /** 12 | * Provides interaction with an Arbitrable Transaction contract deployed on the blockchain. 13 | */ 14 | class ArbitrableTransaction extends Arbitrable { 15 | /** 16 | * Constructor ArbitrableTransaction. 17 | * @param {object} web3Provider instance 18 | * @param {string} contractAddress of the contract 19 | */ 20 | constructor(web3Provider, contractAddress) { 21 | super(web3Provider, arbitrableTransactionArtifact, contractAddress) 22 | } 23 | 24 | /** 25 | * Deploy ArbitrableTransaction. 26 | * @param {object} account Ethereum account 27 | * @param {number} value funds to be placed in contract 28 | * @param {string} arbitratorAddress The address of the arbitrator contract 29 | * @param {number} timeout Time after which a party automatically loose a dispute. 30 | * @param {string} partyB The recipient of the transaction. 31 | * @param {bytes} arbitratorExtraData Extra data for the arbitrator. (default empty string) 32 | * @param {string} metaEvidenceUri The uri for the metaEvidence 33 | * @param {object} web3Provider web3 provider object 34 | * @returns {object} truffle-contract Object | err The deployed contract or an error 35 | */ 36 | static deploy = async ( 37 | account, 38 | value = ethConstants.TRANSACTION.VALUE, 39 | arbitratorAddress, 40 | timeout, 41 | partyB, 42 | arbitratorExtraData = '', 43 | metaEvidenceUri, 44 | web3Provider 45 | ) => { 46 | const contractDeployed = await deployContractAsync( 47 | account, 48 | value, 49 | arbitrableTransactionArtifact, 50 | web3Provider, 51 | arbitratorAddress, 52 | timeout, 53 | partyB, 54 | arbitratorExtraData, 55 | metaEvidenceUri 56 | ) 57 | 58 | return contractDeployed 59 | } 60 | 61 | /** 62 | * Pay the party B. To be called when the good is delivered or the service rendered. 63 | * @param {string} account - Ethereum address. 64 | * @returns {object} - The result transaction object. 65 | */ 66 | pay = async account => { 67 | await this.loadContract() 68 | 69 | try { 70 | return this.contractInstance.pay({ 71 | from: account, 72 | value: 0 73 | }) 74 | } catch (err) { 75 | console.error(err) 76 | throw new Error(errorConstants.UNABLE_TO_PAY_SELLER) 77 | } 78 | } 79 | 80 | /** 81 | * Pay the arbitration fee to raise a dispute. To be called by the party A. 82 | * @param {string} account - Ethereum address. 83 | * @param {number} arbitrationCost - Arbitration fee in wei. 84 | * @returns {object} - The result transaction object. 85 | */ 86 | payArbitrationFeeByPartyA = async (account, arbitrationCost) => { 87 | await this.loadContract() 88 | 89 | try { 90 | return this.contractInstance.payArbitrationFeeByPartyA({ 91 | from: account, 92 | value: arbitrationCost 93 | }) 94 | } catch (err) { 95 | console.error(err) 96 | throw new Error(errorConstants.UNABLE_TO_PAY_ARBITRATION_FEE) 97 | } 98 | } 99 | 100 | /** 101 | * Pay the arbitration fee to raise a dispute. To be called by the party B. 102 | * @param {string} account Ethereum address. 103 | * @param {number} arbitrationCost Amount to pay the arbitrator. 104 | * @returns {object} - The result transaction object. 105 | */ 106 | payArbitrationFeeByPartyB = async (account, arbitrationCost) => { 107 | await this.loadContract() 108 | 109 | try { 110 | return this.contractInstance.payArbitrationFeeByPartyB({ 111 | from: account, 112 | value: arbitrationCost, 113 | gas: process.env.GAS || undefined 114 | }) 115 | } catch (err) { 116 | console.error(err) 117 | throw new Error(errorConstants.UNABLE_TO_PAY_ARBITRATION_FEE) 118 | } 119 | } 120 | 121 | /** 122 | * Submit evidence. 123 | * @param {string} account ETH address of user. 124 | * @param {string} url A link to an evidence using its URI. 125 | * @returns {string} txHash Hash transaction. 126 | */ 127 | submitEvidence = async (account, url) => { 128 | await this.loadContract() 129 | 130 | const txHashObj = await this.contractInstance.submitEvidence(url, { 131 | from: account, 132 | value: 0 133 | }) 134 | 135 | return txHashObj.tx 136 | } 137 | 138 | /** 139 | * Call by partyA if partyB is timeout 140 | * @param {string} account ETH address of user 141 | * @returns {object} The result transaction object. 142 | */ 143 | callTimeOutPartyA = async account => { 144 | await this.loadContract() 145 | 146 | const status = (await this.contractInstance.status()).toNumber() 147 | const timeout = (await this.contractInstance.timeout()).toNumber() 148 | const lastInteraction = (await this.contractInstance.lastInteraction()).toNumber() 149 | 150 | if (status !== contractConstants.STATUS.WAITING_PARTY_B) { 151 | throw new Error(errorConstants.CONTRACT_IS_NOT_WAITING_ON_OTHER_PARTY) 152 | } else if (Date.now() >= lastInteraction + timeout) { 153 | throw new Error(errorConstants.TIMEOUT_NOT_REACHED) 154 | } 155 | 156 | try { 157 | return this.contractInstance.timeOutByPartyA({ 158 | from: account, 159 | value: 0 160 | }) 161 | } catch (err) { 162 | console.error(err) 163 | throw new Error(errorConstants.UNABLE_TO_CALL_TIMEOUT) 164 | } 165 | } 166 | 167 | /** 168 | * Call by partyB if partyA is timeout. 169 | * @param {string} account - ETH address of user. 170 | * @returns {object} The result transaction object. 171 | */ 172 | callTimeOutPartyB = async account => { 173 | await this.loadContract() 174 | 175 | const status = await this.contractInstance.status() 176 | const timeout = await this.contractInstance.timeout() 177 | const lastInteraction = await this.contractInstance.lastInteraction() 178 | 179 | if (status !== contractConstants.STATUS.WAITING_PARTY_A) { 180 | throw new Error(errorConstants.CONTRACT_IS_NOT_WAITING_ON_OTHER_PARTY) 181 | } else if (Date.now() >= lastInteraction + timeout) { 182 | throw new Error(errorConstants.TIMEOUT_NOT_REACHED) 183 | } 184 | 185 | try { 186 | return this.contractInstance.timeOutByPartyB({ 187 | from: account, 188 | value: 0 189 | }) 190 | } catch (err) { 191 | console.error(err) 192 | throw new Error(errorConstants.UNABLE_TO_CALL_TIMEOUT) 193 | } 194 | } 195 | 196 | /** 197 | * Appeal an appealable ruling. 198 | * @param {string} account Ethereum account (default account[0]). 199 | * @param {number} appealCost Fee for the appeal 200 | * @param {bytes} extraData for the arbitrator appeal procedure. 201 | * @returns {object} - The result transaction object. 202 | */ 203 | appeal = async (account, appealCost, extraData = 0x0) => { 204 | await this.loadContract() 205 | 206 | try { 207 | return this.contractInstance.appeal(extraData, { 208 | from: account, 209 | value: appealCost 210 | }) 211 | } catch (err) { 212 | console.error(err) 213 | throw new Error(errorConstants.UNABLE_TO_RAISE_AN_APPEAL) 214 | } 215 | } 216 | 217 | /** 218 | * Fetch the parties involved in the arbitrable transaction contract. 219 | * @returns {object} returns a mapping of partyA and partyB to ETH addresses. 220 | */ 221 | getParties = async () => { 222 | await this.loadContract() 223 | 224 | const [partyA, partyB] = await Promise.all([ 225 | this.contractInstance.partyA(), 226 | this.contractInstance.partyB() 227 | ]) 228 | 229 | return { 230 | partyA, 231 | partyB 232 | } 233 | } 234 | 235 | /** 236 | * Data of the contract 237 | * @returns {object} Object Data of the contract. 238 | */ 239 | getData = async () => { 240 | await this.loadContract() 241 | 242 | const [ 243 | arbitrator, 244 | extraData, 245 | timeout, 246 | partyA, 247 | partyB, 248 | status, 249 | arbitratorExtraData, 250 | disputeID, 251 | partyAFee, 252 | partyBFee, 253 | lastInteraction, 254 | amount, 255 | evidence, 256 | metaEvidence 257 | ] = await Promise.all([ 258 | this.contractInstance.arbitrator(), 259 | this.contractInstance.arbitratorExtraData(), 260 | // this.contractInstance.hashContract(), 261 | this.contractInstance.timeout(), 262 | this.contractInstance.partyA(), 263 | this.contractInstance.partyB(), 264 | this.contractInstance.status(), 265 | this.contractInstance.arbitratorExtraData(), 266 | this.contractInstance.disputeID(), 267 | this.contractInstance.partyAFee(), 268 | this.contractInstance.partyBFee(), 269 | this.contractInstance.lastInteraction(), 270 | this.contractInstance.amount(), 271 | this.getEvidence(), 272 | this.getMetaEvidence() 273 | ]) 274 | 275 | return { 276 | address: this.getContractAddress(), 277 | arbitrator, 278 | extraData, 279 | timeout: timeout.toNumber(), 280 | partyA, 281 | partyB, 282 | status: status.toNumber(), 283 | arbitratorExtraData, 284 | disputeID: disputeID.toNumber(), 285 | partyAFee, 286 | partyBFee, 287 | lastInteraction: lastInteraction.toNumber(), 288 | amount: amount.toNumber(), 289 | evidence, 290 | metaEvidence 291 | } 292 | } 293 | } 294 | 295 | export default ArbitrableTransaction 296 | -------------------------------------------------------------------------------- /src/contracts/implementations/arbitrable/MultipleArbitrableTransaction.js: -------------------------------------------------------------------------------- 1 | import multipleArbitrableTransactionArtifact from 'kleros-interaction/build/contracts/MultipleArbitrableTransaction' 2 | import _ from 'lodash' 3 | 4 | import * as contractConstants from '../../../constants/contract' 5 | import * as errorConstants from '../../../constants/error' 6 | import deployContractAsync from '../../../utils/deployContractAsync' 7 | 8 | import Arbitrable from './Arbitrable' 9 | 10 | /** 11 | * Provides interaction with an Arbitrable Transaction contract deployed on the blockchain. 12 | */ 13 | class MultipleArbitrableTransaction extends Arbitrable { 14 | /** 15 | * Constructor ArbitrableTransaction. 16 | * @param {object} web3Provider instance 17 | * @param {string} contractAddress of the contract 18 | */ 19 | constructor(web3Provider, contractAddress) { 20 | super(web3Provider, multipleArbitrableTransactionArtifact, contractAddress) 21 | 22 | this.arbitrableTransactionId = null 23 | } 24 | 25 | /** 26 | * Deploy MultipleArbitrableTransaction. 27 | * @param {object} account Ethereum account 28 | * @param {object} web3Provider web3 provider object 29 | * @returns {object} truffle-contract Object | err The deployed contract or an error 30 | */ 31 | static deploy = async (account, web3Provider) => { 32 | const contractDeployed = await deployContractAsync( 33 | account, 34 | 0, 35 | multipleArbitrableTransactionArtifact, 36 | web3Provider 37 | ) 38 | 39 | return contractDeployed 40 | } 41 | 42 | /** 43 | * Create MultipleArbitrableTransaction. 44 | * @param {object} account Ethereum account 45 | * @param {string} arbitratorAddress The address of the arbitrator contract 46 | * @param {object} seller Seller Ethereum account 47 | * @param {number} value funds to be placed in contract 48 | * @param {number} timeout Time (seconds) after which a party automatically loose a dispute. (default 3600) 49 | * @param {bytes} arbitratorExtraData Extra data for the arbitrator. (default empty string) 50 | * @param {string} metaEvidenceUri Uri meta-evidence. (default empty string) 51 | * @returns {object} truffle-contract Object | err The deployed contract or an error 52 | */ 53 | createArbitrableTransaction = async ( 54 | account, 55 | arbitratorAddress, 56 | seller, 57 | value, 58 | timeout = 3600, 59 | arbitratorExtraData = 0x0, 60 | metaEvidenceUri 61 | ) => { 62 | await this.loadContract() 63 | 64 | try { 65 | return this.contractInstance.createTransaction( 66 | arbitratorAddress, 67 | timeout, 68 | seller, 69 | arbitratorExtraData, 70 | metaEvidenceUri, 71 | { 72 | from: account, 73 | value: value, 74 | gas: process.env.GAS || undefined 75 | } 76 | ) 77 | } catch (err) { 78 | console.error(err) 79 | throw new Error(errorConstants.UNABLE_TO_CREATE_TRANSACTION) 80 | } 81 | } 82 | 83 | /** 84 | * Pay the seller. To be called when the good is delivered or the service rendered. 85 | * @param {string} account - Ethereum account. 86 | * @param {number} arbitrableTransactionId - The index of the transaction. 87 | * @param {amount} amount - Part or all of the amount of the good or the service. 88 | * @returns {object} - The result transaction object. 89 | */ 90 | pay = async (account, arbitrableTransactionId, amount) => { 91 | await this.loadContract() 92 | 93 | try { 94 | return this.contractInstance.pay( 95 | arbitrableTransactionId, 96 | this._Web3Wrapper.toWei(amount, 'ether'), 97 | { 98 | from: account, 99 | value: 0 100 | } 101 | ) 102 | } catch (err) { 103 | console.error(err) 104 | throw new Error(errorConstants.UNABLE_TO_PAY_SELLER) 105 | } 106 | } 107 | 108 | /** 109 | * Reimburse the seller. To be called when the good is not delivered or the service rendered. 110 | * @param {string} account - Ethereum account. 111 | * @param {number} arbitrableTransactionId - The index of the transaction. 112 | * @param {amount} amount - Part or all of the amount of the good or the service. 113 | * @returns {object} - The result transaction object. 114 | */ 115 | reimburse = async ( 116 | account, 117 | arbitrableTransactionId, 118 | amount 119 | ) => { 120 | await this.loadContract() 121 | 122 | try { 123 | return this.contractInstance.reimburse( 124 | arbitrableTransactionId, 125 | this._Web3Wrapper.toWei(amount, 'ether'), 126 | { 127 | from: account, 128 | value: 0 129 | } 130 | ) 131 | } catch (err) { 132 | console.error(err) 133 | throw new Error(errorConstants.UNABLE_TO_REIMBURSE_BUYER) 134 | } 135 | } 136 | 137 | /** 138 | * Pay the arbitration fee to raise a dispute. To be called by the buyer. 139 | * @param {string} account - Ethereum account. 140 | * @param {number} arbitrableTransactionId - The index of the transaction. 141 | * @param {number} arbitrationCost - Arbitration cost. 142 | * @returns {object} - The result transaction object. 143 | */ 144 | payArbitrationFeeByBuyer = async ( 145 | account, 146 | arbitrableTransactionId, 147 | arbitrationCost 148 | ) => { 149 | await this.loadContract() 150 | 151 | try { 152 | return this.contractInstance.payArbitrationFeeByBuyer( 153 | arbitrableTransactionId, 154 | { 155 | from: account, 156 | value: arbitrationCost 157 | } 158 | ) 159 | } catch (err) { 160 | console.error(err) 161 | throw new Error(errorConstants.UNABLE_TO_PAY_ARBITRATION_FEE) 162 | } 163 | } 164 | 165 | /** 166 | * Pay the arbitration fee to raise a dispute. To be called by the seller. 167 | * @param {string} account Ethereum account. 168 | * @param {number} arbitrableTransactionId - The index of the transaction. 169 | * @param {number} arbitrationCost - Arbitration cost. 170 | * @returns {object} - The result transaction object. 171 | */ 172 | payArbitrationFeeBySeller = async ( 173 | account, 174 | arbitrableTransactionId, 175 | arbitrationCost 176 | ) => { 177 | await this.loadContract() 178 | 179 | try { 180 | return this.contractInstance.payArbitrationFeeBySeller( 181 | arbitrableTransactionId, 182 | { 183 | from: account, 184 | value: arbitrationCost, 185 | gas: process.env.GAS || undefined 186 | } 187 | ) 188 | } catch (err) { 189 | console.error(err) 190 | throw new Error(errorConstants.UNABLE_TO_PAY_ARBITRATION_FEE) 191 | } 192 | } 193 | 194 | /** 195 | * Submit evidence. 196 | * @param {string} account ETH address of user. 197 | * @param {number} arbitrableTransactionId - The index of the transaction. 198 | * @param {string} url A link to an evidence using its URI. 199 | * @returns {string} txHash Hash transaction. 200 | */ 201 | submitEvidence = async ( 202 | account, 203 | arbitrableTransactionId, 204 | url 205 | ) => { 206 | await this.loadContract() 207 | 208 | const txHashObj = await this.contractInstance.submitEvidence( 209 | arbitrableTransactionId, 210 | url, 211 | { 212 | from: account, 213 | value: 0 214 | } 215 | ) 216 | 217 | return txHashObj.tx 218 | } 219 | 220 | /** 221 | * Call by buyer if seller is timeout 222 | * @param {string} account ETH address of user 223 | * @param {number} arbitrableTransactionId - The index of the transaction. 224 | * @returns {object} The result transaction object. 225 | */ 226 | callTimeOutBuyer = async ( 227 | account, 228 | arbitrableTransactionId 229 | ) => { 230 | await this.loadContract() 231 | 232 | const transactionArbitrableData = await this.getData( 233 | arbitrableTransactionId 234 | ) 235 | 236 | const status = transactionArbitrableData.status 237 | const timeout = transactionArbitrableData.timeout 238 | const lastInteraction = transactionArbitrableData.lastInteraction 239 | 240 | if (status !== contractConstants.STATUS.WAITING_SELLER) { 241 | throw new Error(errorConstants.CONTRACT_IS_NOT_WAITING_ON_OTHER_PARTY) 242 | } else if (Math.trunc(Date.now() / 1000) <= lastInteraction + timeout) { 243 | throw new Error(errorConstants.TIMEOUT_NOT_REACHED) 244 | } 245 | 246 | try { 247 | return this.contractInstance.timeOutByBuyer(arbitrableTransactionId, { 248 | from: account, 249 | value: 0 250 | }) 251 | } catch (err) { 252 | console.error(err) 253 | throw new Error(errorConstants.UNABLE_TO_CALL_TIMEOUT) 254 | } 255 | } 256 | 257 | /** 258 | * Call by seller if buyer is timeout. 259 | * @param {string} account - ETH address of user. 260 | * @param {number} arbitrableTransactionId - The index of the transaction. 261 | * @param {string} contractAddress - ETH address of contract. 262 | * @returns {object} The result transaction object. 263 | */ 264 | callTimeOutSeller = async ( 265 | account, 266 | arbitrableTransactionId 267 | ) => { 268 | await this.loadContract() 269 | 270 | const status = await this.contractInstance.status() 271 | const timeout = await this.contractInstance.timeout() 272 | const lastInteraction = await this.contractInstance.lastInteraction() 273 | 274 | if (status !== contractConstants.STATUS.WAITING_BUYER) { 275 | throw new Error(errorConstants.CONTRACT_IS_NOT_WAITING_ON_OTHER_PARTY) 276 | } else if (Date.now() >= lastInteraction + timeout) { 277 | throw new Error(errorConstants.TIMEOUT_NOT_REACHED) 278 | } 279 | 280 | try { 281 | return this.contractInstance.timeOutBySeller(arbitrableTransactionId, { 282 | from: account, 283 | value: 0 284 | }) 285 | } catch (err) { 286 | console.error(err) 287 | throw new Error(errorConstants.UNABLE_TO_CALL_TIMEOUT) 288 | } 289 | } 290 | 291 | /** 292 | * Appeal an appealable ruling. 293 | * @param {string} account Ethereum account. 294 | * @param {number} arbitrableTransactionId - The index of the transaction. 295 | * @param {bytes} extraData for the arbitrator appeal procedure. 296 | * @param {number} appealCost Amount to pay the arbitrator. (default 0.35 ether). 297 | * @returns {object} - The result transaction object. 298 | */ 299 | appeal = async ( 300 | account, 301 | arbitrableTransactionId, 302 | extraData = 0x0, 303 | appealCost = 0.3 304 | ) => { 305 | await this.loadContract() 306 | 307 | try { 308 | return this.contractInstance.appeal(arbitrableTransactionId, extraData, { 309 | from: account, 310 | value: appealCost 311 | }) 312 | } catch (err) { 313 | console.error(err) 314 | throw new Error(errorConstants.UNABLE_TO_RAISE_AN_APPEAL) 315 | } 316 | } 317 | 318 | /** 319 | * Set the arbitrable transaction id 320 | * @param {number} arbitrableTransactionId - The index of the transaction. 321 | * @returns {object} Object Data of the contract. 322 | */ 323 | setArbitrableTransactionId = arbitrableTransactionId => 324 | (this.arbitrableTransactionId = arbitrableTransactionId) 325 | 326 | /** 327 | * Fetch the parties involved in the arbitrable transaction contract. 328 | * @returns {object} returns a mapping of partyA and partyB to ETH addresses. 329 | */ 330 | getParties = async (arbitrableTransactionId) => { 331 | await this.loadContract() 332 | 333 | const arbitrableTransaction = await this.contractInstance.transactions( 334 | arbitrableTransactionId 335 | ) 336 | 337 | return { 338 | seller: arbitrableTransaction[0], 339 | buyer: arbitrableTransaction[1] 340 | } 341 | } 342 | 343 | /** 344 | * Data of the contract 345 | * @param {number} arbitrableTransactionId - The index of the transaction. 346 | * @returns {object} Object Data of the contract. 347 | */ 348 | getData = async arbitrableTransactionId => { 349 | await this.loadContract() 350 | 351 | const arbitrableTransaction = await this.contractInstance.transactions( 352 | arbitrableTransactionId 353 | ) 354 | 355 | return { 356 | seller: arbitrableTransaction[0], 357 | buyer: arbitrableTransaction[1], 358 | amount: this._Web3Wrapper.fromWei( 359 | arbitrableTransaction[2].toNumber(), 360 | 'ether' 361 | ), 362 | timeout: arbitrableTransaction[3].toNumber(), 363 | disputeId: arbitrableTransaction[4].toNumber(), 364 | arbitrator: arbitrableTransaction[5], 365 | arbitratorExtraData: arbitrableTransaction[6], 366 | sellerFee: this._Web3Wrapper.fromWei(arbitrableTransaction[7], 'ether'), 367 | buyerFee: this._Web3Wrapper.fromWei(arbitrableTransaction[8], 'ether'), 368 | lastInteraction: arbitrableTransaction[9].toNumber(), 369 | status: arbitrableTransaction[10].toNumber(), 370 | metaEvidence: await this.getMetaEvidence(arbitrableTransactionId), 371 | evidence: await this.getEvidence( 372 | arbitrableTransaction[5], 373 | arbitrableTransaction[4].toNumber() 374 | ) 375 | } 376 | } 377 | } 378 | 379 | export default MultipleArbitrableTransaction 380 | -------------------------------------------------------------------------------- /src/contracts/implementations/arbitrable/index.js: -------------------------------------------------------------------------------- 1 | import ArbitrableTransaction from './ArbitrableTransaction' 2 | import MultipleArbitrableTransaction from './MultipleArbitrableTransaction' 3 | import ArbitrablePermissionList from './ArbitrablePermissionList' 4 | 5 | export { 6 | ArbitrableTransaction, 7 | MultipleArbitrableTransaction, 8 | ArbitrablePermissionList 9 | } 10 | -------------------------------------------------------------------------------- /src/contracts/implementations/arbitrator/KlerosPOC.js: -------------------------------------------------------------------------------- 1 | import klerosPOCArtifact from 'kleros/build/contracts/KlerosPOC' 2 | import _ from 'lodash' 3 | 4 | import * as ethConstants from '../../../constants/eth' 5 | import deployContractAsync from '../../../utils/deployContractAsync' 6 | 7 | import Kleros from './Kleros' 8 | 9 | /** 10 | * Provides interaction with a KlerosPOC contract on the blockchain. 11 | */ 12 | class KlerosPOC extends Kleros { 13 | /** 14 | * Create new KlerosPOC Implementation. 15 | * @param {object} web3Provider - web3 instance. 16 | * @param {string} contractAddress - Address of the KlerosPOC contract. 17 | */ 18 | constructor(web3Provider, contractAddress) { 19 | super(web3Provider, contractAddress, klerosPOCArtifact) 20 | } 21 | 22 | /** 23 | * STATIC: Deploy a KlerosPOC contract on the blockchain. 24 | * @param {string} rngAddress address of random number generator contract 25 | * @param {string} pnkAddress address of pinakion contract 26 | * @param {number[]} timesPerPeriod array of 5 ints indicating the time limit for each period of contract 27 | * @param {string} account address of user 28 | * @param {number} value amout of eth to send to contract 29 | * @param {object} web3Provider web3 provider object NOTE: NOT Kleros Web3Wrapper 30 | * @returns {object} truffle-contract Object | err The contract object or error deploy 31 | */ 32 | static deploy = async ( 33 | rngAddress, 34 | pnkAddress, 35 | timesPerPeriod = [300, 0, 300, 300, 300], 36 | account, 37 | value = ethConstants.TRANSACTION.VALUE, 38 | web3Provider 39 | ) => { 40 | const contractDeployed = await deployContractAsync( 41 | account, 42 | value, 43 | klerosPOCArtifact, 44 | web3Provider, 45 | pnkAddress, 46 | rngAddress, 47 | timesPerPeriod 48 | ) 49 | 50 | return contractDeployed 51 | } 52 | 53 | /** 54 | * Purchase PNK. 55 | * @param {string} amount - The amount of pinakion to buy in wei. 56 | * @param {string} account - The address of the user. 57 | * @returns {object} - The result transaction object. 58 | */ 59 | buyPNK = async (amount, account) => { 60 | await this.loadContract() 61 | return this.contractInstance.buyPinakion({ 62 | from: account, 63 | value: amount, 64 | gas: process.env.GAS || undefined 65 | }) 66 | } 67 | } 68 | 69 | export default KlerosPOC 70 | -------------------------------------------------------------------------------- /src/contracts/implementations/arbitrator/index.js: -------------------------------------------------------------------------------- 1 | import KlerosPOC from './KlerosPOC' 2 | 3 | export { KlerosPOC } 4 | -------------------------------------------------------------------------------- /src/contracts/implementations/index.js: -------------------------------------------------------------------------------- 1 | import * as arbitrator from './arbitrator' 2 | import * as arbitrable from './arbitrable' 3 | import * as PNK from './PNK' 4 | import * as RNG from './RNG' 5 | 6 | export { arbitrator, arbitrable, PNK, RNG } 7 | -------------------------------------------------------------------------------- /src/contracts/index.js: -------------------------------------------------------------------------------- 1 | import * as abstractions from './abstractions' 2 | import * as implementations from './implementations' 3 | 4 | export { abstractions, implementations } 5 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Kleros from './kleros.js' 2 | 3 | export { Kleros } 4 | -------------------------------------------------------------------------------- /src/kleros.js: -------------------------------------------------------------------------------- 1 | import isRequired from './utils/isRequired' 2 | import Web3Wrapper from './utils/Web3Wrapper' 3 | import StoreProviderWrapper from './utils/StoreProviderWrapper' 4 | import * as contracts from './contracts' 5 | import * as resources from './resources' 6 | import EventListener from './utils/EventListener' 7 | 8 | /** 9 | * The Kleros Api provides access to the full suite of functionality. It will initialize 10 | * contract instances for you when possible and creates an object that you can use to 11 | * call all of the other api modules. If you are only going to be interacting with 12 | * specific apis, or you don't want certain functionality such as the off chain store, 13 | * you might find it easier to initialze a specific instance of the api you want. 14 | */ 15 | class Kleros { 16 | web3Wrapper = {} 17 | 18 | storeWrapper = {} 19 | 20 | eventListener = null 21 | 22 | /** 23 | * Instantiates a new Kelros instance that provides the public interface 24 | * to Kleros contracts and library. All params are required. To use an individual 25 | * portion of the API import a class and initialize it yourself. 26 | * @param {string} ethereumProvider - The Web3.js Provider instance you would like the 27 | * Kleros.js library to use for interacting with the 28 | * Ethereum network. 29 | * @param {string} storeUri - The storage provider uri used to 30 | * get metadata from the cloud for the UI. e.g. Kleros-Store, 31 | * IPFS, Swarm etc. 32 | * @param {string} arbitratorAddress - Address of the arbitrator contract we should 33 | * use when initializing KlerosPOC 34 | * @param {string} arbitrableContractAddress - Address of the arbitrator contract we should 35 | * use when initializing KlerosPOC 36 | */ 37 | constructor( 38 | ethereumProvider = isRequired('ethereumProvider'), 39 | storeUri = isRequired('storeUri'), 40 | arbitratorAddress, 41 | arbitrableContractAddress 42 | ) { 43 | /** 44 | * We need a set of implementations that we expose to the outside api and a set we use 45 | * internally. This is because the implementation class itself sets the contract instance 46 | * and we don't want race conditions between external and internal calls. 47 | * 48 | * FIXME this is an ugly way of doing this and still has some race conditions. See issue #138. 49 | */ 50 | // EXTERNAL 51 | const _klerosPOC = new contracts.implementations.arbitrator.KlerosPOC( 52 | ethereumProvider, 53 | arbitratorAddress 54 | ) 55 | const _arbitrableTransaction = new contracts.implementations.arbitrable.MultipleArbitrableTransaction( 56 | ethereumProvider, 57 | arbitrableContractAddress 58 | ) 59 | // INTERNAL 60 | const _klerosPOCInternal = new contracts.implementations.arbitrator.KlerosPOC( 61 | ethereumProvider, 62 | arbitratorAddress 63 | ) 64 | const _arbitrableTransactionInternal = new contracts.implementations.arbitrable.MultipleArbitrableTransaction( 65 | ethereumProvider, 66 | arbitrableContractAddress 67 | ) 68 | 69 | // **************************** // 70 | // * INITIALIZED CLASSES * // 71 | // **************************** // 72 | // KLEROS WRAPPERS 73 | this.web3Wrapper = new Web3Wrapper(ethereumProvider) 74 | this.storeWrapper = new StoreProviderWrapper(storeUri) 75 | // ARBITRATOR 76 | this.arbitrator = new contracts.abstractions.Arbitrator( 77 | _klerosPOC, 78 | this.storeWrapper 79 | ) 80 | // ARBITRABLE CONTRACTS 81 | this.arbitrable = new contracts.abstractions.Arbitrable( 82 | _arbitrableTransaction, 83 | this.storeWrapper 84 | ) 85 | 86 | // Create new instance of arbitator and arbitrable for behind the scene task runners to use 87 | const _arbitrator = new contracts.abstractions.Arbitrator( 88 | _klerosPOCInternal, 89 | this.storeWrapper 90 | ) 91 | const _arbitrable = new contracts.abstractions.Arbitrable( 92 | _arbitrableTransactionInternal, 93 | this.storeWrapper 94 | ) 95 | // DISPUTES 96 | this.disputes = new resources.Disputes( 97 | _arbitrator, 98 | _arbitrable, 99 | this.storeWrapper 100 | ) 101 | // NOTIFICATIONS 102 | this.notifications = new resources.Notifications( 103 | _arbitrator, 104 | _arbitrable, 105 | this.storeWrapper 106 | ) 107 | // AUTH 108 | this.auth = new resources.Auth(this.web3Wrapper, this.storeWrapper) 109 | } 110 | 111 | /** 112 | * Set a new arbitrable contract for Kleros instance of arbitrableContracts 113 | * @param {string} contractAddress - Address of arbitrable contract 114 | */ 115 | setArbitrableContractAddress = contractAddress => { 116 | this.arbitrable.setContractInstance(contractAddress) 117 | } 118 | 119 | /** 120 | * Bootstraps an EventListener and adds all Kleros handlers for event logs. Use 121 | * this if you want to watch the chain for notifications, or are using the off chain 122 | * store for metadata. 123 | * @param {string} account Address of the user 124 | * @param {function} callback The function to be called once a notification 125 | * @returns {Promise} the watcher promise so that user can wait for event watcher to start before taking other actions. 126 | */ 127 | watchForEvents = async ( 128 | account, 129 | callback // for notification callback 130 | ) => { 131 | // stop current event listeners 132 | if (this.eventListener) { 133 | this.eventListener.stopWatchingForEvents() 134 | } 135 | // reinitialize with current arbitrator contract instance 136 | this.eventListener = new EventListener([this.arbitrator]) 137 | // add handlers for notifications 138 | this.notifications.registerArbitratorNotifications( 139 | account, 140 | this.eventListener, 141 | callback 142 | ) 143 | // add handlers for event driven store updates 144 | this.disputes.registerStoreUpdateEventListeners(account, this.eventListener) 145 | // fetch last block for user 146 | const fromBlock = await this.storeWrapper.getLastBlock(account) 147 | // start event listener 148 | return this.eventListener.watchForEvents(fromBlock) 149 | } 150 | 151 | /** 152 | * Stop watching for events on the Arbitrator initialized in the Kleros Instance. 153 | */ 154 | stopWatchingForEvents = () => { 155 | if (this.eventListener) 156 | this.eventListener.removeContractInstance(this.arbitrator) 157 | } 158 | 159 | /** 160 | * Sets the store provider uri for all higher level apis in the Kleros Instance. 161 | * @param {string} storeUri - The URI that the store provider will use 162 | */ 163 | setStoreProvider = storeUri => { 164 | this.storeWrapper = new StoreProviderWrapper(storeUri) 165 | 166 | this.disputes.setStoreProviderInstance(this.storeWrapper) 167 | this.arbitrable.setStoreProviderInstance(this.storeWrapper) 168 | this.arbitrator.setStoreProviderInstance(this.storeWrapper) 169 | this.notifications.setStoreProviderInstance(this.storeWrapper) 170 | this.auth.setStoreProviderInstance(this.storeWrapper) 171 | } 172 | } 173 | 174 | export default Kleros 175 | -------------------------------------------------------------------------------- /src/resources/Auth.js: -------------------------------------------------------------------------------- 1 | import Personal from 'web3-eth-personal' 2 | 3 | import isRequired from '../utils/isRequired' 4 | import { UNABLE_TO_SIGN_TOKEN } from '../constants/error' 5 | 6 | class Auth { 7 | constructor( 8 | web3Wrapper = isRequired('web3Wrapper'), 9 | storeProviderInstance = isRequired('storeProviderInstance') 10 | ) { 11 | this._Web3Wrapper = web3Wrapper 12 | this._StoreProviderInstance = storeProviderInstance 13 | } 14 | 15 | /** 16 | * Set store provider instance. 17 | * @param {object} storeProviderInstance - instance of store provider wrapper. 18 | */ 19 | setStoreProviderInstance = storeProviderInstance => { 20 | this._StoreProviderInstance = storeProviderInstance 21 | } 22 | 23 | /** 24 | * Set an auth token in the Store Provider. Call this instead of validateNewAuthToken 25 | * if you have a signed token saved. 26 | * @param {string} token - Hex representation of signed token. 27 | */ 28 | setAuthToken = token => { 29 | this._StoreProviderInstance.setAuthToken(token) 30 | } 31 | 32 | /** 33 | * Validate a new auth token. Note if you validate a new token old signed tokens 34 | * will not longer be valid regardless of their expiration time. 35 | * @param {string} userAddress - Address of the user profile 36 | * @returns {string} Signed token for future use. 37 | */ 38 | getNewAuthToken = async userAddress => { 39 | const unsignedToken = (await this._StoreProviderInstance.newAuthToken( 40 | userAddress 41 | )).unsignedToken 42 | 43 | const signedToken = await this.signMessage(userAddress, unsignedToken) 44 | // make sure token is valid 45 | if (!await this.validateAuthToken(userAddress, signedToken)) 46 | throw new Error(UNABLE_TO_SIGN_TOKEN) 47 | 48 | return signedToken 49 | } 50 | 51 | /** 52 | * Sign a message with your private key. Uses web3 1.0 personal sign 53 | * @param {string} userAddress - The address with which we want to sign the message 54 | * @param {string} data - Hex encoded data to sign 55 | * @returns {string} signed data 56 | */ 57 | signMessage = (userAddress, data) => { 58 | const ethPersonal = new Personal(this._Web3Wrapper.getProvider()) 59 | return new Promise((resolve, reject) => { 60 | ethPersonal.sign(data, userAddress, (error, result) => { 61 | if (error) reject(error) 62 | 63 | resolve(result) 64 | }) 65 | }) 66 | } 67 | 68 | /** 69 | * Validate an auth token. 70 | * @param {string} userAddress - The address of the user. 71 | * @param {string} authToken - Token to check. 72 | * @returns {Promise} resolves to True if token is valid. 73 | */ 74 | validateAuthToken = (userAddress, authToken) => 75 | this._StoreProviderInstance.isTokenValid(userAddress, authToken) 76 | } 77 | 78 | export default Auth 79 | -------------------------------------------------------------------------------- /src/resources/Disputes.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | import * as arbitratorConstants from '../constants/arbitrator' 4 | import * as disputeConstants from '../constants/dispute' 5 | import isRequired from '../utils/isRequired' 6 | 7 | /** 8 | * Disputes API. Provides cross arbitrator and arbitrable contracts functionality. 9 | * Requires Store Provider to be set. 10 | */ 11 | class Disputes { 12 | constructor( 13 | arbitratorInstance = isRequired('arbitratorInstance'), 14 | arbitrableInstance = isRequired('arbitrableInstance'), 15 | storeProviderInstance = isRequired('storeProviderInstance') 16 | ) { 17 | this._ArbitratorInstance = arbitratorInstance 18 | this._ArbitrableInstance = arbitrableInstance 19 | this._StoreProviderInstance = storeProviderInstance 20 | this.disputeCache = {} 21 | } 22 | /** 23 | * Set arbitrator instance. 24 | * @param {object} arbitratorInstance - instance of an arbitrator contract. 25 | */ 26 | setArbitratorInstance = arbitratorInstance => { 27 | this._ArbitratorInstance = arbitratorInstance 28 | } 29 | /** 30 | * Set arbitrable instance. 31 | * @param {object} arbitrableInstance - instance of an arbitrable contract. 32 | */ 33 | setArbitrableInstance = arbitrableInstance => { 34 | this._ArbitrableInstance = arbitrableInstance 35 | } 36 | /** 37 | * Set store provider instance. 38 | * @param {object} storeProviderInstance - instance of store provider wrapper. 39 | */ 40 | setStoreProviderInstance = storeProviderInstance => { 41 | this._StoreProviderInstance = storeProviderInstance 42 | } 43 | 44 | // **************************** // 45 | // * Events * // 46 | // **************************** // 47 | 48 | /** 49 | * Method to register all dispute handlers to an EventListener. 50 | * @param {string} account - The address of the user. 51 | * @param {object} eventListener - The EventListener instance. See utils/EventListener.js. 52 | */ 53 | registerStoreUpdateEventListeners = ( 54 | account = isRequired('account'), 55 | eventListener = isRequired('eventListener') 56 | ) => { 57 | const eventHandlerMap = { 58 | DisputeCreation: [this._storeNewDisputeHandler] 59 | } 60 | 61 | for (let event in eventHandlerMap) { 62 | if (eventHandlerMap.hasOwnProperty(event)) { 63 | eventHandlerMap[event].forEach(handler => { 64 | eventListener.addEventHandler(this._ArbitratorInstance, event, args => 65 | handler(args, account) 66 | ) 67 | }) 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * Event listener handler that stores dispute in store upon creation 74 | * @param {string} event - The event log. 75 | * @param {string} account - Account of user to update store data for 76 | */ 77 | _storeNewDisputeHandler = async (event, account) => { 78 | // There is no need to handle this event if we are not using the store 79 | const disputeID = event.args._disputeID.toNumber() 80 | 81 | const arbitratorAddress = this._ArbitratorInstance.getContractAddress() 82 | // arbitrator data 83 | const disputeData = await this._ArbitratorInstance.getDispute(disputeID) 84 | // arbitrable contract data 85 | await this._ArbitrableInstance.setContractInstance( 86 | disputeData.arbitrableContractAddress 87 | ) 88 | 89 | const arbitrableContractData = await this._ArbitrableInstance.getData( 90 | account 91 | ) 92 | 93 | if ( 94 | account === arbitrableContractData.partyA || 95 | account === arbitrableContractData.partyB 96 | ) { 97 | await this._StoreProviderInstance.updateDisputeProfile( 98 | account, 99 | arbitratorAddress, 100 | disputeID, 101 | { 102 | contractAddress: disputeData.arbitrableContractAddress, 103 | partyA: arbitrableContractData.partyA, 104 | partyB: arbitrableContractData.partyB, 105 | blockNumber: event.blockNumber 106 | } 107 | ) 108 | } 109 | } 110 | 111 | // **************************** // 112 | // * Internal * // 113 | // **************************** // 114 | 115 | /** 116 | * Add new data to the cache 117 | * @param {number} disputeID - The index of the dispute. Used as the key in cache 118 | * @param {object} newCacheData - Freeform data to cache. Will overwrite data with the same keys. 119 | */ 120 | _updateDisputeCache = (disputeID, newCacheData = {}) => { 121 | this.disputeCache[disputeID] = { 122 | ...this.disputeCache[disputeID], 123 | ...newCacheData 124 | } 125 | } 126 | 127 | /** 128 | * Get the block at which a dispute was created. Used to find timestamps for dispute. 129 | * The start block is cached after it has been found once as it will never change. 130 | * @param {number} disputeID - The index of the dispute. 131 | * @param {string} account - The address of the user. 132 | * @returns {number} The block number that the dispute was created. 133 | */ 134 | _getDisputeStartBlock = async (disputeID, account) => { 135 | const cachedDispute = this.disputeCache[disputeID] 136 | if (cachedDispute && cachedDispute.startBlock) 137 | return cachedDispute.startBlock 138 | 139 | const arbitratorAddress = this._ArbitratorInstance.getContractAddress() 140 | 141 | let blockNumber 142 | 143 | try { 144 | const userData = await this._StoreProviderInstance.getDispute( 145 | account, 146 | arbitratorAddress, 147 | disputeID 148 | ) 149 | blockNumber = userData.blockNumber 150 | // eslint-disable-next-line no-unused-vars 151 | } catch (err) {} 152 | // if block number is not stored we can look it up 153 | if (!blockNumber) { 154 | // Fetching a dispute will fail if it hasn't been added to the store yet. This is ok, we can just not return store data 155 | // see if we can get dispute start block from events 156 | const disputeCreationEvent = await this._ArbitratorInstance.getDisputeCreationEvent( 157 | disputeID 158 | ) 159 | if (disputeCreationEvent) { 160 | blockNumber = disputeCreationEvent.blockNumber 161 | } 162 | } 163 | 164 | // cache start block for dispute 165 | this._updateDisputeCache(disputeID, { startBlock: blockNumber }) 166 | return blockNumber 167 | } 168 | 169 | // **************************** // 170 | // * Public * // 171 | // **************************** // 172 | /** 173 | * Fetch the shared dispute data from the store. 174 | * @param {string} account - The users account. 175 | * @param {string} disputeID - The index of the dispute. 176 | * @returns {Promise} The dispute data in the store. 177 | */ 178 | getDisputeFromStore = async (account, disputeID) => { 179 | const arbitratorAddress = this._ArbitratorInstance.getContractAddress() 180 | return this._StoreProviderInstance.getDispute( 181 | account, 182 | arbitratorAddress, 183 | disputeID 184 | ) 185 | } 186 | 187 | /** 188 | * Get the dispute deadline for the appeal. 189 | * @param {number} disputeID - The index of the dispute. 190 | * @param {number} appeal - The appeal number. 0 if there have been no appeals. 191 | * @returns {number} timestamp of the appeal 192 | */ 193 | getDisputeDeadline = async (disputeID, appeal = 0) => { 194 | const cachedDispute = this.disputeCache[disputeID] || {} 195 | if (cachedDispute.appealDeadlines && cachedDispute.appealDeadlines[appeal]) 196 | return cachedDispute.appealDeadlines[appeal] 197 | 198 | const dispute = await this._ArbitratorInstance.getDispute(disputeID) 199 | 200 | const deadlineTimestamp = await this._ArbitratorInstance.getDisputeDeadlineTimestamp( 201 | dispute.firstSession + appeal 202 | ) 203 | 204 | if (deadlineTimestamp) { 205 | const currentDeadlines = cachedDispute.appealDeadlines || [] 206 | currentDeadlines[appeal] = deadlineTimestamp 207 | // cache the deadline for the appeal 208 | this._updateDisputeCache(disputeID, { 209 | appealDeadlines: currentDeadlines 210 | }) 211 | } 212 | 213 | return deadlineTimestamp 214 | } 215 | 216 | /** 217 | * Get the timestamp on when the dispute's ruling was finalized. 218 | * @param {number} disputeID - The index of the dispute. 219 | * @param {number} appeal - The appeal number. 0 if there have been no appeals. 220 | * @returns {number} timestamp of the appeal 221 | */ 222 | getAppealRuledAt = async (disputeID, appeal = 0) => { 223 | const cachedDispute = this.disputeCache[disputeID] || {} 224 | if ( 225 | cachedDispute.appealRuledAt && 226 | cachedDispute.appealRuledAt[appeal] 227 | ) 228 | return cachedDispute.appealRuledAt[appeal] 229 | 230 | const dispute = await this._ArbitratorInstance.getDispute(disputeID) 231 | const appealRuledAtTimestamp = await this._ArbitratorInstance.getAppealRuledAtTimestamp( 232 | dispute.firstSession + appeal 233 | ) 234 | 235 | // cache the deadline for the appeal 236 | if (appealRuledAtTimestamp) { 237 | const currentRuledAt = cachedDispute.appealRuledAt || [] 238 | currentRuledAt[appeal] = appealRuledAtTimestamp 239 | this._updateDisputeCache(disputeID, { 240 | appealRuledAt: currentRuledAt 241 | }) 242 | } 243 | 244 | return appealRuledAtTimestamp 245 | } 246 | 247 | /** 248 | * Get the timestamp on when the dispute's appeal was created 249 | * @param {number} disputeID - The index of the dispute. 250 | * @param {string} account - The users address. 251 | * @param {number} appeal - The appeal number. 0 if there have been no appeals. 252 | * @returns {number} timestamp of the appeal 253 | */ 254 | getAppealCreatedAt = async (disputeID, account, appeal = 0) => { 255 | const cachedDispute = this.disputeCache[disputeID] || {} 256 | if ( 257 | cachedDispute.appealCreatedAt && 258 | cachedDispute.appealCreatedAt[appeal] 259 | ) 260 | return cachedDispute.appealCreatedAt[appeal] 261 | 262 | const dispute = await this._ArbitratorInstance.getDispute(disputeID) 263 | 264 | let appealCreatedAtTimestamp = null 265 | if (appeal === 0) { 266 | const creationBlock = await this._getDisputeStartBlock(disputeID, account) 267 | if (creationBlock) { 268 | const timestampSeconds = await this._ArbitratorInstance._getTimestampForBlock( 269 | creationBlock 270 | ) 271 | 272 | appealCreatedAtTimestamp = timestampSeconds * 1000 273 | } 274 | } else { 275 | appealCreatedAtTimestamp = await this._ArbitratorInstance.getAppealCreationTimestamp( 276 | dispute.firstSession + (appeal - 1) // appeal was created during previous session 277 | ) 278 | 279 | // cache the deadline for the appeal 280 | if (appealCreatedAtTimestamp) { 281 | const currentCreatedAt = cachedDispute.appealCreatedAt || [] 282 | currentCreatedAt[appeal] = appealCreatedAtTimestamp 283 | this._updateDisputeCache(disputeID, { 284 | appealCreatedAt: currentCreatedAt 285 | }) 286 | } 287 | } 288 | 289 | return appealCreatedAtTimestamp 290 | } 291 | 292 | /** 293 | * Get data for a dispute. This method provides data from the store as well as both 294 | * arbitrator and arbitrable contracts. Used to get all relevant data on a dispute. 295 | * @param {number} disputeID - The dispute's ID. 296 | * @param {string} account - The juror's address. 297 | * @returns {object} - Data object for the dispute that uses data from the contract and the store. 298 | */ 299 | getDataForDispute = async (disputeID, account) => { 300 | const arbitratorAddress = this._ArbitratorInstance.getContractAddress() 301 | // Get dispute data from contract. Also get the current session and period. 302 | const [dispute, period, session] = await Promise.all([ 303 | this._ArbitratorInstance.getDispute(disputeID, true), 304 | this._ArbitratorInstance.getPeriod(), 305 | this._ArbitratorInstance.getSession() 306 | ]) 307 | 308 | // Get arbitrable contract data and evidence 309 | const arbitrableContractAddress = dispute.arbitrableContractAddress 310 | await this._ArbitrableInstance.setContractInstance( 311 | arbitrableContractAddress 312 | ) 313 | const [metaEvidence, evidence, parties] = await Promise.all([ 314 | this._ArbitrableInstance.getMetaEvidence(), 315 | this._ArbitrableInstance.getEvidence(), 316 | this._ArbitrableInstance.getParties() 317 | ]) 318 | 319 | // Get dispute data from the store 320 | let appealDraws = [] 321 | 322 | // get draws if they have been added to store. 323 | try { 324 | const userData = await this._StoreProviderInstance.getDispute( 325 | account, 326 | arbitratorAddress, 327 | disputeID 328 | ) 329 | if (userData.appealDraws) appealDraws = userData.appealDraws || [] 330 | // eslint-disable-next-line no-unused-vars 331 | } catch (err) { 332 | // Dispute exists on chain but not in store. We have lost draws for past disputes. 333 | console.error('Dispute does not exist in store.') 334 | } 335 | 336 | const netPNK = await this._ArbitratorInstance.getNetTokensForDispute( 337 | disputeID, 338 | account 339 | ) 340 | 341 | // Build juror info and ruling arrays, indexed by appeal number 342 | const lastSession = dispute.firstSession + dispute.numberOfAppeals 343 | const appealJuror = [] 344 | const appealRulings = [] 345 | 346 | for (let appeal = 0; appeal <= dispute.numberOfAppeals; appeal++) { 347 | const isLastAppeal = dispute.firstSession + appeal === lastSession 348 | // Get appeal data 349 | const draws = appealDraws[appeal] || [] 350 | let canRule = false 351 | let canRepartition = false 352 | let canExecute = false 353 | let ruling 354 | const rulingPromises = [ 355 | this._ArbitratorInstance.currentRulingForDispute(disputeID) 356 | ] 357 | 358 | // Extra info for the last appeal 359 | if (isLastAppeal) { 360 | if (draws.length > 0) 361 | rulingPromises.push( 362 | this._ArbitratorInstance.canRuleDispute(disputeID, draws, account) 363 | ) 364 | 365 | if (session && period) 366 | canRepartition = 367 | lastSession <= session && // Not appealed to the next session 368 | period === arbitratorConstants.PERIOD.EXECUTE && // Executable period 369 | dispute.state === disputeConstants.STATE.OPEN // Open dispute 370 | canExecute = dispute.state === disputeConstants.STATE.EXECUTABLE // Executable state 371 | } 372 | 373 | // Wait for parallel requests to complete 374 | ;[ruling, canRule] = await Promise.all(rulingPromises) 375 | 376 | let jurorRuling = null 377 | // if can't rule that means they already did or they missed it 378 | if (!canRule) { 379 | jurorRuling = await this._ArbitratorInstance.getVoteForJuror( 380 | dispute.disputeID, 381 | appeal, 382 | account 383 | ) 384 | } 385 | 386 | const appealCreatedAt = await this.getAppealCreatedAt( 387 | dispute.disputeID, 388 | account, 389 | appeal 390 | ) 391 | const appealDeadline = await this.getDisputeDeadline( 392 | dispute.disputeID, 393 | appeal 394 | ) 395 | const appealRuledAt = await this.getAppealRuledAt( 396 | dispute.disputeID, 397 | appeal 398 | ) 399 | 400 | appealJuror[appeal] = { 401 | createdAt: appealCreatedAt, 402 | fee: dispute.arbitrationFeePerJuror.mul(draws.length), 403 | draws, 404 | jurorRuling, 405 | canRule 406 | } 407 | appealRulings[appeal] = { 408 | voteCounter: dispute.voteCounters[appeal], 409 | deadline: appealDeadline, 410 | ruledAt: appealRuledAt, 411 | ruling, 412 | canRepartition, 413 | canExecute 414 | } 415 | } 416 | 417 | return { 418 | // Arbitrable Contract Data 419 | arbitrableContractAddress, 420 | arbitratorAddress, 421 | parties, 422 | evidence, 423 | metaEvidence, 424 | 425 | // Dispute Data 426 | disputeID, 427 | firstSession: dispute.firstSession, 428 | lastSession, 429 | numberOfAppeals: dispute.numberOfAppeals, 430 | disputeState: dispute.state, 431 | disputeStatus: dispute.status, 432 | appealJuror, 433 | appealRulings, 434 | netPNK 435 | } 436 | } 437 | } 438 | 439 | export default Disputes 440 | -------------------------------------------------------------------------------- /src/resources/index.js: -------------------------------------------------------------------------------- 1 | import Disputes from './Disputes' 2 | import Notifications from './Notifications' 3 | import Auth from './Auth' 4 | 5 | export { Disputes, Notifications, Auth } 6 | -------------------------------------------------------------------------------- /src/utils/EventListener.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | import PromiseQueue from '../utils/PromiseQueue' 4 | import isRequired from '../utils/isRequired' 5 | import * as errorConstants from '../constants/error' 6 | 7 | /** 8 | * EventListener is used to watch events on the blockchain for a set of contracts. 9 | * Handlers for specific events can be added. When an event log is found EventListener 10 | * will fire all handlers registered for the contract. 11 | */ 12 | class EventListener { 13 | /** 14 | * Listen for events in contract and handles callbacks with registered event handlers. 15 | * @param {object[]} _contractImplementations - Contract Implementation instances to fetch event logs for. 16 | */ 17 | constructor(_contractImplementations = []) { 18 | this.contractInstances = [] 19 | // map address -> { event: [handlers], ... } 20 | this.contractEventHandlerMap = {} 21 | // map address -> watcher instance 22 | this.watcherInstances = {} 23 | // event handler queue 24 | this.eventHandlerQueue = new PromiseQueue() 25 | // initialize class variables for new contract instances 26 | _contractImplementations.forEach(instance => { 27 | this.addContractImplementation(instance) 28 | }) 29 | } 30 | 31 | /** 32 | * Fetch all logs from contractInstance in a block range. 33 | * @param {object} contractImplementationInstance - Contract Implementation instance. 34 | * @param {number} firstBlock - Lower bound of search range. 35 | * @param {number} lastBlock - Upper bound of search range. 36 | * @returns {Promise} All events in block range. 37 | */ 38 | static getAllEventLogs = async ( 39 | contractImplementationInstance = isRequired( 40 | 'contractImplementationInstance' 41 | ), 42 | firstBlock = 0, 43 | lastBlock = 'latest' 44 | ) => 45 | Promise.all( 46 | (await contractImplementationInstance.loadContract()) 47 | .allEvents({ 48 | fromBlock: firstBlock, 49 | toBlock: lastBlock 50 | }) 51 | .get((error, result) => { 52 | if (error) 53 | throw new Error(errorConstants.ERROR_FETCHING_EVENTS(error)) 54 | 55 | return result 56 | }) 57 | ) 58 | 59 | /** 60 | * Fetch logs from contractInstance for a specific event in a block range. 61 | * @param {object} contractImplementationInstance - contract Implementation instance. 62 | * @param {string} eventName - Name of the event. 63 | * @param {number} firstBlock - Lower bound of search range. 64 | * @param {number} lastBlock - Upper bound of search range. 65 | * @param {object} filters - Extra filters 66 | * @returns {Promise} All events in block range. 67 | */ 68 | static getEventLogs = async ( 69 | contractImplementationInstance = isRequired( 70 | 'contractImplementationInstance' 71 | ), 72 | eventName = isRequired('eventName'), 73 | firstBlock = 0, 74 | lastBlock = 'latest', 75 | filters = {} 76 | ) => { 77 | await contractImplementationInstance.loadContract() 78 | return new Promise((resolve, reject) => { 79 | contractImplementationInstance.contractInstance[eventName](filters, { 80 | fromBlock: firstBlock, 81 | toBlock: lastBlock 82 | }).get((error, result) => { 83 | if (error) reject(errorConstants.ERROR_FETCHING_EVENTS(error)) 84 | 85 | resolve(result) 86 | }) 87 | }) 88 | } 89 | 90 | /** 91 | * Add a contract instance to watch for new event logs. 92 | * @param {object} contractImplementationInstance - Contract Implementation instance 93 | */ 94 | addContractImplementation = contractImplementationInstance => { 95 | this.contractInstances.push(contractImplementationInstance) 96 | this.contractEventHandlerMap[ 97 | contractImplementationInstance.getContractAddress() 98 | ] = {} 99 | } 100 | 101 | /** 102 | * Remove contract instance being watched. Will also remove all handlers. 103 | * @param {string} contractImplementationInstance - contract implementation instance 104 | */ 105 | removeContractInstance = ( 106 | contractImplementationInstance = isRequired( 107 | 'contractImplementationInstance' 108 | ) 109 | ) => { 110 | const contractAddress = contractImplementationInstance.getContractAddress() 111 | // remove instance from this.contractInstances 112 | const removedInstance = _.remove( 113 | this.contractInstances, 114 | instance => instance.getContractAddress() === contractAddress 115 | ) 116 | // if we didn't remove anything throw error 117 | if (removedInstance.length === 0) 118 | throw new Error(errorConstants.MISSING_CONTRACT_INSTANCE(contractAddress)) 119 | // stop watching on these instances 120 | removedInstance.forEach(instance => this.stopWatchingForEvents(instance)) 121 | 122 | // remove handlers for contract instance 123 | delete this.contractEventHandlerMap[contractAddress] 124 | } 125 | 126 | /** 127 | * Add event handler that will be called when event log is found. 128 | * @param {string} contractImplementationInstance - Contract implementation instance 129 | * @param {string} eventName - Name of event. 130 | * @param {function} handler - Function to be called when event is consumed. 131 | */ 132 | addEventHandler = ( 133 | contractImplementationInstance = isRequired('contractAddress'), 134 | eventName = isRequired('eventName'), 135 | handler = isRequired('handler') 136 | ) => { 137 | const contractAddress = contractImplementationInstance.getContractAddress() 138 | if (!this.contractEventHandlerMap[contractAddress][eventName]) 139 | this.contractEventHandlerMap[contractAddress][eventName] = [] 140 | this.contractEventHandlerMap[contractAddress][eventName].push(handler) 141 | } 142 | 143 | /** 144 | * Watch for events on all contract instances. Call registered handlers when logs are found. 145 | * @param {number} fromBlock - A block number can be passed to catch up on missed logs 146 | * @returns {Promise} - Promise resolves when all watchers have been started 147 | */ 148 | watchForEvents = async (fromBlock = 'latest') => 149 | Promise.all( 150 | this.contractInstances.map(async contractImplementation => { 151 | const instance = await contractImplementation.loadContract() 152 | const newWatcherInstance = instance.allEvents({ 153 | fromBlock: fromBlock, 154 | lastBlock: 'latest' 155 | }) 156 | 157 | // NOTE: should we allow more than one listener per contract instance? 158 | if (this.watcherInstances[instance.address]) 159 | this.watcherInstances[instance.address].stopWatching() 160 | 161 | this.watcherInstances[instance.address] = newWatcherInstance 162 | newWatcherInstance.watch((error, result) => { 163 | if (!error) { 164 | const handlers = this.contractEventHandlerMap[instance.address][ 165 | result.event 166 | ] 167 | if (handlers) { 168 | handlers.forEach(handler => { 169 | this._queueEvent(handler, result) 170 | }) 171 | } 172 | } 173 | }) 174 | }) 175 | ) 176 | 177 | /** 178 | * Stop listening on contract. If no contractAddress supplied it stops all listeners. 179 | * @param {string} contractImplementationInstance - Address of the contract to stop watching 180 | */ 181 | stopWatchingForEvents = contractImplementationInstance => { 182 | if (contractImplementationInstance) { 183 | const watcherInstance = this.watcherInstances[ 184 | contractImplementationInstance.getContractAddress() 185 | ] 186 | 187 | if (watcherInstance) watcherInstance.stopWatching() 188 | } else 189 | this.contractInstances.forEach(instance => { 190 | this.watcherInstances[instance.getContractAddress()].stopWatching() 191 | }) 192 | } 193 | 194 | /** 195 | * Queues an event. 196 | * @param {function} handler - The handler. 197 | * @param {object} event - The event. 198 | */ 199 | _queueEvent = async (handler, event) => { 200 | const eventTask = async () => { 201 | await handler(event) 202 | } 203 | 204 | this.eventHandlerQueue.push(eventTask) 205 | } 206 | } 207 | 208 | export default EventListener 209 | -------------------------------------------------------------------------------- /src/utils/PromiseQueue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Chain promises so that they are evaluated in order. 3 | * @returns {object} - The promise queue object. 4 | */ 5 | const PromiseQueue = () => { 6 | let promise = Promise.resolve() 7 | 8 | return { 9 | push: fn => { 10 | promise = promise.then(fn, fn) 11 | }, 12 | fetch: fn => { 13 | let returnResolver 14 | let returnRejecter 15 | const returnPromise = new Promise((resolve, reject) => { 16 | returnResolver = resolve 17 | returnRejecter = reject 18 | }) 19 | promise = promise 20 | .then(fn, fn) 21 | .then(res => returnResolver(res), err => returnRejecter(err)) 22 | 23 | return returnPromise 24 | } 25 | } 26 | } 27 | 28 | export default PromiseQueue 29 | -------------------------------------------------------------------------------- /src/utils/StoreProviderWrapper.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | import * as errorConstants from '../constants/error' 4 | 5 | import PromiseQueue from './PromiseQueue' 6 | import httpRequest from './httpRequest' 7 | 8 | /** 9 | * A wrapper for interacting with Kleros Store. 10 | */ 11 | class StoreProviderWrapper { 12 | /** 13 | * Create a new instance of StoreProviderWrapper. 14 | * @param {string} storeProviderUri - The uri of kleros store. 15 | */ 16 | constructor(storeProviderUri) { 17 | this._storeUri = storeProviderUri 18 | this._storeQueue = new PromiseQueue() 19 | this._cachedProfiles = {} 20 | } 21 | 22 | /** 23 | * use the queue for write request. this allows a function to be passed so we can read immediately before we write 24 | * @param {fn} getBodyFn async function to call before we write. Should to reads and return JSON to be used as body. 25 | * @param {string} verb POST or PUT 26 | * @param {string} uri uri to call 27 | * @param {string} userAddress The users ETH address. Used to clear cache. 28 | * @returns {promise} promise that returns result of request. wait on this if you need it to be syncronous 29 | */ 30 | queueWriteRequest = (getBodyFn, verb, uri = null, userAddress) => { 31 | // Clear cache on write TODO update cache after every write 32 | this._cachedProfiles[userAddress] = null 33 | 34 | return this._storeQueue.fetch(() => 35 | getBodyFn().then(result => httpRequest(verb, uri, result)) 36 | ) 37 | } 38 | 39 | /** 40 | * If we know we are waiting on some other write before we want to read we can add a read request to the end of the queue. 41 | * @param {string} uri uri to hit 42 | * @returns {Promise} promise of the result function 43 | */ 44 | queueReadRequest = uri => 45 | this._storeQueue.fetch(() => httpRequest('GET', uri)) 46 | 47 | getMetaEvidenceUri = ( 48 | userAddress, 49 | contractAddress, 50 | arbitrableTransactionIndex 51 | ) => 52 | `${ 53 | this._storeUri 54 | }/${userAddress}/contracts/${contractAddress}/arbitrable-transaction/${arbitrableTransactionIndex}/meta-evidence` 55 | 56 | getEvidenceUri = ( 57 | userAddress, 58 | contractAddress, 59 | arbitrableTransactionIndex, 60 | evidenceIndex 61 | ) => 62 | `${ 63 | this._storeUri 64 | }/${userAddress}/contracts/${contractAddress}/arbitrable-transaction/${arbitrableTransactionIndex}/evidence/${evidenceIndex}` 65 | 66 | // **************************** // 67 | // * Read * // 68 | // **************************** // 69 | 70 | /** 71 | * Fetch stored user profile. 72 | * @param {string} userAddress - Address of user. 73 | * @returns {object} - a response object. 74 | */ 75 | getUserProfile = async userAddress => { 76 | const httpResponse = await httpRequest( 77 | 'GET', 78 | `${this._storeUri}/${userAddress}` 79 | ) 80 | this._cachedProfiles[userAddress] = httpResponse.body 81 | 82 | return httpResponse.body 83 | } 84 | 85 | /** 86 | * Fetch stored data on a contract for a user. 87 | * @param {string} userAddress - Address of the user. 88 | * @param {string} addressContract - The address of the contract. 89 | * @returns {object} - Contact data. 90 | */ 91 | getContractByAddress = async (userAddress, addressContract) => { 92 | const userProfile = await this.getUserProfile(userAddress) 93 | if (!userProfile) return {} 94 | 95 | let contract = _.filter( 96 | userProfile.contracts, 97 | contract => contract.address === addressContract 98 | ) 99 | 100 | return contract[0] 101 | } 102 | 103 | /** 104 | * Get all stored data for a dispute. Must exist in User Profile. 105 | * @param {string} userAddress - Address of user. 106 | * @param {string} arbitratorAddress - Address of arbitrator contract. 107 | * @param {number} disputeID - Index of the dispute. 108 | * @returns {object} - a response object. 109 | */ 110 | getDispute = async (userAddress, arbitratorAddress, disputeID) => { 111 | const userProfile = await this.getUserProfile(userAddress) 112 | if (!userProfile) 113 | throw new Error(errorConstants.PROFILE_NOT_FOUND(userAddress)) 114 | 115 | const dispute = _.filter( 116 | userProfile.disputes, 117 | o => 118 | o.arbitratorAddress === arbitratorAddress && o.disputeId === disputeID 119 | )[0] || {} 120 | dispute.disputeID = dispute.disputeId 121 | return dispute 122 | } 123 | 124 | /** 125 | * Fetch stored disputes for a user. 126 | * @param {string} userAddress - Address of user. 127 | * @returns {object} - a response object. 128 | */ 129 | getDisputes = async userAddress => { 130 | const userProfile = await this.getUserProfile(userAddress) 131 | if (!userProfile) return [] 132 | 133 | return userProfile.disputes.map(dispute => { 134 | dispute.disputeID = dispute.disputeId 135 | return dispute 136 | }) 137 | } 138 | 139 | /** 140 | * Fetch the last block seen for a user. This is commonly used with EventListerer. 141 | * @param {string} userAddress - Address of user. 142 | * @returns {number} The last block number. 143 | */ 144 | getLastBlock = async userAddress => { 145 | let userProfile 146 | try { 147 | userProfile = (await this.newUserProfile(userAddress)) || {} 148 | // eslint-disable-next-line no-unused-vars 149 | } catch (err) { 150 | userProfile = {} 151 | } 152 | 153 | return userProfile.lastBlock || 0 154 | } 155 | 156 | // **************************** // 157 | // * Write * // 158 | // **************************** // 159 | 160 | /** 161 | * Set up a new user profile if one does not exist. 162 | * @param {string} userAddress - user's address 163 | * @returns {object} - users existing or created profile 164 | */ 165 | newUserProfile = async userAddress => { 166 | let userProfile = await this.getUserProfile(userAddress) 167 | if (_.isNull(userProfile)) { 168 | // we can safely make request without queuing because all other writes for profile will fail if it hasn't been created. 169 | const response = await httpRequest( 170 | 'POST', 171 | `${this._storeUri}/${userAddress}` 172 | ) 173 | userProfile = response.body 174 | } 175 | 176 | return userProfile 177 | } 178 | 179 | /** 180 | * Update users last block seen. This is the only item in user profile that can be overwritten. 181 | * @param {string} userAddress - User's address. 182 | * @param {string} blockNumber - The newest block number seen by user. 183 | * @returns {object} - HTTP response. 184 | */ 185 | updateLastBlock = async (userAddress, blockNumber) => { 186 | const getBodyFn = () => 187 | new Promise(resolve => 188 | resolve( 189 | JSON.stringify({ 190 | lastBlock: blockNumber 191 | }) 192 | ) 193 | ) 194 | 195 | return this.queueWriteRequest( 196 | getBodyFn, 197 | 'POST', 198 | `${this._storeUri}/${userAddress}/lastBlock` 199 | ) 200 | } 201 | 202 | /** 203 | * Update users last processed session. 204 | * @param {string} userAddress - User's address. 205 | * @param {string} session - The current session that the user has processed 206 | * @returns {object} - HTTP response. 207 | */ 208 | updateUserSession = async (userAddress, session) => { 209 | const getBodyFn = () => 210 | new Promise(resolve => 211 | resolve( 212 | JSON.stringify({ 213 | session 214 | }) 215 | ) 216 | ) 217 | 218 | return this.queueWriteRequest( 219 | getBodyFn, 220 | 'POST', 221 | `${this._storeUri}/${userAddress}/session` 222 | ) 223 | } 224 | 225 | /** 226 | * Update the stored data on a contract for a user. Note that you cannot overwrite contract data. 227 | * @param {string} userAddress - The user's address. 228 | * @param {string} contractAddress - The address of the contract. 229 | * @param {object} params - Params we want to update. 230 | * @returns {Promise} - The resulting contract data. 231 | */ 232 | updateContract = async (userAddress, contractAddress, params) => { 233 | const getBodyFn = async () => { 234 | let currentContractData = await this.getContractByAddress( 235 | userAddress, 236 | contractAddress 237 | ) 238 | if (!currentContractData) currentContractData = {} 239 | delete currentContractData._id 240 | 241 | params.address = contractAddress 242 | 243 | return JSON.stringify({ ...currentContractData, ...params }) 244 | } 245 | 246 | const httpResponse = await this.queueWriteRequest( 247 | getBodyFn, 248 | 'POST', 249 | `${this._storeUri}/${userAddress}/contracts/${contractAddress}` 250 | ) 251 | 252 | return _.filter( 253 | httpResponse.body[0].contracts, 254 | contract => contract.address === contractAddress 255 | )[0] 256 | } 257 | 258 | /** 259 | * Adds new evidence to the store for a users contract. NOTE this will only update the 260 | * stored evidence for the specified user, not all parties of the dispute. 261 | * @param {string} contractAddress - Address of the contract 262 | * @param {string} userAddress - Address of the user. 263 | * @param {string} arbitrableTransactionIndex - Id of the arbitrable transaction. 264 | * @param {string} name - Name of evidence. 265 | * @param {string} description - Description of evidence. 266 | * @param {string} url - A link to the evidence. 267 | * @param {string} hash - The hash of the evidence. 268 | * @returns {number} - The index of the evidence 269 | */ 270 | addEvidenceContract = async ( 271 | contractAddress, 272 | userAddress, 273 | arbitrableTransactionIndex, 274 | name, 275 | description, 276 | url, 277 | hash 278 | ) => { 279 | const getBodyFn = () => 280 | new Promise(resolve => 281 | resolve( 282 | JSON.stringify({ 283 | name, 284 | description, 285 | URI: url, 286 | hash 287 | }) 288 | ) 289 | ) 290 | 291 | const response = await this.queueWriteRequest( 292 | getBodyFn, 293 | 'POST', 294 | `${ 295 | this._storeUri 296 | }/${userAddress}/contracts/${contractAddress}/arbitrable-transaction/${arbitrableTransactionIndex}/evidence` 297 | ) 298 | 299 | if (response.status !== 201) 300 | throw new Error( 301 | errorConstants.REQUEST_FAILED('Unable to submit evidence') 302 | ) 303 | 304 | return response.body.evidenceIndex 305 | } 306 | 307 | /** 308 | * Update stored dispute data for a user. Note this will not overwrite data. 309 | * @param {string} userAddress - The address of the user. 310 | * @param {string} arbitratorAddress - The address of the arbitrator contract. 311 | * @param {number} disputeID - The index of the dispute. 312 | * @param {object} params - The dispute data we are updating. 313 | * @returns {Promise} The resulting dispute data. 314 | */ 315 | updateDisputeProfile = ( 316 | userAddress, 317 | arbitratorAddress, 318 | disputeID, 319 | params 320 | ) => { 321 | const getBodyFn = async () => { 322 | const userProfile = await this.newUserProfile(userAddress) 323 | 324 | const currentDisputeProfile = 325 | _.filter( 326 | userProfile.disputes, 327 | dispute => 328 | dispute.arbitratorAddress === arbitratorAddress && 329 | dispute.disputeId === disputeID 330 | )[0] || {} 331 | 332 | delete currentDisputeProfile._id 333 | // set these so if it is a new dispute they are included 334 | params.disputeId = disputeID 335 | params.arbitratorAddress = arbitratorAddress 336 | 337 | return JSON.stringify({ ...currentDisputeProfile, ...params }) 338 | } 339 | 340 | return this.queueWriteRequest( 341 | getBodyFn, 342 | 'POST', 343 | `${ 344 | this._storeUri 345 | }/${userAddress}/arbitrators/${arbitratorAddress}/disputes/${disputeID}` 346 | ) 347 | } 348 | 349 | /** 350 | * Adds draws for juror to dispute profile. 351 | * @param {string} userAddress - The address of the user. 352 | * @param {string} arbitratorAddress - The address of the arbitrator contract. 353 | * @param {number} disputeID - The index of the dispute. 354 | * @param {number[]} draws - The draws the juror has. 355 | * @param {number} appeal - The appeal for which it is for. 356 | * @returns {Promise} The resulting dispute data. 357 | */ 358 | addNewDrawsDisputeProfile = ( 359 | userAddress, 360 | arbitratorAddress, 361 | disputeID, 362 | draws, 363 | appeal 364 | ) => { 365 | const getBodyFn = () => 366 | new Promise(resolve => 367 | resolve( 368 | JSON.stringify({ 369 | draws, 370 | appeal 371 | }) 372 | ) 373 | ) 374 | 375 | return this.queueWriteRequest( 376 | getBodyFn, 377 | 'POST', 378 | `${ 379 | this._storeUri 380 | }/${userAddress}/arbitrators/${arbitratorAddress}/disputes/${disputeID}/draws` 381 | ) 382 | } 383 | 384 | /** 385 | * Create a new notification in the store. 386 | * @param {string} userAddress - The address of the user. 387 | * @param {string} txHash - The transaction hash which produced this event log. Used as an identifier. 388 | * @param {number} logIndex - The index of the log in the transaction. Used as an identifier. 389 | * @param {number} notificationType - The type of the notification. See constants/notification. 390 | * @param {string} message - The message to be stored with the notification. 391 | * @param {object} data - Any extra data stored with the notification. 392 | * @param {boolean} read - If the notification has been read or not. 393 | * @returns {Promise} - The resulting notification. 394 | */ 395 | newNotification = async ( 396 | userAddress, 397 | txHash, 398 | logIndex, 399 | notificationType, 400 | message = '', 401 | data = {}, 402 | read = false 403 | ) => { 404 | const getBodyFn = () => 405 | new Promise(resolve => 406 | resolve( 407 | JSON.stringify({ 408 | notificationType, 409 | logIndex, 410 | read, 411 | message, 412 | data 413 | }) 414 | ) 415 | ) 416 | 417 | return this.queueWriteRequest( 418 | getBodyFn, 419 | 'POST', 420 | `${this._storeUri}/${userAddress}/notifications/${txHash}` 421 | ) 422 | } 423 | 424 | /** 425 | * Create a new notification in the store. 426 | * @param {string} userAddress - The address of the user. 427 | * @param {string} txHash - The transaction hash which produced this event log. Used as an identifier. 428 | * @param {number} logIndex - The index of the log in the transaction. Used as an identifier. 429 | * @param {boolean} isRead - If the notification has been read or not. 430 | * @returns {Promise} - The resulting notification. 431 | */ 432 | markNotificationAsRead = async ( 433 | userAddress, 434 | txHash, 435 | logIndex, 436 | isRead = true 437 | ) => { 438 | const getBodyFn = () => 439 | new Promise(resolve => 440 | resolve( 441 | JSON.stringify({ 442 | logIndex, 443 | isRead 444 | }) 445 | ) 446 | ) 447 | 448 | const result = await this.queueWriteRequest( 449 | getBodyFn, 450 | 'POST', 451 | `${this._storeUri}/${userAddress}/notifications/${txHash}/read` 452 | ) 453 | return result.body.notifications 454 | } 455 | } 456 | 457 | export default StoreProviderWrapper 458 | -------------------------------------------------------------------------------- /src/utils/Web3Wrapper.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import Web3 from 'web3' 3 | 4 | class Web3Wrapper { 5 | /** 6 | * Constructor Web3 wrapper. 7 | * @param {object} web3Provider - The web3 instance. 8 | */ 9 | constructor(web3Provider) { 10 | this._web3 = new Web3(web3Provider) 11 | } 12 | 13 | isAddress = address => this._web3.isAddress(address) 14 | 15 | getAccount = index => this._web3.eth.accounts[index] 16 | 17 | getProvider = () => this._web3.currentProvider 18 | 19 | getCoinbase = () => this._web3.eth.coinbase 20 | 21 | getNonce = async address => { 22 | const nonce = await this._web3.eth.getTransactionCount(address) 23 | return nonce 24 | } 25 | 26 | toWei = (amount, unit) => { 27 | const newAmount = this._web3.toWei(amount, unit) 28 | return newAmount.toNumber ? newAmount.toNumber() : Number(newAmount) 29 | } 30 | 31 | fromWei = (amount, unit) => { 32 | const newAmount = this._web3.fromWei(amount, unit) 33 | return newAmount.toNumber ? newAmount.toNumber() : Number(newAmount) 34 | } 35 | 36 | toBigNumber = number => this._web3.toBigNumber(number) 37 | 38 | blockNumber = () => this._web3.eth.blockNumber 39 | 40 | sign = (userAddress, data) => 41 | new Promise((resolve, reject) => { 42 | this._web3.eth.sign(userAddress, data, (error, result) => { 43 | if (error) reject(error) 44 | 45 | resolve(result) 46 | }) 47 | }) 48 | 49 | getBlock = blockNumber => 50 | new Promise((resolve, reject) => { 51 | this._web3.eth.getBlock(blockNumber, (error, result) => { 52 | if (error) reject(error) 53 | 54 | resolve(result) 55 | }) 56 | }) 57 | 58 | doesContractExistAtAddressAsync = async address => { 59 | const code = await this._web3.eth.getCode(address) 60 | // Regex matches 0x0, 0x00, 0x in order to accommodate poorly implemented clients 61 | const codeIsEmpty = /^0x0{0,40}$/i.test(code) 62 | 63 | return !codeIsEmpty 64 | } 65 | 66 | _getNetworkIdIfExistsAsync = async () => { 67 | if (!_.isUndefined(this.networkIdIfExists)) { 68 | return this.networkIdIfExists 69 | } 70 | 71 | try { 72 | const networkId = await this._getNetworkAsync() 73 | 74 | this.networkIdIfExists = Number(networkId) 75 | return this.networkIdIfExists 76 | } catch (err) { 77 | console.log(err) 78 | return undefined 79 | } 80 | } 81 | 82 | _getNetworkAsync = async () => { 83 | const networkId = await this._web3.version.network 84 | 85 | return networkId 86 | } 87 | 88 | getBalanceInWeiAsync = owner => { 89 | if (_.isUndefined(owner)) { 90 | owner = this._web3.eth.accounts[0] 91 | } 92 | 93 | let balanceInWei = this._web3.eth.getBalance(owner) 94 | 95 | return balanceInWei.toString() 96 | } 97 | } 98 | 99 | export default Web3Wrapper 100 | -------------------------------------------------------------------------------- /src/utils/delegateCalls.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | /** 4 | * delegate calls from a baseClass to a providerClass if the call does not exist in base class 5 | * @param {object} baseClass - The base object that has first priority to make calls 6 | * @param {object} providerClass - The class whose methods will be called by baseClass 7 | * @param {function} middlewareCall - Middleware function that can act as 8 | * intermediary between base and provider. Should take a two params which is the provider 9 | * classes method call and the params passed 10 | */ 11 | const delegateCalls = (baseClass, providerClass, middlewareCall) => { 12 | // we don't want to delegate any calls that are part of the base class 13 | const existingMethods = Object.getOwnPropertyNames(baseClass).concat( 14 | Object.getPrototypeOf(baseClass) 15 | ) 16 | // methods of provider 17 | const providerMethods = Object.getOwnPropertyNames(providerClass) 18 | // calls we will delegate 19 | const delegatableMethods = providerMethods.filter(method => { 20 | if ( 21 | !providerClass.hasOwnProperty(method) || 22 | typeof providerClass[method] !== 'function' 23 | ) { 24 | return false 25 | } 26 | 27 | return !_.includes(existingMethods, method) 28 | }) 29 | 30 | // delegate calls in baseClass 31 | delegatableMethods.forEach(methodName => { 32 | let curriedCall 33 | if (middlewareCall) 34 | curriedCall = (...args) => 35 | middlewareCall(providerClass[methodName], ...args) 36 | else curriedCall = (...args) => providerClass[methodName](...args) 37 | // set method in baseClass 38 | baseClass[methodName] = curriedCall 39 | }) 40 | } 41 | 42 | /** 43 | { constant: true, 44 | inputs: [], 45 | name: 'lastPeriodChange', 46 | outputs: [ [Object] ], 47 | payable: false, 48 | stateMutability: 'view', 49 | type: 'function' }, 50 | */ 51 | 52 | export default delegateCalls 53 | -------------------------------------------------------------------------------- /src/utils/deployContractAsync.js: -------------------------------------------------------------------------------- 1 | import contract from 'truffle-contract' 2 | 3 | import * as ethConstants from '../constants/eth' 4 | import { UNABLE_TO_DEPLOY_CONTRACT } from '../constants/error' 5 | 6 | import isRequired from './isRequired' 7 | 8 | /** 9 | * Deploy a contract on the Ethereum network using the contract artifact. 10 | * @param {string} account - The account to deploy it under. 11 | * @param {number} value - The value to send. 12 | * @param {object} artifact - JSON artifact of the contract. 13 | * @param {object} web3Provider - Web3 Provider object (NOTE NOT Kleros Web3Wrapper) 14 | * @param {...any} args - Extra arguments. 15 | * @returns {object} - truffle-contract Object | err The contract object or an error 16 | */ 17 | const deployContractAsync = async ( 18 | account = isRequired('account'), 19 | value = isRequired('value'), 20 | artifact = isRequired('artifact'), 21 | web3Provider = isRequired('web3Provider'), 22 | ...args 23 | ) => { 24 | try { 25 | const MyContract = contract({ 26 | abi: artifact.abi, 27 | unlinked_binary: artifact.bytecode 28 | ? artifact.bytecode 29 | : artifact.unlinked_binary 30 | }) 31 | MyContract.setProvider(web3Provider) 32 | 33 | return MyContract.new(...args, { 34 | from: account, 35 | value: value, 36 | gas: ethConstants.TRANSACTION.GAS 37 | }) 38 | } catch (err) { 39 | console.error(err) 40 | throw new Error(UNABLE_TO_DEPLOY_CONTRACT) 41 | } 42 | } 43 | 44 | export default deployContractAsync 45 | -------------------------------------------------------------------------------- /src/utils/getContractAddress.js: -------------------------------------------------------------------------------- 1 | import ethUtil from 'ethereumjs-util' 2 | 3 | const getContractAddress = (account, nonce) => 4 | ethUtil.bufferToHex(ethUtil.generateAddress(account, nonce)) 5 | 6 | export default getContractAddress 7 | -------------------------------------------------------------------------------- /src/utils/httpRequest.js: -------------------------------------------------------------------------------- 1 | import * as errorConstants from '../constants/error' 2 | 3 | /** 4 | * Helper method for sending an http requests. 5 | * @param {string} verb - HTTP verb to be used in request. E.g. GET, POST, PUT. 6 | * @param {string} uri - The uri to send the request to. 7 | * @param {string} body - json string of the body. 8 | * @returns {Promise} request promise that resolves to the HTTP response. 9 | */ 10 | const httpRequest = (verb, uri, body = null) => { 11 | const httpRequest = new XMLHttpRequest() 12 | return new Promise((resolve, reject) => { 13 | try { 14 | httpRequest.open(verb, uri, true) 15 | if (body) { 16 | httpRequest.setRequestHeader( 17 | 'Content-Type', 18 | 'application/json;charset=UTF-8' 19 | ) 20 | } 21 | httpRequest.onreadystatechange = () => { 22 | if (httpRequest.readyState === 4) { 23 | let body = null 24 | try { 25 | body = JSON.parse(httpRequest.responseText) 26 | // eslint-disable-next-line no-unused-vars 27 | } catch (err) {} 28 | resolve({ 29 | body: body, 30 | status: httpRequest.status 31 | }) 32 | } 33 | } 34 | httpRequest.send(body) 35 | } catch (err) { 36 | reject(errorConstants.REQUEST_FAILED(err)) 37 | } 38 | }) 39 | } 40 | 41 | export default httpRequest 42 | -------------------------------------------------------------------------------- /src/utils/isRequired.js: -------------------------------------------------------------------------------- 1 | import { MISSING_PARAMETERS } from '../constants/error' 2 | 3 | /** 4 | * Used as the default parameter for an arguemnt that is considered required. It will 5 | * throw an error if the argument is not supplied by the user. 6 | * @param {string} name - The name of the missing argument. 7 | */ 8 | const isRequired = name => { 9 | throw new Error(MISSING_PARAMETERS(name)) 10 | } 11 | 12 | export default isRequired 13 | -------------------------------------------------------------------------------- /tests/helpers/asyncMockResponse.js: -------------------------------------------------------------------------------- 1 | const asyncMockResponse = response => 2 | new Promise(resolve => { 3 | setTimeout(() => { 4 | resolve(response) 5 | }, 200) 6 | }) 7 | 8 | export default asyncMockResponse 9 | -------------------------------------------------------------------------------- /tests/helpers/delaySecond.js: -------------------------------------------------------------------------------- 1 | const delaySecond = (seconds = 1) => 2 | new Promise(resolve => { 3 | setTimeout(() => { 4 | resolve(true) 5 | }, 1000 * seconds) 6 | }) 7 | 8 | export default delaySecond 9 | -------------------------------------------------------------------------------- /tests/helpers/setUpContracts.js: -------------------------------------------------------------------------------- 1 | import BlockHashRNG from '../../src/contracts/implementations/RNG/BlockHashRNG' 2 | import MiniMePinakion from '../../src/contracts/implementations/PNK/MiniMePinakion' 3 | import TokenFactory from '../../src/contracts/implementations/PNK/TokenFactory' 4 | import KlerosPOC from '../../src/contracts/implementations/arbitrator/KlerosPOC' 5 | import MultipleArbitrableTransaction from '../../src/contracts/implementations/arbitrable/MultipleArbitrableTransaction' 6 | 7 | const setUpContracts = async ( 8 | provider, 9 | klerosPOCParams, 10 | arbitrableContractParams 11 | ) => { 12 | // initialize RNG 13 | const rngContract = await BlockHashRNG.deploy( 14 | klerosPOCParams.account, 15 | provider 16 | ) 17 | // minime token 18 | const tokenFactory = await TokenFactory.deploy( 19 | klerosPOCParams.account, 20 | provider 21 | ) 22 | const pnkContract = await MiniMePinakion.deploy( 23 | klerosPOCParams.account, 24 | provider, 25 | tokenFactory.address 26 | ) 27 | 28 | // initialize KlerosPOC 29 | const klerosCourt = await KlerosPOC.deploy( 30 | rngContract.address, 31 | pnkContract.address, 32 | klerosPOCParams.timesPerPeriod, 33 | klerosPOCParams.account, 34 | klerosPOCParams.value, 35 | provider 36 | ) 37 | const pinakionPOC = new MiniMePinakion(provider, pnkContract.address) 38 | // transfer ownership 39 | await pinakionPOC.changeController( 40 | klerosCourt.address, 41 | klerosPOCParams.account 42 | ) 43 | 44 | const contractArbitrableTransaction = await MultipleArbitrableTransaction.deploy( 45 | arbitrableContractParams.partyA, 46 | provider 47 | ) 48 | 49 | return [ 50 | klerosCourt.address, 51 | contractArbitrableTransaction.address, 52 | rngContract.address, 53 | pnkContract.address 54 | ] 55 | } 56 | 57 | export default setUpContracts 58 | -------------------------------------------------------------------------------- /tests/helpers/waitNotifications.js: -------------------------------------------------------------------------------- 1 | const waitNotifications = (initialAmount = undefined, notificationCallback) => { 2 | let amount 3 | let currentAmount = 0 4 | let notificationList = [] 5 | let resolver 6 | let promise = new Promise(resolve => { 7 | resolver = resolve 8 | }) 9 | let callback = notification => { 10 | notificationCallback(notification) 11 | notificationList.push(notification) 12 | currentAmount += 1 13 | if (typeof amount !== 'undefined' && currentAmount >= amount) 14 | resolver(notificationList) 15 | } 16 | let setAmount = n => { 17 | amount = n 18 | if (currentAmount >= amount) resolver(notificationList) 19 | } 20 | if (typeof initialAmount !== 'undefined') setAmount(initialAmount) 21 | 22 | return { promise, callback, setAmount } 23 | } 24 | 25 | export default waitNotifications 26 | -------------------------------------------------------------------------------- /tests/integration/EventListener.test.js: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | 3 | import KlerosPOC from '../../src/contracts/implementations/arbitrator/KlerosPOC' 4 | import EventListener from '../../src/utils/EventListener' 5 | import Kleros from '../../src/kleros' 6 | import * as ethConstants from '../../src/constants/eth' 7 | import setUpContracts from '../helpers/setUpContracts' 8 | import waitNotifications from '../helpers/waitNotifications' 9 | import delaySecond from '../helpers/delaySecond' 10 | 11 | describe('Event Listener', () => { 12 | let partyA 13 | let partyB 14 | let other 15 | let web3 16 | let eventCallback 17 | let eventLogs = [] 18 | let klerosPOCData 19 | let arbitrableContractData 20 | let provider 21 | 22 | beforeAll(async () => { 23 | // use testRPC 24 | provider = await new Web3.providers.HttpProvider( 25 | ethConstants.LOCALHOST_ETH_PROVIDER 26 | ) 27 | 28 | web3 = await new Web3(provider) 29 | 30 | partyA = web3.eth.accounts[1] 31 | partyB = web3.eth.accounts[2] 32 | other = web3.eth.accounts[5] 33 | 34 | klerosPOCData = { 35 | timesPerPeriod: [1, 1, 1, 1, 1], 36 | account: other, 37 | value: 0 38 | } 39 | 40 | arbitrableContractData = { 41 | partyA, 42 | partyB, 43 | value: 1, 44 | timeout: 1, 45 | extraData: '', 46 | title: 'test title', 47 | description: 'test description', 48 | email: 'test@test.test', 49 | metaEvidenceUri: 'https://test-meta-evidence.com' 50 | } 51 | 52 | eventCallback = log => { 53 | eventLogs.push(log) 54 | } 55 | }) 56 | 57 | it( 58 | 'registers handler for event and successfully fires callback on log', 59 | async () => { 60 | const [ 61 | klerosPOCAddress, 62 | arbitrableContractAddress, 63 | rngAddress, 64 | pnkAddress 65 | ] = await setUpContracts(provider, klerosPOCData, arbitrableContractData) 66 | 67 | expect(klerosPOCAddress).toBeDefined() 68 | expect(arbitrableContractAddress).toBeDefined() 69 | expect(rngAddress).toBeDefined() 70 | expect(pnkAddress).toBeDefined() 71 | 72 | const KlerosPOCInstance = new KlerosPOC(provider, klerosPOCAddress) 73 | 74 | const EventListenerInstance = new EventListener([KlerosPOCInstance]) 75 | // set up callback 76 | let { promise: waitPromise, callback: waitCallback } = waitNotifications( 77 | 1, 78 | eventCallback 79 | ) 80 | // add event handler 81 | const eventName = 'NewPeriod' 82 | EventListenerInstance.addEventHandler( 83 | KlerosPOCInstance, 84 | eventName, 85 | waitCallback 86 | ) 87 | await EventListenerInstance.watchForEvents() 88 | 89 | await delaySecond() 90 | KlerosPOCInstance.passPeriod(other) 91 | // we will wait for 2 seconds for promise to resolve or else throw 92 | let throwError = true 93 | setTimeout(() => { 94 | if (throwError) { 95 | EventListenerInstance.stopWatchingForEvents(KlerosPOCInstance) 96 | throw new Error('Callback Promise did not resolve') 97 | } 98 | }, 1000 * 2) 99 | 100 | await waitPromise 101 | throwError = false 102 | 103 | expect(eventLogs.length).toEqual(1) 104 | const log = eventLogs[0] 105 | expect(log.event).toEqual(eventName) 106 | 107 | EventListenerInstance.stopWatchingForEvents(KlerosPOCInstance) 108 | }, 109 | 50000 110 | ) 111 | it('can stop and remove contract instance', async () => { 112 | const [ 113 | klerosPOCAddress, 114 | arbitrableContractAddress, 115 | rngAddress, 116 | pnkAddress 117 | ] = await setUpContracts(provider, klerosPOCData, arbitrableContractData) 118 | 119 | expect(klerosPOCAddress).toBeDefined() 120 | expect(arbitrableContractAddress).toBeDefined() 121 | expect(rngAddress).toBeDefined() 122 | expect(pnkAddress).toBeDefined() 123 | 124 | const KlerosPOCInstance = new KlerosPOC(provider, klerosPOCAddress) 125 | 126 | // add conract instance with event handler 127 | const EventListenerInstance = new EventListener([KlerosPOCInstance]) 128 | EventListenerInstance.addEventHandler( 129 | KlerosPOCInstance, 130 | 'FakeEvent', 131 | () => {} 132 | ) 133 | await EventListenerInstance.watchForEvents() 134 | 135 | EventListenerInstance.removeContractInstance(KlerosPOCInstance) 136 | 137 | expect(EventListenerInstance.contractInstances.length).toEqual(0) 138 | expect( 139 | EventListenerInstance.contractEventHandlerMap[KlerosPOCInstance] 140 | ).toBeUndefined() 141 | }) 142 | it( 143 | 'registers handler for event, stops and starts again using only second handler from kleros', 144 | async () => { 145 | const [ 146 | klerosPOCAddress, 147 | arbitrableContractAddress, 148 | rngAddress, 149 | pnkAddress 150 | ] = await setUpContracts(provider, klerosPOCData, arbitrableContractData) 151 | 152 | expect(klerosPOCAddress).toBeDefined() 153 | expect(arbitrableContractAddress).toBeDefined() 154 | expect(rngAddress).toBeDefined() 155 | expect(pnkAddress).toBeDefined() 156 | 157 | const KlerosInstance = new Kleros(provider, '', klerosPOCAddress) 158 | 159 | // start event watching 160 | await KlerosInstance.watchForEvents(partyA) 161 | // stop watching for events to clear store provider events and to reset contractInstances 162 | await KlerosInstance.stopWatchingForEvents() 163 | expect(KlerosInstance.eventListener.contractInstances.length).toEqual(0) 164 | expect( 165 | KlerosInstance.eventListener.contractEventHandlerMap[ 166 | KlerosInstance.arbitrator 167 | ] 168 | ).toBeUndefined() 169 | expect( 170 | KlerosInstance.eventListener.watcherInstances[KlerosInstance.arbitrator] 171 | ).toBeUndefined() 172 | // add the contract back 173 | await KlerosInstance.eventListener.addContractImplementation( 174 | KlerosInstance.arbitrator 175 | ) 176 | // set up callback 177 | let { promise: waitPromise, callback: waitCallback } = waitNotifications( 178 | 1, 179 | eventCallback 180 | ) 181 | // add event handler 182 | const eventName = 'NewPeriod' 183 | KlerosInstance.eventListener.addEventHandler( 184 | KlerosInstance.arbitrator, 185 | eventName, 186 | waitCallback 187 | ) 188 | // start watching for events again 189 | await KlerosInstance.eventListener.watchForEvents() 190 | 191 | await delaySecond() 192 | await KlerosInstance.arbitrator.passPeriod(other) 193 | // we will wait for 2 seconds for promise to resolve or else throw 194 | let throwError = true 195 | setTimeout(() => { 196 | if (throwError) { 197 | KlerosInstance.eventListener.stopWatchingForEvents( 198 | KlerosInstance.arbitrator 199 | ) 200 | throw new Error('Callback Promise did not resolve') 201 | } 202 | }, 1000 * 2) 203 | 204 | await waitPromise 205 | throwError = false 206 | 207 | expect( 208 | eventLogs.filter( 209 | event => 210 | event.address === KlerosInstance.arbitrator.getContractAddress() 211 | ).length 212 | ).toEqual(1) 213 | 214 | KlerosInstance.eventListener.stopWatchingForEvents( 215 | KlerosInstance.arbitrator 216 | ) 217 | }, 218 | 50000 219 | ) 220 | }) 221 | -------------------------------------------------------------------------------- /tests/integration/contracts.test.js: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | 3 | import KlerosPOC from '../../src/contracts/implementations/arbitrator/KlerosPOC' 4 | import MultipleArbitrableTransaction from '../../src/contracts/implementations/arbitrable/MultipleArbitrableTransaction' 5 | import * as ethConstants from '../../src/constants/eth' 6 | import * as errorConstants from '../../src/constants/error' 7 | import setUpContracts from '../helpers/setUpContracts' 8 | import delaySecond from '../helpers/delaySecond' 9 | 10 | describe('Contracts', () => { 11 | let partyA 12 | let partyB 13 | let jurorContract1 14 | let jurorContract2 15 | let other 16 | let web3 17 | let klerosPOCData 18 | let arbitrableContractData 19 | let provider 20 | 21 | beforeAll(async () => { 22 | // use testRPC 23 | provider = await new Web3.providers.HttpProvider( 24 | ethConstants.LOCALHOST_ETH_PROVIDER 25 | ) 26 | 27 | web3 = await new Web3(provider) 28 | 29 | partyA = web3.eth.accounts[0] 30 | partyB = web3.eth.accounts[1] 31 | other = web3.eth.accounts[2] 32 | 33 | // these accounts are not used 34 | jurorContract1 = web3.eth.accounts[11] 35 | jurorContract2 = web3.eth.accounts[12] 36 | 37 | klerosPOCData = { 38 | timesPerPeriod: [1, 1, 1, 1, 1], // activation, draw, vote, appeal, execution (seconds) 39 | account: other, 40 | value: 0 41 | } 42 | 43 | arbitrableContractData = { 44 | partyA, 45 | partyB, 46 | value: 0, 47 | timeout: 1, 48 | extraData: '', 49 | metaEvidenceUri: 'https://jsonplaceholder.typicode.com/posts/42', // assume this api is available and renders the content below 50 | metaEvidence: { 51 | userId: 5, 52 | id: 42, 53 | title: "commodi ullam sint et excepturi error explicabo praesentium voluptas", 54 | body: "odio fugit voluptatum ducimus earum autem est incidunt voluptatem\nodit reiciendis aliquam sunt sequi nulla dolorem\nnon facere repellendus voluptates quia\nratione harum vitae ut" 55 | } 56 | } 57 | }) 58 | 59 | describe('KlerosPOC', async () => { 60 | it('deploys arbitrator with contractInstance', async () => { 61 | const newKlerosPOC = await KlerosPOC.deploy( 62 | '', // rngAddress param 63 | '', // pnkAddress param 64 | klerosPOCData.timesPerPeriod, 65 | klerosPOCData.account, 66 | klerosPOCData.value, 67 | provider 68 | ) 69 | 70 | expect(newKlerosPOC.address).toBeTruthy() 71 | 72 | // Check that we can bootstrap with address 73 | const newKlerosInstance = new KlerosPOC(provider, newKlerosPOC.address) 74 | const contractInstance = await newKlerosInstance.loadContract() 75 | // this should load contract. Called in all KlerosPOC methods 76 | expect(contractInstance).toBeTruthy() 77 | expect(contractInstance.address).toEqual(newKlerosPOC.address) 78 | }) 79 | it('initializing contract with bad address fails', async () => { 80 | const newKlerosPOC = new KlerosPOC(provider, 0x0) // bad address param 81 | try { 82 | // Check that we can bootstrap with address 83 | await newKlerosPOC.loadContract() 84 | } catch (err) { 85 | expect(err.message).toEqual(errorConstants.CONTRACT_INSTANCE_NOT_SET) 86 | } 87 | }) 88 | it('setContractInstance throws with undefined parameters', async () => { 89 | const newKlerosPOC = await KlerosPOC.deploy( 90 | '', // rngAddress param 91 | '', // pnkAddress param 92 | klerosPOCData.timesPerPeriod, 93 | klerosPOCData.account, 94 | klerosPOCData.value, 95 | provider 96 | ) 97 | 98 | expect(newKlerosPOC.address).toBeTruthy() 99 | 100 | // Check that we can bootstrap with address 101 | const newKlerosInstance = new KlerosPOC(provider, newKlerosPOC.address) 102 | 103 | try { 104 | await newKlerosInstance.setContractInstance() 105 | } catch (err) { 106 | expect(err.message).toEqual(errorConstants.UNABLE_TO_LOAD_CONTRACT) 107 | } 108 | }) 109 | it('throws if we initialize KlerosPOC without an address', async () => { 110 | try { 111 | const _ = new KlerosPOC(provider) 112 | } catch (err) { 113 | expect(err.message).toEqual( 114 | errorConstants.MISSING_PARAMETERS('contractAddress') 115 | ) 116 | } 117 | }) 118 | }) 119 | 120 | describe('ArbitrableTransaction', async () => { 121 | it( 122 | 'deploy a arbitrableTransaction contract', 123 | async () => { 124 | const [ 125 | klerosPOCAddress, 126 | arbitrableContractAddress, 127 | rngAddress, 128 | pnkAddress 129 | ] = await setUpContracts( 130 | provider, 131 | klerosPOCData, 132 | arbitrableContractData 133 | ) 134 | expect(klerosPOCAddress).toBeDefined() 135 | expect(arbitrableContractAddress).toBeDefined() 136 | expect(rngAddress).toBeDefined() 137 | expect(pnkAddress).toBeDefined() 138 | 139 | // KlerosPOC 140 | const KlerosPOCInstance = new KlerosPOC(provider, klerosPOCAddress) 141 | const klerosCourtData = await KlerosPOCInstance.getData() 142 | expect(klerosCourtData.pinakionContractAddress).toEqual(pnkAddress) 143 | expect(klerosCourtData.rngContractAddress).toEqual(rngAddress) 144 | expect(klerosCourtData.period).toEqual(0) 145 | expect(klerosCourtData.session).toEqual(1) 146 | }, 147 | 10000 148 | ) 149 | it( 150 | 'create a arbitrable transaction', 151 | async () => { 152 | const [ 153 | klerosPOCAddress, 154 | arbitrableContractAddress 155 | ] = await setUpContracts( 156 | provider, 157 | klerosPOCData, 158 | arbitrableContractData 159 | ) 160 | 161 | expect(klerosPOCAddress).toBeDefined() 162 | expect(arbitrableContractAddress).toBeDefined() 163 | // arbitrable contract 164 | const ArbitrableTransactionInstance = new MultipleArbitrableTransaction( 165 | provider, 166 | arbitrableContractAddress 167 | ) 168 | await ArbitrableTransactionInstance.createArbitrableTransaction( 169 | arbitrableContractData.partyA, 170 | klerosPOCAddress, 171 | arbitrableContractData.partyB, 172 | arbitrableContractData.value, 173 | arbitrableContractData.timeout, 174 | arbitrableContractData.extraData, 175 | arbitrableContractData.metaEvidenceUri 176 | ) 177 | const transactionArbitrable0 = await ArbitrableTransactionInstance.getData( 178 | 0 179 | ) 180 | 181 | expect(transactionArbitrable0.seller).toEqual(arbitrableContractData.partyB) 182 | expect(transactionArbitrable0.buyer).toEqual(arbitrableContractData.partyA) 183 | expect(transactionArbitrable0.amount).toEqual(arbitrableContractData.value) 184 | expect(transactionArbitrable0.timeout).toEqual(arbitrableContractData.timeout) 185 | expect(transactionArbitrable0.disputeId).toEqual(0) 186 | expect(transactionArbitrable0.arbitrator).toEqual(klerosPOCAddress) 187 | expect(transactionArbitrable0.arbitratorExtraData).toEqual('0x') 188 | expect(transactionArbitrable0.sellerFee).toEqual(0) 189 | expect(transactionArbitrable0.buyerFee).toEqual(0) 190 | expect(transactionArbitrable0.lastInteraction).toBeDefined() 191 | expect(transactionArbitrable0.status).toEqual(0) 192 | expect(transactionArbitrable0.metaEvidence).toEqual(arbitrableContractData.metaEvidence) 193 | }, 194 | 10000 195 | ) 196 | it( 197 | 'Arbitrable Contract where partyA pays partyB', 198 | async () => { 199 | const [ 200 | klerosPOCAddress, 201 | arbitrableContractAddress 202 | ] = await setUpContracts( 203 | provider, 204 | klerosPOCData, 205 | arbitrableContractData 206 | ) 207 | 208 | expect(klerosPOCAddress).toBeDefined() 209 | expect(arbitrableContractAddress).toBeDefined() 210 | 211 | const ArbitrableTransactionInstance = new MultipleArbitrableTransaction( 212 | provider, 213 | arbitrableContractAddress 214 | ) 215 | 216 | // create a arbitrable transaction 217 | await ArbitrableTransactionInstance.createArbitrableTransaction( 218 | arbitrableContractData.partyA, 219 | klerosPOCAddress, 220 | arbitrableContractData.partyB, 221 | arbitrableContractData.value, 222 | arbitrableContractData.timeout, 223 | arbitrableContractData.extraData, 224 | arbitrableContractData.metaEvidenceUri 225 | ) 226 | 227 | // buyer pays the seller 228 | const transactionArbitrable0 = await ArbitrableTransactionInstance.pay( 229 | arbitrableContractData.partyA, 230 | 0, 231 | arbitrableContractData.value 232 | ) 233 | 234 | expect(transactionArbitrable0.tx).toEqual( 235 | expect.stringMatching(/^0x[a-f0-9]{64}$/) 236 | ) // tx hash 237 | }, 238 | 50000 239 | ) 240 | it( 241 | 'dispute with a timeout call by the buyer', 242 | async () => { 243 | const [ 244 | klerosPOCAddress, 245 | arbitrableContractAddress 246 | ] = await setUpContracts( 247 | provider, 248 | klerosPOCData, 249 | arbitrableContractData 250 | ) 251 | expect(klerosPOCAddress).toBeDefined() 252 | expect(arbitrableContractAddress).toBeDefined() 253 | 254 | const ArbitrableTransactionInstance = new MultipleArbitrableTransaction( 255 | provider, 256 | arbitrableContractAddress 257 | ) 258 | 259 | // create a arbitrable transaction 260 | await ArbitrableTransactionInstance.createArbitrableTransaction( 261 | arbitrableContractData.partyA, 262 | klerosPOCAddress, 263 | arbitrableContractData.partyB, 264 | arbitrableContractData.value, 265 | arbitrableContractData.timeout, 266 | arbitrableContractData.extraData, 267 | arbitrableContractData.metaEvidenceUri 268 | ) 269 | 270 | const KlerosInstance = new KlerosPOC(provider, klerosPOCAddress) 271 | // return a bigint with the default value : 10000 wei fees in ether 272 | const arbitrationCost = await KlerosInstance.getArbitrationCost( 273 | arbitrableContractData.extraData 274 | ) 275 | 276 | // buyer A pays fee 277 | const raiseDisputeByBuyerTxObj = await ArbitrableTransactionInstance.payArbitrationFeeByBuyer( 278 | arbitrableContractData.partyA, 279 | 0, 280 | arbitrationCost 281 | ) 282 | 283 | expect(raiseDisputeByBuyerTxObj.tx).toEqual( 284 | expect.stringMatching(/^0x[a-f0-9]{64}$/) 285 | ) // tx hash 286 | 287 | await delaySecond(2) 288 | // call timeout by the buyer 289 | const txHashTimeOutByBuyer = await ArbitrableTransactionInstance.callTimeOutBuyer( 290 | arbitrableContractData.partyA, 291 | 0 292 | ) 293 | expect(txHashTimeOutByBuyer.tx).toEqual( 294 | expect.stringMatching(/^0x[a-f0-9]{64}$/) 295 | ) // tx hash 296 | }, 297 | 50000 298 | ) 299 | it( 300 | 'dispute with an appeal call by the buyer', 301 | async () => { 302 | const [ 303 | klerosPOCAddress, 304 | arbitrableContractAddress 305 | ] = await setUpContracts( 306 | provider, 307 | klerosPOCData, 308 | arbitrableContractData 309 | ) 310 | await expect(klerosPOCAddress).toBeDefined() 311 | await expect(arbitrableContractAddress).toBeDefined() 312 | 313 | const ArbitrableTransactionInstance = new MultipleArbitrableTransaction( 314 | provider, 315 | arbitrableContractAddress 316 | ) 317 | 318 | // create a arbitrable transaction 319 | await ArbitrableTransactionInstance.createArbitrableTransaction( 320 | arbitrableContractData.partyA, 321 | klerosPOCAddress, 322 | arbitrableContractData.partyB, 323 | arbitrableContractData.value, 324 | arbitrableContractData.timeout, 325 | arbitrableContractData.extraData, 326 | arbitrableContractData.metaEvidenceUri 327 | ) 328 | 329 | const KlerosPOCInstance = await new KlerosPOC(provider, klerosPOCAddress) 330 | 331 | // // ****** Juror side (activate token) ****** // 332 | 333 | // jurors buy PNK 334 | const pnkAmount = '1000000000000000000' 335 | const buyPNKJurors = await Promise.all([ 336 | await KlerosPOCInstance.buyPNK(pnkAmount, jurorContract1), 337 | await KlerosPOCInstance.buyPNK(pnkAmount, jurorContract2) 338 | ]) 339 | 340 | const newBalance = await KlerosPOCInstance.getPNKBalance(jurorContract1) 341 | 342 | expect(newBalance.tokenBalance.toString()).toEqual(pnkAmount) 343 | 344 | // activate PNK jurors 345 | if (buyPNKJurors) { 346 | await KlerosPOCInstance.activatePNK(pnkAmount, jurorContract1) 347 | await KlerosPOCInstance.activatePNK(pnkAmount, jurorContract2) 348 | } 349 | 350 | // ****** Parties side (raise dispute) ****** // 351 | 352 | const KlerosInstance = new KlerosPOC(provider, klerosPOCAddress) 353 | // return a bigint with the default value : 10000 wei fees in ether 354 | const arbitrationCost = await KlerosInstance.getArbitrationCost( 355 | arbitrableContractData.extraData 356 | ) 357 | 358 | // buyer A pays fee 359 | const raiseDisputeByBuyerTxObj = await ArbitrableTransactionInstance.payArbitrationFeeByBuyer( 360 | arbitrableContractData.partyA, 361 | 0, 362 | arbitrationCost 363 | ) 364 | 365 | expect(raiseDisputeByBuyerTxObj.tx).toEqual( 366 | expect.stringMatching(/^0x[a-f0-9]{64}$/) 367 | ) // tx hash 368 | 369 | // seller pays fee 370 | const raiseDisputeBySellerTxObj = await ArbitrableTransactionInstance.payArbitrationFeeBySeller( 371 | arbitrableContractData.partyB, 372 | 0, 373 | arbitrationCost 374 | ) 375 | 376 | expect(raiseDisputeBySellerTxObj.tx).toEqual( 377 | expect.stringMatching(/^0x[a-f0-9]{64}$/) 378 | ) // tx hash 379 | 380 | // ****** Juror side (pass period) ****** // 381 | 382 | let newPeriod 383 | // pass state so jurors are selected 384 | for (let i = 1; i < 3; i++) { 385 | // NOTE we need to make another block before we can generate the random number. Should not be an issue on main nets where avg block time < period length 386 | if (i === 2) 387 | web3.eth.sendTransaction({ 388 | from: partyA, 389 | to: partyB, 390 | value: 10000, 391 | data: '0x' 392 | }) 393 | await delaySecond() 394 | await KlerosPOCInstance.passPeriod(other) 395 | 396 | newPeriod = await KlerosPOCInstance.getPeriod() 397 | expect(newPeriod).toEqual(i) 398 | } 399 | 400 | let drawA = [] 401 | let drawB = [] 402 | for (let i = 1; i <= 3; i++) { 403 | if ( 404 | await KlerosPOCInstance.isJurorDrawnForDispute(0, i, jurorContract1) 405 | ) { 406 | drawA.push(i) 407 | } else { 408 | drawB.push(i) 409 | } 410 | } 411 | 412 | const rulingJuror1 = 1 // vote for partyA 413 | await KlerosPOCInstance.submitVotes( 414 | 0, 415 | rulingJuror1, 416 | drawA, 417 | jurorContract1 418 | ) 419 | 420 | const rulingJuror2 = 2 // vote for partyB 421 | await KlerosPOCInstance.submitVotes( 422 | 0, 423 | rulingJuror2, 424 | drawB, 425 | jurorContract2 426 | ) 427 | await delaySecond() 428 | await KlerosPOCInstance.passPeriod(other) 429 | 430 | const currentRuling = await KlerosPOCInstance.currentRulingForDispute(0, 0) 431 | expect(currentRuling.toString()).toBeTruthy() // make sure the ruling exists 432 | 433 | const appealCost = await KlerosPOCInstance.getAppealCost( 434 | 0, 435 | arbitrableContractData.extraData 436 | ) 437 | 438 | // raise appeal party A 439 | const raiseAppealByPartyATxObj = await ArbitrableTransactionInstance.appeal( 440 | partyA, 441 | 0, 442 | arbitrableContractData.extraData, 443 | appealCost 444 | ) 445 | expect(raiseAppealByPartyATxObj.tx).toEqual( 446 | expect.stringMatching(/^0x[a-f0-9]{64}$/) 447 | ) // tx hash 448 | 449 | const dispute = await KlerosPOCInstance.getDispute(0) 450 | expect(dispute.numberOfAppeals).toEqual(1) 451 | }, 452 | 50000 453 | ) 454 | }) 455 | }) 456 | -------------------------------------------------------------------------------- /tests/integration/disputeResolution.test.js: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | 3 | import KlerosPOC from '../../src/contracts/implementations/arbitrator/KlerosPOC' 4 | import MultipleArbitrableTransaction from '../../src/contracts/implementations/arbitrable/MultipleArbitrableTransaction' 5 | import Notifications from '../../src/resources/Notifications' 6 | import * as ethConstants from '../../src/constants/eth' 7 | import * as notificationConstants from '../../src/constants/notification' 8 | import setUpContracts from '../helpers/setUpContracts' 9 | import delaySecond from '../helpers/delaySecond' 10 | 11 | describe('Dispute Resolution', () => { 12 | let partyA 13 | let partyB 14 | let juror1 15 | let juror2 16 | let other 17 | let web3 18 | let klerosPOCData 19 | let arbitrableContractData 20 | let provider 21 | 22 | beforeAll(async () => { 23 | // use testRPC 24 | provider = await new Web3.providers.HttpProvider( 25 | ethConstants.LOCALHOST_ETH_PROVIDER 26 | ) 27 | 28 | web3 = await new Web3(provider) 29 | 30 | partyA = web3.eth.accounts[6] 31 | partyB = web3.eth.accounts[7] 32 | juror1 = web3.eth.accounts[8] 33 | juror2 = web3.eth.accounts[9] 34 | other = web3.eth.accounts[10] 35 | 36 | klerosPOCData = { 37 | timesPerPeriod: [1, 1, 1, 1, 1], 38 | account: other, 39 | value: 0 40 | } 41 | 42 | arbitrableContractData = { 43 | partyA, 44 | partyB, 45 | value: '1000000000000000000', 46 | timeout: 1, 47 | extraData: '', 48 | metaEvidenceUri: 'https://my-meta-evidence.ipfs.io' 49 | } 50 | }) 51 | 52 | it( 53 | 'KlerosPOC full dispute resolution flow', 54 | async () => { 55 | const [ 56 | klerosPOCAddress, 57 | arbitrableContractAddress, 58 | rngAddress, 59 | pnkAddress 60 | ] = await setUpContracts(provider, klerosPOCData, arbitrableContractData) 61 | expect(klerosPOCAddress).toBeDefined() 62 | expect(arbitrableContractAddress).toBeDefined() 63 | expect(rngAddress).toBeDefined() 64 | expect(pnkAddress).toBeDefined() 65 | 66 | const KlerosPOCInstance = new KlerosPOC(provider, klerosPOCAddress) 67 | // arbitrable contract 68 | const ArbitrableTransactionInstance = new MultipleArbitrableTransaction( 69 | provider, 70 | arbitrableContractAddress 71 | ) 72 | // Notifications instance for testing stateful notifications 73 | const NotificationsInstance = new Notifications( 74 | KlerosPOCInstance, 75 | ArbitrableTransactionInstance 76 | ) 77 | // stateful notifications juror1 78 | let juror1StatefullNotifications = await NotificationsInstance.getStatefulNotifications( 79 | juror1, 80 | true 81 | ) 82 | expect(juror1StatefullNotifications.length).toEqual(1) 83 | expect(juror1StatefullNotifications[0].notificationType).toEqual( 84 | notificationConstants.TYPE.CAN_ACTIVATE 85 | ) 86 | // juror1 should have no balance to start with 87 | const initialBalance = await KlerosPOCInstance.getPNKBalance(juror1) 88 | expect(initialBalance.tokenBalance.toString()).toEqual('0') 89 | // buy 1 PNK juror1 90 | const amount = web3.toWei(web3.toBigNumber(1), 'ether') 91 | await KlerosPOCInstance.buyPNK(amount, juror1) 92 | const newBalance = await KlerosPOCInstance.getPNKBalance(juror1) 93 | 94 | expect(newBalance.tokenBalance.toString()).toEqual(amount.toString()) 95 | // buy PNK for juror2 96 | await KlerosPOCInstance.buyPNK(amount, juror2) 97 | 98 | // activate PNK juror1 99 | const activatedTokenAmount = amount.div(2) 100 | const balance = await KlerosPOCInstance.activatePNK( 101 | activatedTokenAmount, 102 | juror1 103 | ) 104 | expect(balance.tokenBalance.toString()).toEqual(amount.toString()) 105 | expect(balance.activatedTokens.toString()).toEqual( 106 | activatedTokenAmount.toString() 107 | ) 108 | 109 | // stateful notifications juror1 110 | juror1StatefullNotifications = await NotificationsInstance.getStatefulNotifications( 111 | juror1, 112 | true 113 | ) 114 | expect(juror1StatefullNotifications.length).toEqual(0) 115 | // activate PNK juror2 116 | await KlerosPOCInstance.activatePNK(activatedTokenAmount, juror2) 117 | 118 | // load klerosPOC 119 | const klerosPOCInstance = await KlerosPOCInstance.loadContract() 120 | 121 | const juror1Data = await klerosPOCInstance.jurors(juror1) 122 | expect(juror1Data[2].toNumber()).toEqual( 123 | (await klerosPOCInstance.session()).toNumber() 124 | ) 125 | expect(juror1Data[4].minus(juror1Data[3]).toString()).toEqual( 126 | activatedTokenAmount.toString() 127 | ) 128 | // return a bigint 129 | // FIXME use arbitrableTransaction 130 | await ArbitrableTransactionInstance.createArbitrableTransaction( 131 | arbitrableContractData.partyA, 132 | klerosPOCAddress, 133 | arbitrableContractData.partyB, 134 | arbitrableContractData.value, 135 | arbitrableContractData.timeout, 136 | arbitrableContractData.extraData, 137 | arbitrableContractData.metaEvidenceUri 138 | ) 139 | // return a bigint with the default value : 10000 wei fees in ether 140 | const arbitrationCost = await KlerosPOCInstance.getArbitrationCost( 141 | arbitrableContractData.extraData 142 | ) 143 | 144 | // buyer pays arbitration fee 145 | const raiseDisputeByBuyerTxObj = await ArbitrableTransactionInstance.payArbitrationFeeByBuyer( 146 | arbitrableContractData.partyA, 147 | 0, 148 | arbitrationCost 149 | ) 150 | 151 | expect(raiseDisputeByBuyerTxObj.tx).toEqual( 152 | expect.stringMatching(/^0x[a-f0-9]{64}$/) 153 | ) // tx hash 154 | 155 | // seller pays fee to raise a dispute 156 | const raiseDisputeBySellerTxObj = await ArbitrableTransactionInstance.payArbitrationFeeBySeller( 157 | arbitrableContractData.partyB, 158 | 0, 159 | arbitrationCost 160 | ) 161 | 162 | expect(raiseDisputeBySellerTxObj.tx).toEqual( 163 | expect.stringMatching(/^0x[a-f0-9]{64}$/) 164 | ) // tx hash 165 | 166 | const dispute = await KlerosPOCInstance.getDispute(0, true) 167 | expect(dispute.arbitrableContractAddress).toEqual( 168 | arbitrableContractAddress 169 | ) 170 | expect(dispute.firstSession).toEqual( 171 | (await klerosPOCInstance.session()).toNumber() 172 | ) 173 | expect(dispute.numberOfAppeals).toEqual(0) 174 | expect(dispute.voteCounters).toEqual( 175 | new Array(dispute.numberOfAppeals + 1).fill( 176 | new Array(dispute.rulingChoices + 1).fill(0) 177 | ) 178 | ) 179 | 180 | // add an evidence for the buyer 181 | const txHashAddEvidence = await ArbitrableTransactionInstance.submitEvidence( 182 | arbitrableContractData.partyA, 183 | 0, // arbitrable transaction id 184 | 'https://my-evidence.ipfs.io' // evidence url 185 | ) 186 | expect(txHashAddEvidence).toEqual( 187 | expect.stringMatching(/^0x[a-f0-9]{64}$/) 188 | ) // tx hash 189 | 190 | // check initial state of contract 191 | // FIXME var must be more explicit 192 | const initialState = await KlerosPOCInstance.getData() 193 | expect(initialState.session).toEqual(1) 194 | expect(initialState.period).toEqual(0) 195 | 196 | let newPeriod 197 | // pass state so jurors are selected 198 | for (let i = 1; i < 3; i++) { 199 | // NOTE we need to make another block before we can generate the random number. Should not be an issue on main nets where avg block time < period length 200 | if (i === 2) 201 | await web3.eth.sendTransaction({ 202 | from: partyA, 203 | to: partyB, 204 | value: 10000, 205 | data: '0x' 206 | }) 207 | await delaySecond() 208 | await KlerosPOCInstance.passPeriod(other) 209 | 210 | newPeriod = await KlerosPOCInstance.getPeriod() 211 | expect(newPeriod).toEqual(i) 212 | } 213 | 214 | // check deadline timestamp 215 | const deadline = await KlerosPOCInstance.getDisputeDeadlineTimestamp(1) 216 | expect(deadline).toBeTruthy() 217 | 218 | // check deadline timestamp 219 | const noAppeal = await KlerosPOCInstance.getAppealRuledAtTimestamp(1) 220 | expect(noAppeal).toBeFalsy() 221 | 222 | let drawA = [] 223 | let drawB = [] 224 | for (let i = 1; i <= 3; i++) { 225 | if (await KlerosPOCInstance.isJurorDrawnForDispute(0, i, juror1)) { 226 | drawA.push(i) 227 | } else if ( 228 | await KlerosPOCInstance.isJurorDrawnForDispute(0, i, juror2) 229 | ) { 230 | drawB.push(i) 231 | } 232 | } 233 | expect(drawA.length + drawB.length).toEqual(3) 234 | const disputesForJuror1 = await KlerosPOCInstance.getDisputesForJuror( 235 | juror1 236 | ) 237 | const disputesForJuror2 = await KlerosPOCInstance.getDisputesForJuror( 238 | juror2 239 | ) 240 | expect( 241 | disputesForJuror1.length > 0 || disputesForJuror2.length > 0 242 | ).toBeTruthy() 243 | 244 | const disputeForJuror = 245 | disputesForJuror1.length > 0 246 | ? disputesForJuror1[0] 247 | : disputesForJuror2[0] 248 | expect(disputeForJuror.arbitrableContractAddress).toEqual( 249 | arbitrableContractAddress 250 | ) 251 | 252 | const jurorForNotifications = 253 | drawA.length > drawB.length ? juror1 : juror2 254 | // stateful notifications juror1 255 | let jurorStatefullNotifications = await NotificationsInstance.getStatefulNotifications( 256 | jurorForNotifications, 257 | true 258 | ) 259 | expect(jurorStatefullNotifications.length).toEqual(1) 260 | expect(jurorStatefullNotifications[0].notificationType).toEqual( 261 | notificationConstants.TYPE.CAN_VOTE 262 | ) 263 | 264 | // submit rulings 265 | const rulingJuror1 = 1 266 | await KlerosPOCInstance.submitVotes(0, rulingJuror1, drawA, juror1) 267 | const rulingJuror2 = 2 268 | await KlerosPOCInstance.submitVotes(0, rulingJuror2, drawB, juror2) 269 | const winningRuling = 270 | drawA.length > drawB.length ? rulingJuror1 : rulingJuror2 271 | 272 | await delaySecond() 273 | await KlerosPOCInstance.passPeriod(other) 274 | 275 | const currentRuling = await klerosPOCInstance.currentRuling(0) 276 | expect(`${currentRuling}`).toEqual(`${winningRuling}`) 277 | 278 | await delaySecond() 279 | await KlerosPOCInstance.passPeriod(other) 280 | 281 | // check ruled at timestamp 282 | const ruledAt = await KlerosPOCInstance.getAppealRuledAtTimestamp(1) 283 | expect(ruledAt).toBeTruthy() 284 | 285 | // stateful notifications 286 | jurorStatefullNotifications = await NotificationsInstance.getStatefulNotifications( 287 | jurorForNotifications, 288 | true 289 | ) 290 | expect(jurorStatefullNotifications.length).toEqual(1) 291 | expect(jurorStatefullNotifications[0].notificationType).toEqual( 292 | notificationConstants.TYPE.CAN_REPARTITION 293 | ) 294 | // balances before ruling is executed 295 | const partyABalance = web3.eth.getBalance(partyA) 296 | const partyBBalance = web3.eth.getBalance(partyB) 297 | // repartition tokens 298 | await KlerosPOCInstance.repartitionJurorTokens(0, other) 299 | 300 | // stateful notifications 301 | jurorStatefullNotifications = await NotificationsInstance.getStatefulNotifications( 302 | jurorForNotifications, 303 | true 304 | ) 305 | expect(jurorStatefullNotifications.length).toEqual(1) 306 | expect(jurorStatefullNotifications[0].notificationType).toEqual( 307 | notificationConstants.TYPE.CAN_EXECUTE 308 | ) 309 | // execute ruling 310 | await KlerosPOCInstance.executeRuling(0, other) 311 | 312 | juror1StatefullNotifications = await NotificationsInstance.getStatefulNotifications( 313 | juror1, 314 | true 315 | ) 316 | expect(juror1StatefullNotifications.length).toEqual(0) 317 | let partyAStatefullNotifications = await NotificationsInstance.getStatefulNotifications( 318 | partyA, 319 | false 320 | ) 321 | expect(partyAStatefullNotifications.length).toEqual(0) 322 | // balances after ruling 323 | // partyA wins so they should recieve their arbitration fee as well as the value locked in contract 324 | if (winningRuling === rulingJuror1) { 325 | expect( 326 | web3.eth 327 | .getBalance(partyA) 328 | .minus(partyABalance) 329 | .toString() 330 | ).toEqual( 331 | arbitrationCost 332 | .plus(web3.toBigNumber(arbitrableContractData.value)) 333 | .toString() 334 | ) 335 | // partyB lost so their balance should remain the same 336 | expect(web3.eth.getBalance(partyB).toString()).toEqual( 337 | partyBBalance.toString() 338 | ) 339 | } else { 340 | expect( 341 | web3.eth 342 | .getBalance(partyB) 343 | .minus(partyBBalance) 344 | .toString() 345 | ).toEqual( 346 | arbitrationCost 347 | .plus(web3.toBigNumber(arbitrableContractData.value)) 348 | .toString() 349 | ) 350 | // partyB lost so their balance should remain the same 351 | expect(web3.eth.getBalance(partyA).toString()).toEqual( 352 | partyABalance.toString() 353 | ) 354 | } 355 | const netPNK = await KlerosPOCInstance.getNetTokensForDispute(0, partyA) 356 | 357 | const updatedTransactionArbitrable0Data = await ArbitrableTransactionInstance.getData(0) 358 | expect(updatedTransactionArbitrable0Data.status).toEqual(4) // Resolved status expected 359 | }, 360 | 100000 361 | ) 362 | }) 363 | -------------------------------------------------------------------------------- /tests/integration/kleros.test.js: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | 3 | import Kleros from '../../src/kleros' 4 | import * as ethConstants from '../../src/constants/eth' 5 | 6 | describe('Kleros', () => { 7 | it('can be created', () => { 8 | const mockStoreUri = '' 9 | const ethProvider = new Web3.providers.HttpProvider( 10 | ethConstants.LOCALHOST_ETH_PROVIDER 11 | ) 12 | const mockArbitrator = '0x0' 13 | const mockArbitrable = '0x1' 14 | 15 | const klerosInstance = new Kleros( 16 | ethProvider, 17 | mockStoreUri, 18 | mockArbitrator, 19 | mockArbitrable 20 | ) 21 | 22 | expect(klerosInstance.arbitrator).toBeTruthy() 23 | expect(klerosInstance.arbitrable).toBeTruthy() 24 | expect(klerosInstance.disputes).toBeTruthy() 25 | expect(klerosInstance.notifications).toBeTruthy() 26 | 27 | expect(klerosInstance.arbitrator.getContractAddress()).toEqual( 28 | mockArbitrator 29 | ) 30 | expect(klerosInstance.arbitrable.getContractAddress()).toEqual( 31 | mockArbitrable 32 | ) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /tests/unit/contracts/abstractions/Arbitrator.test.js: -------------------------------------------------------------------------------- 1 | import ArbitratorApi from '../../../../src/contracts/abstractions/Arbitrator' 2 | import _asyncMockResponse from '../../../helpers/asyncMockResponse' 3 | 4 | describe('Arbitrator', () => { 5 | let arbitratorAddress = '0xDcB2db3E3fA7a6cba5dFE964408099d860246D7Z' 6 | let account = '0x' 7 | let arbitratorInstance 8 | 9 | beforeEach(async () => { 10 | arbitratorInstance = new ArbitratorApi({}, {}) 11 | }) 12 | 13 | describe('getDisputesForUser', async () => { 14 | it('has wrong period', async () => { 15 | const mockGetDisputesForUser = jest.fn() 16 | const mockShouldNotCall = jest.fn() 17 | const mockDispute = { 18 | arbitratorAddress: arbitratorAddress, 19 | disputeID: '1' 20 | } 21 | const mockStoreProvider = { 22 | getDisputes: mockGetDisputesForUser.mockReturnValue( 23 | _asyncMockResponse([mockDispute]) 24 | ), 25 | newUserProfile: mockShouldNotCall 26 | } 27 | 28 | arbitratorInstance.setStoreProviderInstance(mockStoreProvider) 29 | 30 | const mockArbitrator = { 31 | getPeriod: jest.fn().mockReturnValue(_asyncMockResponse(0)), 32 | getSession: jest.fn().mockReturnValue(_asyncMockResponse(1)), 33 | getDispute: jest.fn().mockReturnValue(_asyncMockResponse(mockDispute)), 34 | getContractAddress: jest.fn().mockReturnValue(arbitratorAddress) 35 | } 36 | arbitratorInstance._contractImplementation = mockArbitrator 37 | 38 | const disputes = await arbitratorInstance.getDisputesForUser( 39 | account, 40 | false 41 | ) 42 | 43 | expect(disputes.length).toBe(1) 44 | expect(disputes[0]).toEqual(mockDispute) 45 | expect(mockGetDisputesForUser.mock.calls.length).toBe(1) 46 | expect(mockShouldNotCall.mock.calls.length).toBe(0) 47 | }) 48 | 49 | it('has different arbitrator', async () => { 50 | const mockGetDisputesForUser = jest.fn() 51 | const mockShouldNotCall = jest.fn() 52 | const mockDispute = { 53 | arbitratorAddress: arbitratorAddress, 54 | disputeID: '1' 55 | } 56 | const mockStoreProvider = { 57 | getDisputes: mockGetDisputesForUser.mockReturnValue( 58 | _asyncMockResponse([mockDispute]) 59 | ), 60 | newUserProfile: mockShouldNotCall 61 | } 62 | 63 | arbitratorInstance.setStoreProviderInstance(mockStoreProvider) 64 | 65 | const mockArbitrator = { 66 | getPeriod: jest.fn().mockReturnValue(_asyncMockResponse(0)), 67 | getSession: jest.fn().mockReturnValue(_asyncMockResponse(1)), 68 | getDispute: jest.fn().mockReturnValue(_asyncMockResponse(mockDispute)), 69 | getContractAddress: jest.fn().mockReturnValue(arbitratorAddress + 'x') 70 | } 71 | arbitratorInstance._contractImplementation = mockArbitrator 72 | 73 | const disputes = await arbitratorInstance.getDisputesForUser( 74 | account, 75 | false 76 | ) 77 | 78 | expect(disputes.length).toBe(0) 79 | expect(mockGetDisputesForUser.mock.calls.length).toBe(1) 80 | expect(mockShouldNotCall.mock.calls.length).toBe(0) 81 | }) 82 | 83 | it('has wrong session. already updated store', async () => { 84 | const mockGetDisputesForUser = jest.fn() 85 | const mockSetUpUserProfile = jest.fn() 86 | const mockShouldNotCall = jest.fn() 87 | const mockDispute = { 88 | arbitratorAddress: arbitratorAddress, 89 | disputeID: '1' 90 | } 91 | const mockStoreProvider = { 92 | getDisputes: mockGetDisputesForUser.mockReturnValue( 93 | _asyncMockResponse([mockDispute]) 94 | ), 95 | newUserProfile: mockSetUpUserProfile.mockReturnValue( 96 | _asyncMockResponse({ 97 | session: 1 98 | }) 99 | ) 100 | } 101 | 102 | arbitratorInstance.setStoreProviderInstance(mockStoreProvider) 103 | 104 | const mockArbitrator = { 105 | getPeriod: jest.fn().mockReturnValue(_asyncMockResponse(2)), 106 | getSession: jest.fn().mockReturnValue(_asyncMockResponse(1)), 107 | getDispute: jest.fn().mockReturnValue(_asyncMockResponse(mockDispute)), 108 | getContractAddress: jest.fn().mockReturnValue(arbitratorAddress) 109 | } 110 | arbitratorInstance._contractImplementation = mockArbitrator 111 | 112 | const disputes = await arbitratorInstance.getDisputesForUser( 113 | account, 114 | false 115 | ) 116 | 117 | expect(disputes.length).toBe(1) 118 | expect(disputes[0]).toEqual(mockDispute) 119 | expect(mockGetDisputesForUser.mock.calls.length).toBe(1) 120 | expect(mockSetUpUserProfile.mock.calls.length).toBe(1) 121 | expect(mockShouldNotCall.mock.calls.length).toBe(0) 122 | }) 123 | 124 | it('has new disputes', async () => { 125 | const mockGetDisputesForUser = jest.fn() 126 | const mockSetUpUserProfile = jest.fn() 127 | const mockGetDisputesForJuror = jest.fn() 128 | const mockUpdateDisputeProfile = jest.fn() 129 | const mockUpdateSession = jest.fn() 130 | const mockAddNewDrawsDisputeProfile = jest.fn() 131 | const mockDispute = { 132 | arbitratorAddress: arbitratorAddress, 133 | disputeID: '1', 134 | appealDraws: [[1]], 135 | numberOfAppeals: 0 136 | } 137 | const mockStoreProvider = { 138 | getDisputes: mockGetDisputesForUser.mockReturnValue( 139 | _asyncMockResponse([mockDispute]) 140 | ), 141 | newUserProfile: mockSetUpUserProfile.mockReturnValue( 142 | _asyncMockResponse({ 143 | session: 1 144 | }) 145 | ), 146 | updateDisputeProfile: mockUpdateDisputeProfile, 147 | updateUserSession: mockUpdateSession, 148 | addNewDrawsDisputeProfile: mockAddNewDrawsDisputeProfile 149 | } 150 | 151 | arbitratorInstance.setStoreProviderInstance(mockStoreProvider) 152 | 153 | const mockArbitrator = { 154 | getPeriod: jest.fn().mockReturnValue(_asyncMockResponse(2)), 155 | getSession: jest.fn().mockReturnValue(_asyncMockResponse(2)), 156 | getDispute: jest.fn().mockReturnValue(_asyncMockResponse(mockDispute)), 157 | getDisputesForJuror: mockGetDisputesForJuror.mockReturnValue( 158 | _asyncMockResponse([mockDispute]) 159 | ), 160 | getDisputeCreationEvent: jest.fn().mockReturnValue({ blockNumber: 1 }), 161 | getContractAddress: jest.fn().mockReturnValue(arbitratorAddress) 162 | } 163 | arbitratorInstance._contractImplementation = mockArbitrator 164 | 165 | const disputes = await arbitratorInstance.getDisputesForUser( 166 | account, 167 | false 168 | ) 169 | 170 | expect(disputes.length).toBe(1) 171 | expect(disputes[0]).toEqual(mockDispute) 172 | expect(mockGetDisputesForUser.mock.calls.length).toBe(1) 173 | 174 | expect(mockSetUpUserProfile.mock.calls.length).toBe(1) 175 | expect(mockSetUpUserProfile.mock.calls[0][0]).toBe(account) 176 | 177 | expect(mockGetDisputesForJuror.mock.calls.length).toBe(1) 178 | expect(mockGetDisputesForJuror.mock.calls[0][0]).toBe(account) 179 | 180 | expect(mockUpdateDisputeProfile.mock.calls.length).toBe(1) 181 | expect(mockUpdateDisputeProfile.mock.calls[0][0]).toBe(account) 182 | expect(mockUpdateDisputeProfile.mock.calls[0][1]).toBe(arbitratorAddress) 183 | expect(mockUpdateDisputeProfile.mock.calls[0][2]).toBe( 184 | mockDispute.disputeID 185 | ) 186 | expect(mockUpdateDisputeProfile.mock.calls[0][3]).toEqual({ 187 | blockNumber: 1 188 | }) 189 | expect(mockAddNewDrawsDisputeProfile.mock.calls.length).toBe(1) 190 | expect(mockUpdateSession.mock.calls.length).toBe(1) 191 | }) 192 | }) 193 | }) 194 | -------------------------------------------------------------------------------- /tests/unit/resources/Disputes.test.js: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | 3 | import DisputesApi from '../../../src/resources/Disputes' 4 | import _asyncMockResponse from '../../helpers/asyncMockResponse' 5 | 6 | describe('Disputes', () => { 7 | let mockArbitratorWrapper = {} 8 | let mockArbitrableContractWrapper = {} 9 | let arbitratorAddress = '0xDcB2db3E3fA7a6cba5dFE964408099d860246D7Z' 10 | let arbitrableConctractAddress = '0xEcB2db3E3fA7a6cba5dFE964408099d860246D7Z' 11 | let account = '0x' 12 | let disputesInstance 13 | let web3 = new Web3() 14 | 15 | beforeEach(async () => { 16 | disputesInstance = new DisputesApi( 17 | mockArbitratorWrapper, 18 | mockArbitrableContractWrapper, 19 | {}, 20 | {} 21 | ) 22 | 23 | mockArbitratorWrapper = { 24 | address: arbitratorAddress 25 | } 26 | 27 | mockArbitrableContractWrapper = { 28 | address: arbitrableConctractAddress 29 | } 30 | 31 | disputesInstance.setArbitrator = mockArbitratorWrapper 32 | disputesInstance.setArbitrable = mockArbitrableContractWrapper 33 | }) 34 | 35 | describe('getDataForDispute', async () => { 36 | it('gets data for new dispute (no store data)', async () => { 37 | const disputeID = 0 38 | const arbitrableContractAddress = '0xfakeaddress' 39 | const session = 1 40 | const period = 0 41 | const numberOfAppeals = 0 42 | const rulingChoices = [0, 1] 43 | const initialNumberJurors = 3 44 | const arbitrationFeePerJuror = web3.toBigNumber(web3.toWei(0.15, 'ether')) 45 | const voteCounters = [] 46 | const partyA = '0x0' 47 | const partyB = '0x1' 48 | const appealDeadlines = 1 49 | const appealRuledAt = 2 50 | const appealCreatedAt = 3 51 | 52 | const mockArbitratorGetDispute = jest.fn().mockReturnValue( 53 | _asyncMockResponse({ 54 | arbitratorAddress, 55 | disputeID, 56 | arbitrableContractAddress, 57 | firstSession: session, 58 | numberOfAppeals, 59 | rulingChoices, 60 | initialNumberJurors, 61 | arbitrationFeePerJuror, 62 | state: 0, 63 | voteCounters, 64 | status: 0 65 | }) 66 | ) 67 | const mockArbitrator = { 68 | getVoteForJuror: jest.fn().mockReturnValue([0, 1]), 69 | getDispute: mockArbitratorGetDispute, 70 | getPeriod: jest.fn().mockReturnValue(period), 71 | getSession: jest.fn().mockReturnValue(session), 72 | currentRulingForDispute: jest.fn().mockReturnValue(0), 73 | canRuleDispute: jest.fn().mockReturnValue(false), 74 | getContractAddress: jest.fn().mockReturnValue(arbitratorAddress), 75 | getDisputeCreationEvent: jest.fn().mockReturnValue({ blockNumber: 1 }), 76 | getNetTokensForDispute: jest.fn().mockReturnValue(0), 77 | getAppealRuledAtTimestamp: jest.fn().mockReturnValue(appealRuledAt), 78 | getDisputeDeadlineTimestamp: jest.fn().mockReturnValue(appealDeadlines), 79 | getAppealCreationTimestamp: jest.fn().mockReturnValue(appealCreatedAt), 80 | _getTimestampForBlock: jest.fn().mockReturnValue(appealCreatedAt / 1000) 81 | } 82 | disputesInstance._ArbitratorInstance = mockArbitrator 83 | 84 | const mockArbitrableContractData = { 85 | metaEvidence: { 86 | title: 'test' 87 | } 88 | } 89 | const mockArbitrableContract = { 90 | getParties: jest.fn().mockReturnValue({ partyA, partyB }), 91 | getMetaEvidence: jest 92 | .fn() 93 | .mockReturnValue(mockArbitrableContractData.metaEvidence), 94 | getEvidence: jest.fn().mockReturnValue([]), 95 | setContractInstance: jest.fn() 96 | } 97 | disputesInstance._ArbitrableInstance = mockArbitrableContract 98 | 99 | const mockContract = { 100 | description: 'testdesc', 101 | email: 'testemail@test.com' 102 | } 103 | const mockStoreProvider = { 104 | getContractByAddress: jest.fn().mockReturnValue(mockContract) 105 | } 106 | disputesInstance.setStoreProviderInstance(mockStoreProvider) 107 | 108 | const disputeData = await disputesInstance.getDataForDispute( 109 | disputeID, 110 | account 111 | ) 112 | 113 | expect(disputeData).toBeTruthy() 114 | expect(disputeData.arbitrableContractAddress).toEqual( 115 | arbitrableContractAddress 116 | ) 117 | expect(disputeData.arbitratorAddress).toEqual(arbitratorAddress) 118 | expect(disputeData.parties.partyA).toEqual(partyA) 119 | expect(disputeData.parties.partyB).toEqual(partyB) 120 | expect(disputeData.disputeID).toEqual(disputeID) 121 | expect(disputeData.firstSession).toEqual(session) 122 | expect(disputeData.lastSession).toEqual(session) 123 | expect(disputeData.numberOfAppeals).toEqual(numberOfAppeals) 124 | expect(disputeData.disputeState).toEqual(0) 125 | expect(disputeData.disputeStatus).toEqual(0) 126 | expect(disputeData.appealJuror.length).toBe(1) 127 | expect(disputeData.appealRulings.length).toBe(1) 128 | expect(disputeData.evidence).toEqual([]) 129 | expect(disputeData.netPNK).toEqual(0) 130 | expect(disputeData.metaEvidence).toEqual( 131 | mockArbitrableContractData.metaEvidence 132 | ) 133 | }) 134 | it('gets data active dispute', async () => { 135 | const disputeID = 0 136 | const arbitrableContractAddress = '0xfakeaddress' 137 | const session = 1 138 | const period = 3 139 | const numberOfAppeals = 0 140 | const rulingChoices = [0, 1] 141 | const initialNumberJurors = 3 142 | const arbitrationFeePerJuror = web3.toBigNumber(web3.toWei(0.15, 'ether')) 143 | const voteCounters = [[2, 4]] 144 | const partyA = '0x0' 145 | const partyB = '0x1' 146 | const appealDeadlines = 1 147 | const appealRuledAt = null 148 | const appealCreatedAt = 3 149 | 150 | const mockArbitratorGetDispute = jest.fn().mockReturnValue( 151 | _asyncMockResponse({ 152 | arbitratorAddress, 153 | disputeID, 154 | arbitrableContractAddress, 155 | firstSession: session, 156 | numberOfAppeals, 157 | rulingChoices, 158 | initialNumberJurors, 159 | arbitrationFeePerJuror, 160 | state: 0, 161 | voteCounters, 162 | status: 0 163 | }) 164 | ) 165 | const mockArbitrator = { 166 | getVoteForJuror: jest.fn().mockReturnValue([0, 1]), 167 | getDispute: mockArbitratorGetDispute, 168 | getPeriod: jest.fn().mockReturnValue(period), 169 | getSession: jest.fn().mockReturnValue(session), 170 | currentRulingForDispute: jest.fn().mockReturnValue(1), 171 | canRuleDispute: jest.fn().mockReturnValue(true), 172 | getContractAddress: jest.fn().mockReturnValue(arbitratorAddress), 173 | getDisputeCreationEvent: jest.fn().mockReturnValue({ blockNumber: 1 }), 174 | getNetTokensForDispute: jest.fn().mockReturnValue(0), 175 | getAppealRuledAtTimestamp: jest.fn().mockReturnValue(appealRuledAt), 176 | getDisputeDeadlineTimestamp: jest.fn().mockReturnValue(appealDeadlines), 177 | getAppealCreationTimestamp: jest.fn().mockReturnValue(appealCreatedAt), 178 | _getTimestampForBlock: jest.fn().mockReturnValue(appealCreatedAt / 1000) 179 | } 180 | disputesInstance._ArbitratorInstance = mockArbitrator 181 | 182 | const mockArbitrableContractData = { 183 | metaEvidence: { 184 | title: 'test' 185 | } 186 | } 187 | const mockArbitrableContract = { 188 | getParties: jest.fn().mockReturnValue({ partyA, partyB }), 189 | getMetaEvidence: jest 190 | .fn() 191 | .mockReturnValue(mockArbitrableContractData.metaEvidence), 192 | getEvidence: jest.fn().mockReturnValue([]), 193 | setContractInstance: jest.fn() 194 | } 195 | disputesInstance._ArbitrableInstance = mockArbitrableContract 196 | 197 | const mockContract = { 198 | description: 'testdesc', 199 | email: 'testemail@test.com' 200 | } 201 | const mockUserData = { 202 | appealDraws: [[1, 2]] 203 | } 204 | const mockStoreProvider = { 205 | getContractByAddress: jest.fn().mockReturnValue(mockContract), 206 | getDispute: jest.fn().mockReturnValue(mockUserData) 207 | } 208 | disputesInstance.setStoreProviderInstance(mockStoreProvider) 209 | 210 | const disputeData = await disputesInstance.getDataForDispute( 211 | disputeID, 212 | account 213 | ) 214 | 215 | expect(disputeData).toBeTruthy() 216 | expect(disputeData.arbitrableContractAddress).toEqual( 217 | arbitrableContractAddress 218 | ) 219 | expect(disputeData.arbitratorAddress).toEqual(arbitratorAddress) 220 | expect(disputeData.parties.partyA).toEqual(partyA) 221 | expect(disputeData.parties.partyB).toEqual(partyB) 222 | expect(disputeData.disputeID).toEqual(disputeID) 223 | expect(disputeData.firstSession).toEqual(session) 224 | expect(disputeData.lastSession).toEqual(session) 225 | expect(disputeData.numberOfAppeals).toEqual(numberOfAppeals) 226 | expect(disputeData.disputeState).toEqual(0) 227 | expect(disputeData.disputeStatus).toEqual(0) 228 | 229 | expect(disputeData.appealJuror.length).toBe(1) 230 | const jurorData = disputeData.appealJuror[0] 231 | expect(jurorData.fee).toEqual( 232 | arbitrationFeePerJuror.mul( 233 | mockUserData.appealDraws[numberOfAppeals].length 234 | ) 235 | ) 236 | expect(jurorData.draws).toEqual(mockUserData.appealDraws[numberOfAppeals]) 237 | expect(jurorData.canRule).toBeTruthy() 238 | 239 | expect(disputeData.appealRulings.length).toBe(1) 240 | const appealData = disputeData.appealRulings[0] 241 | expect(appealData.voteCounter).toEqual(voteCounters[numberOfAppeals]) 242 | expect(appealData.ruledAt).toBeFalsy() 243 | expect(appealData.deadline).toEqual(appealDeadlines) 244 | expect(appealData.ruling).toEqual(2) 245 | expect(appealData.canRepartition).toBeFalsy() 246 | expect(appealData.canExecute).toBeFalsy() 247 | 248 | expect(disputeData.evidence).toEqual([]) 249 | expect(disputeData.netPNK).toEqual(0) 250 | expect(disputeData.metaEvidence).toEqual( 251 | mockArbitrableContractData.metaEvidence 252 | ) 253 | }) 254 | it('gets data ruled on dispute -- can repartition', async () => { 255 | const disputeID = 0 256 | const arbitrableContractAddress = '0xfakeaddress' 257 | const session = 1 258 | const period = 4 259 | const numberOfAppeals = 0 260 | const rulingChoices = [0, 1] 261 | const initialNumberJurors = 3 262 | const arbitrationFeePerJuror = web3.toBigNumber(web3.toWei(0.15, 'ether')) 263 | const voteCounters = [[4, 5]] 264 | const partyA = '0x0' 265 | const partyB = '0x1' 266 | const appealDeadlines = 1 267 | const appealRuledAt = 2 268 | const appealCreatedAt = 3 269 | 270 | const mockArbitratorGetDispute = jest.fn().mockReturnValue( 271 | _asyncMockResponse({ 272 | arbitratorAddress, 273 | disputeID, 274 | arbitrableContractAddress, 275 | firstSession: session, 276 | numberOfAppeals, 277 | rulingChoices, 278 | initialNumberJurors, 279 | arbitrationFeePerJuror, 280 | state: 0, 281 | voteCounters, 282 | status: 3 283 | }) 284 | ) 285 | const mockArbitrator = { 286 | getVoteForJuror: jest.fn().mockReturnValue([0, 1]), 287 | getDispute: mockArbitratorGetDispute, 288 | getPeriod: jest.fn().mockReturnValue(period), 289 | getSession: jest.fn().mockReturnValue(session), 290 | currentRulingForDispute: jest.fn().mockReturnValue(1), 291 | canRuleDispute: jest.fn().mockReturnValue(false), 292 | getContractAddress: jest.fn().mockReturnValue(arbitratorAddress), 293 | getDisputeCreationEvent: jest.fn().mockReturnValue({ blockNumber: 1 }), 294 | getNetTokensForDispute: jest.fn().mockReturnValue(0), 295 | getAppealRuledAtTimestamp: jest.fn().mockReturnValue(appealRuledAt), 296 | getDisputeDeadlineTimestamp: jest.fn().mockReturnValue(appealDeadlines), 297 | getAppealCreationTimestamp: jest.fn().mockReturnValue(appealCreatedAt), 298 | _getTimestampForBlock: jest.fn().mockReturnValue(appealCreatedAt / 1000) 299 | } 300 | disputesInstance._ArbitratorInstance = mockArbitrator 301 | 302 | const mockArbitrableContractData = { 303 | metaEvidence: { 304 | title: 'test' 305 | } 306 | } 307 | const mockArbitrableContract = { 308 | getParties: jest.fn().mockReturnValue({ partyA, partyB }), 309 | getMetaEvidence: jest 310 | .fn() 311 | .mockReturnValue(mockArbitrableContractData.metaEvidence), 312 | getEvidence: jest.fn().mockReturnValue([]), 313 | setContractInstance: jest.fn() 314 | } 315 | disputesInstance._ArbitrableInstance = mockArbitrableContract 316 | 317 | const mockContract = { 318 | description: 'testdesc', 319 | email: 'testemail@test.com' 320 | } 321 | const mockUserData = { 322 | appealDraws: [[1, 2]], 323 | appealCreatedAt: [123], 324 | appealDeadlines: [456], 325 | appealRuledAt: [789], 326 | netPNK: 2 327 | } 328 | const mockStoreProvider = { 329 | getContractByAddress: jest.fn().mockReturnValue(mockContract), 330 | getDispute: jest.fn().mockReturnValue(mockUserData) 331 | } 332 | disputesInstance.setStoreProviderInstance(mockStoreProvider) 333 | 334 | const disputeData = await disputesInstance.getDataForDispute( 335 | disputeID, 336 | account 337 | ) 338 | 339 | expect(disputeData).toBeTruthy() 340 | expect(disputeData.arbitrableContractAddress).toEqual( 341 | arbitrableContractAddress 342 | ) 343 | expect(disputeData.arbitratorAddress).toEqual(arbitratorAddress) 344 | expect(disputeData.parties.partyA).toEqual(partyA) 345 | expect(disputeData.parties.partyB).toEqual(partyB) 346 | expect(disputeData.disputeID).toEqual(disputeID) 347 | expect(disputeData.firstSession).toEqual(session) 348 | expect(disputeData.lastSession).toEqual(session) 349 | expect(disputeData.numberOfAppeals).toEqual(numberOfAppeals) 350 | expect(disputeData.disputeState).toEqual(0) 351 | expect(disputeData.disputeStatus).toEqual(3) 352 | 353 | expect(disputeData.appealJuror.length).toBe(1) 354 | const jurorData = disputeData.appealJuror[0] 355 | expect(jurorData.fee).toEqual( 356 | arbitrationFeePerJuror.mul( 357 | mockUserData.appealDraws[numberOfAppeals].length 358 | ) 359 | ) 360 | expect(jurorData.draws).toEqual(mockUserData.appealDraws[numberOfAppeals]) 361 | expect(jurorData.canRule).toBeFalsy() 362 | 363 | expect(disputeData.appealRulings.length).toBe(1) 364 | const appealData = disputeData.appealRulings[0] 365 | expect(appealData.voteCounter).toEqual(voteCounters[numberOfAppeals]) 366 | expect(appealData.ruling).toEqual(2) 367 | expect(appealData.canRepartition).toBeTruthy() 368 | expect(appealData.canExecute).toBeFalsy() 369 | expect(disputeData.evidence).toEqual([]) 370 | }) 371 | it('gets data ruled on dispute -- can execute', async () => { 372 | const disputeID = 0 373 | const arbitrableContractAddress = '0xfakeaddress' 374 | const session = 1 375 | const period = 4 376 | const numberOfAppeals = 0 377 | const rulingChoices = [0, 1] 378 | const initialNumberJurors = 3 379 | const arbitrationFeePerJuror = web3.toBigNumber(web3.toWei(0.15, 'ether')) 380 | const voteCounters = [[4, 5]] 381 | const partyA = '0x0' 382 | const partyB = '0x1' 383 | const appealDeadlines = 1 384 | const appealRuledAt = 2 385 | const appealCreatedAt = 3 386 | 387 | const mockArbitratorGetDispute = jest.fn().mockReturnValue( 388 | _asyncMockResponse({ 389 | arbitratorAddress, 390 | disputeID, 391 | arbitrableContractAddress, 392 | firstSession: session, 393 | numberOfAppeals, 394 | rulingChoices, 395 | initialNumberJurors, 396 | arbitrationFeePerJuror, 397 | state: 2, 398 | voteCounters, 399 | status: 3 400 | }) 401 | ) 402 | const mockArbitrator = { 403 | getVoteForJuror: jest.fn().mockReturnValue([0, 1]), 404 | getDispute: mockArbitratorGetDispute, 405 | getPeriod: jest.fn().mockReturnValue(period), 406 | getSession: jest.fn().mockReturnValue(session), 407 | currentRulingForDispute: jest.fn().mockReturnValue(1), 408 | canRuleDispute: jest.fn().mockReturnValue(false), 409 | getContractAddress: jest.fn().mockReturnValue(arbitratorAddress), 410 | getDisputeCreationEvent: jest.fn().mockReturnValue({ blockNumber: 1 }), 411 | getNetTokensForDispute: jest.fn().mockReturnValue(0), 412 | getAppealRuledAtTimestamp: jest.fn().mockReturnValue(appealRuledAt), 413 | getDisputeDeadlineTimestamp: jest.fn().mockReturnValue(appealDeadlines), 414 | getAppealCreationTimestamp: jest.fn().mockReturnValue(appealCreatedAt), 415 | _getTimestampForBlock: jest.fn().mockReturnValue(appealCreatedAt / 1000) 416 | } 417 | disputesInstance._ArbitratorInstance = mockArbitrator 418 | 419 | const mockArbitrableContractData = { 420 | metaEvidence: { 421 | title: 'test' 422 | } 423 | } 424 | const mockArbitrableContract = { 425 | getParties: jest.fn().mockReturnValue({ partyA, partyB }), 426 | getMetaEvidence: jest 427 | .fn() 428 | .mockReturnValue(mockArbitrableContractData.metaEvidence), 429 | getEvidence: jest.fn().mockReturnValue([]), 430 | setContractInstance: jest.fn() 431 | } 432 | disputesInstance._ArbitrableInstance = mockArbitrableContract 433 | 434 | const mockContract = { 435 | description: 'testdesc', 436 | email: 'testemail@test.com' 437 | } 438 | const mockUserData = { 439 | appealDraws: [[1, 2]], 440 | appealCreatedAt: [123], 441 | appealDeadlines: [456], 442 | appealRuledAt: [789], 443 | lastBlock: 1 444 | } 445 | const mockStoreProvider = { 446 | getContractByAddress: jest.fn().mockReturnValue(mockContract), 447 | getDispute: jest.fn().mockReturnValue(mockUserData) 448 | } 449 | disputesInstance.setStoreProviderInstance(mockStoreProvider) 450 | 451 | const disputeData = await disputesInstance.getDataForDispute( 452 | disputeID, 453 | account 454 | ) 455 | 456 | expect(disputeData).toBeTruthy() 457 | expect(disputeData.arbitrableContractAddress).toEqual( 458 | arbitrableContractAddress 459 | ) 460 | expect(disputeData.arbitratorAddress).toEqual(arbitratorAddress) 461 | expect(disputeData.disputeID).toEqual(disputeID) 462 | expect(disputeData.firstSession).toEqual(session) 463 | expect(disputeData.lastSession).toEqual(session) 464 | expect(disputeData.numberOfAppeals).toEqual(numberOfAppeals) 465 | expect(disputeData.disputeState).toEqual(2) 466 | expect(disputeData.disputeStatus).toEqual(3) 467 | 468 | expect(disputeData.appealJuror.length).toBe(1) 469 | const jurorData = disputeData.appealJuror[0] 470 | expect(jurorData.fee).toEqual( 471 | arbitrationFeePerJuror.mul( 472 | mockUserData.appealDraws[numberOfAppeals].length 473 | ) 474 | ) 475 | expect(jurorData.draws).toEqual(mockUserData.appealDraws[numberOfAppeals]) 476 | expect(jurorData.canRule).toBeFalsy() 477 | 478 | expect(disputeData.appealRulings.length).toBe(1) 479 | const appealData = disputeData.appealRulings[0] 480 | expect(appealData.voteCounter).toEqual(voteCounters[numberOfAppeals]) 481 | expect(appealData.ruling).toEqual(2) 482 | expect(appealData.canRepartition).toBeFalsy() 483 | expect(appealData.canExecute).toBeTruthy() 484 | expect(disputeData.evidence).toEqual([]) 485 | expect(disputeData.metaEvidence).toEqual( 486 | mockArbitrableContractData.metaEvidence 487 | ) 488 | }) 489 | }) 490 | }) 491 | -------------------------------------------------------------------------------- /tests/unit/utils/PromiseQueue.test.js: -------------------------------------------------------------------------------- 1 | import PromiseQueue from '../../../src/utils/PromiseQueue' 2 | import delaySecond from '../../helpers/delaySecond' 3 | 4 | describe('PromiseQueue', () => { 5 | let promiseQueue 6 | 7 | beforeEach(() => { 8 | promiseQueue = new PromiseQueue() 9 | }) 10 | 11 | it('queue keeps promises in order', async () => { 12 | const resolvedValues = [] 13 | 14 | const promiseA = () => 15 | new Promise(resolve => { 16 | setTimeout(() => { 17 | resolvedValues.push(1) 18 | resolve() 19 | }, 500) 20 | }) 21 | 22 | const promiseB = () => 23 | new Promise(resolve => { 24 | setTimeout(() => { 25 | resolvedValues.push(2) 26 | resolve() 27 | }, 100) 28 | }) 29 | 30 | promiseQueue.push(promiseA) 31 | promiseQueue.push(promiseB) 32 | 33 | await delaySecond(1) 34 | expect(resolvedValues).toEqual([1, 2]) 35 | }) 36 | 37 | it('get return value from promise in queue', async () => { 38 | const resolvedValues = [] 39 | 40 | const promiseA = () => 41 | new Promise(resolve => { 42 | setTimeout(() => { 43 | resolvedValues.push(1) 44 | resolve(1) 45 | }, 100) 46 | }) 47 | 48 | const promiseB = () => 49 | new Promise(resolve => { 50 | setTimeout(() => { 51 | resolvedValues.push(2) 52 | resolve(2) 53 | }, 500) 54 | }) 55 | 56 | const promiseC = () => 57 | new Promise(resolve => { 58 | setTimeout(() => { 59 | resolvedValues.push(3) 60 | resolve(3) 61 | }, 100) 62 | }) 63 | 64 | promiseQueue.push(promiseA) 65 | const result = await promiseQueue.fetch(promiseB) 66 | promiseQueue.push(promiseC) 67 | await delaySecond(2) 68 | expect(result).toEqual(2) 69 | expect(resolvedValues).toEqual([1, 2, 3]) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | output: { 6 | path: resolve(__dirname, 'umd/'), 7 | filename: 'kleros-api.js', 8 | libraryTarget: 'umd', 9 | umdNamedDefine: true, 10 | library: 'KlerosAPI' 11 | }, 12 | 13 | devtool: 'source-map', 14 | 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.js$/, 19 | loader: 'babel-loader', 20 | exclude: /node_modules/ 21 | } 22 | ] 23 | } 24 | } 25 | --------------------------------------------------------------------------------