From 64a96cf7cb3a0eb85526c789b3e97e20e1dff9f9 Mon Sep 17 00:00:00 2001 From: David Wolever Date: Wed, 6 Mar 2019 18:45:01 -0500 Subject: [PATCH 1/4] Copy SpankChain client at rev: bfc927ac27ae13078b134ddb86cc7539b9248422 --- .gitignore | 27 + modules/client/.gitignore | 3 + modules/client/README.rst | 131 + modules/client/env-vars | 44 + modules/client/notes.md | 6 + modules/client/package-lock.json | 3480 +++++++++++++++++ modules/client/package.json | 58 + modules/client/src/.Connext.ts.swo | Bin 0 -> 24576 bytes modules/client/src/Connext.test.ts.todo | 621 +++ modules/client/src/Connext.ts | 759 ++++ modules/client/src/StateGenerator.test.ts | 694 ++++ modules/client/src/StateGenerator.ts | 636 +++ modules/client/src/Utils.test.ts | 87 + modules/client/src/Utils.ts | 298 ++ .../client/src/contract/ChannelManager.d.ts | 267 ++ .../client/src/contract/ChannelManagerAbi.ts | 1071 +++++ modules/client/src/contract/README.txt | 4 + .../src/controllers/.DepositController.ts.swo | Bin 0 -> 16384 bytes .../controllers/AbstractController.test.ts | 9 + .../src/controllers/AbstractController.ts | 35 + .../src/controllers/BuyController.test.ts | 188 + .../client/src/controllers/BuyController.ts | 58 + .../controllers/CollateralController.test.ts | 36 + .../src/controllers/CollateralController.ts | 12 + .../src/controllers/DepositController.test.ts | 74 + .../src/controllers/DepositController.ts | 155 + .../controllers/ExchangeController.test.ts | 44 + .../src/controllers/ExchangeController.ts | 84 + .../controllers/StateUpdateController.test.ts | 93 + .../src/controllers/StateUpdateController.ts | 486 +++ .../src/controllers/SyncController.test.ts | 457 +++ .../client/src/controllers/SyncController.ts | 575 +++ .../src/controllers/ThreadsController.ts.todo | 81 + .../controllers/WithdrawalController.test.ts | 62 + .../src/controllers/WithdrawalController.ts | 26 + modules/client/src/hasPendingOps.test.ts | 20 + modules/client/src/hasPendingOps.ts | 12 + modules/client/src/helpers/bn.ts | 37 + modules/client/src/helpers/merkleTree.ts | 101 + modules/client/src/helpers/merkleUtils.ts | 65 + modules/client/src/helpers/naming.ts | 3 + modules/client/src/helpers/networking.ts | 64 + modules/client/src/lib/Logger.ts | 4 + modules/client/src/lib/constants.ts | 44 + .../lib/currency/.CurrencyConvertable.ts.swo | Bin 0 -> 12288 bytes .../client/src/lib/currency/Currency.test.ts | 56 + modules/client/src/lib/currency/Currency.ts | 197 + .../lib/currency/CurrencyConvertable.test.ts | 104 + .../src/lib/currency/CurrencyConvertable.ts | 89 + .../src/lib/currency/bootyToBEI.test.ts | 36 + modules/client/src/lib/currency/bootyToBEI.ts | 25 + modules/client/src/lib/getChannel.ts | 6 + modules/client/src/lib/getExchangeRates.ts | 14 + modules/client/src/lib/getLastThreadId.ts | 5 + modules/client/src/lib/getTxCount.ts | 5 + .../client/src/lib/getUpdateRequestTimeout.ts | 11 + modules/client/src/lib/math.test.ts | 71 + modules/client/src/lib/math.ts | 54 + modules/client/src/lib/poller/Poller.test.ts | 67 + modules/client/src/lib/poller/Poller.ts | 93 + modules/client/src/lib/timestamp.ts | 21 + modules/client/src/lib/utils.test.ts | 22 + modules/client/src/lib/utils.ts | 257 ++ modules/client/src/lib/web3/toFinney.test.ts | 11 + modules/client/src/lib/web3/toFinney.ts | 10 + modules/client/src/register/common.ts | 33 + modules/client/src/register/testing.ts | 11 + .../src/state/ConnextState/CurrencyTypes.ts | 10 + .../src/state/ConnextState/ExchangeRates.ts | 11 + modules/client/src/state/actions.test.ts | 18 + modules/client/src/state/actions.ts | 68 + modules/client/src/state/middleware.ts | 84 + modules/client/src/state/reducers.ts | 44 + modules/client/src/state/store.ts | 94 + .../src/testing/generateExchangeRates.ts | 17 + modules/client/src/testing/index.test.ts | 168 + modules/client/src/testing/index.ts | 837 ++++ modules/client/src/testing/mocks.ts | 579 +++ modules/client/src/types.test.ts | 55 + modules/client/src/types.ts | 995 +++++ modules/client/src/validator.test.ts | 1313 +++++++ modules/client/src/validator.ts | 1031 +++++ modules/client/tsconfig.json | 41 + 83 files changed, 17574 insertions(+) create mode 100644 .gitignore create mode 100644 modules/client/.gitignore create mode 100644 modules/client/README.rst create mode 100644 modules/client/env-vars create mode 100644 modules/client/notes.md create mode 100644 modules/client/package-lock.json create mode 100644 modules/client/package.json create mode 100644 modules/client/src/.Connext.ts.swo create mode 100644 modules/client/src/Connext.test.ts.todo create mode 100644 modules/client/src/Connext.ts create mode 100644 modules/client/src/StateGenerator.test.ts create mode 100644 modules/client/src/StateGenerator.ts create mode 100644 modules/client/src/Utils.test.ts create mode 100644 modules/client/src/Utils.ts create mode 100644 modules/client/src/contract/ChannelManager.d.ts create mode 100644 modules/client/src/contract/ChannelManagerAbi.ts create mode 100644 modules/client/src/contract/README.txt create mode 100644 modules/client/src/controllers/.DepositController.ts.swo create mode 100644 modules/client/src/controllers/AbstractController.test.ts create mode 100644 modules/client/src/controllers/AbstractController.ts create mode 100644 modules/client/src/controllers/BuyController.test.ts create mode 100644 modules/client/src/controllers/BuyController.ts create mode 100644 modules/client/src/controllers/CollateralController.test.ts create mode 100644 modules/client/src/controllers/CollateralController.ts create mode 100644 modules/client/src/controllers/DepositController.test.ts create mode 100644 modules/client/src/controllers/DepositController.ts create mode 100644 modules/client/src/controllers/ExchangeController.test.ts create mode 100644 modules/client/src/controllers/ExchangeController.ts create mode 100644 modules/client/src/controllers/StateUpdateController.test.ts create mode 100644 modules/client/src/controllers/StateUpdateController.ts create mode 100644 modules/client/src/controllers/SyncController.test.ts create mode 100644 modules/client/src/controllers/SyncController.ts create mode 100644 modules/client/src/controllers/ThreadsController.ts.todo create mode 100644 modules/client/src/controllers/WithdrawalController.test.ts create mode 100644 modules/client/src/controllers/WithdrawalController.ts create mode 100644 modules/client/src/hasPendingOps.test.ts create mode 100644 modules/client/src/hasPendingOps.ts create mode 100644 modules/client/src/helpers/bn.ts create mode 100644 modules/client/src/helpers/merkleTree.ts create mode 100644 modules/client/src/helpers/merkleUtils.ts create mode 100644 modules/client/src/helpers/naming.ts create mode 100644 modules/client/src/helpers/networking.ts create mode 100644 modules/client/src/lib/Logger.ts create mode 100644 modules/client/src/lib/constants.ts create mode 100644 modules/client/src/lib/currency/.CurrencyConvertable.ts.swo create mode 100644 modules/client/src/lib/currency/Currency.test.ts create mode 100644 modules/client/src/lib/currency/Currency.ts create mode 100644 modules/client/src/lib/currency/CurrencyConvertable.test.ts create mode 100644 modules/client/src/lib/currency/CurrencyConvertable.ts create mode 100644 modules/client/src/lib/currency/bootyToBEI.test.ts create mode 100644 modules/client/src/lib/currency/bootyToBEI.ts create mode 100644 modules/client/src/lib/getChannel.ts create mode 100644 modules/client/src/lib/getExchangeRates.ts create mode 100644 modules/client/src/lib/getLastThreadId.ts create mode 100644 modules/client/src/lib/getTxCount.ts create mode 100644 modules/client/src/lib/getUpdateRequestTimeout.ts create mode 100644 modules/client/src/lib/math.test.ts create mode 100644 modules/client/src/lib/math.ts create mode 100644 modules/client/src/lib/poller/Poller.test.ts create mode 100644 modules/client/src/lib/poller/Poller.ts create mode 100644 modules/client/src/lib/timestamp.ts create mode 100644 modules/client/src/lib/utils.test.ts create mode 100644 modules/client/src/lib/utils.ts create mode 100644 modules/client/src/lib/web3/toFinney.test.ts create mode 100644 modules/client/src/lib/web3/toFinney.ts create mode 100644 modules/client/src/register/common.ts create mode 100644 modules/client/src/register/testing.ts create mode 100644 modules/client/src/state/ConnextState/CurrencyTypes.ts create mode 100644 modules/client/src/state/ConnextState/ExchangeRates.ts create mode 100644 modules/client/src/state/actions.test.ts create mode 100644 modules/client/src/state/actions.ts create mode 100644 modules/client/src/state/middleware.ts create mode 100644 modules/client/src/state/reducers.ts create mode 100644 modules/client/src/state/store.ts create mode 100644 modules/client/src/testing/generateExchangeRates.ts create mode 100644 modules/client/src/testing/index.test.ts create mode 100644 modules/client/src/testing/index.ts create mode 100644 modules/client/src/testing/mocks.ts create mode 100644 modules/client/src/types.test.ts create mode 100644 modules/client/src/types.ts create mode 100644 modules/client/src/validator.test.ts create mode 100644 modules/client/src/validator.ts create mode 100644 modules/client/tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..031828af84 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +build +dist +ganache*.log +node_modules +.bash_history +.config +.env +.node-gyp +.npm +.npmrc +.node_repl_history +*.swp +**/*.DS_Store +modules/hub/development/data +modules/wallet/.env + +# per-project address book overrides +address-book.json + +cypress/screenshots +cypress/videos +cypress/integration/examples + +modules/database/snapshots/* + +# remove next line once hub is decoupled from spankchain +modules/hub/package-lock.json diff --git a/modules/client/.gitignore b/modules/client/.gitignore new file mode 100644 index 0000000000..93cc1e8b27 --- /dev/null +++ b/modules/client/.gitignore @@ -0,0 +1,3 @@ +.yalc/ +yalc.lock +.env \ No newline at end of file diff --git a/modules/client/README.rst b/modules/client/README.rst new file mode 100644 index 0000000000..aa2294dcdc --- /dev/null +++ b/modules/client/README.rst @@ -0,0 +1,131 @@ +Testing + +There are a number of helper functions to make testing easier:: + + import { assert, getChannelState, updateState, assertStateEqual } from 'client/testing' + + describe('confirm deposit', () => { + const preDepositState = getChannelState('empty', { + balanceWei: [0, 0], + pendingDepositWei: [6, 9], + }) + + it('should add the correct amount', () => { + let actual = confirmDeposit(preDepositState) + assertStateEqual(actual, { + balanceWei: [6, 9], + pendingDepositWei: [0, 0], + }) + }) + }) + +Notice that: + +1. The testing library provides four "default" states: "empty" (where all fields + are zero, except for the ``txCount``, which is ``[1, 1]`` or ``1`` for channels and threads, respectively), and "full", where + each field has a unique value (this is useful for testing, ex, signature + functions). + + ``empty`` channel state:: + + contractAddress: '0xCCC0000000000000000000000000000000000000' + user: '0xAAA0000000000000000000000000000000000000' + recipient: '0x2220000000000000000000000000000000000000' + balanceWei: [ '0', '0' ] + balanceToken: [ '0', '0' ] + pendingDepositWei: [ '0', '0' ] + pendingDepositToken: [ '0', '0' ] + pendingWithdrawalWei: [ '0', '0' ] + pendingWithdrawalToken: [ '0', '0' ] + txCount: [ 1, 1 ], + threadRoot: '0x0000000000000000000000000000000000000000000000000000000000000000' + threadCount: 0 + timeout: 0 + sig: [ '', '' ] + + + ``full`` channel state:: + + contractAddress: '0xCCC0000000000000000000000000000000000000' + user: '0xAAA0000000000000000000000000000000000000' + recipient: '0x2220000000000000000000000000000000000000' + balanceWei: [ '1', '2' ] + balanceToken: [ '3', '4' ] + pendingDepositWei: [ '4', '5' ] + pendingDepositToken: [ '6', '7' ] + pendingWithdrawalWei: [ '8', '9' ] + pendingWithdrawalToken: [ '10', '11' ] + txCount: [ 13, 12 ] + threadRoot: '0x1414140000000000000000000000000000000000000000000000000000000000' + threadCount: 14 + timeout: 15 + sig: [ 'sighub0000000000000000000000000000000000000000000000000000000000', 'siguser0000000000000000000000000000000000000000000000000000000000' ] + + + ``empty`` thread state:: + + contractAddress: '0xCCC0000000000000000000000000000000000000' + user: '0xAAA0000000000000000000000000000000000000' + sender: '0x2220000000000000000000000000000000000000' + receiver: '0x3330000000000000000000000000000000000000' + txCount: 1 + balanceWei: [ '0', '0' ] + balanceToken: [ '0', '0' ] + sigA: '' + + + ``full`` thread state:: + + contractAddress: '0xCCC0000000000000000000000000000000000000' + user: '0xAAA0000000000000000000000000000000000000' + sender: '0x2220000000000000000000000000000000000000' + receiver: '0x3330000000000000000000000000000000000000' + balanceWei: [ '1', '2' ] + balanceToken: [ '3', '4' ] + txCount: 22 + sigA: 'sigA0000000000000000000000000000000000000000000000000000000000' + +2. All operations support "shorthands" for values; internally, ``balanceWei: + [5, 10]`` is expanded to ``balanceWeiHub: 6, balanceWeiUser: 9``. This is done + through two functions: ``expandChannelSuccinct``, ``expandThreadSuccinct``, which expands the fields in a + "succinct" state to a verbose state, and ``makeSuccinctChannel`` and ``makeSuccinctThread``, which do the + opposite. + + Note that these functions can accept partial states, and combinations of + succinct and verbose states. + + Additionally, they will always normalize numeric values to strings. + + For example:: + + + > verbose = expandSuccinctChannel({ + . balanceWei: [6, 9], + . balanceTokenUser: 69, + . timout: 5, + . }) + > verbose + { + balanceWeiHub: '6', + balanceTokenUser: '9', + balanceTokenUser: '69', + timeout: 5, + } + > makeSuccinctChannel(verbose) + { + balanceWei: ['6', '9'], + balanceToken: ['0', '69'], + timeout: 5, + } + +Additionally, useful helper functions: + +* ``mkAddress(prefix)``: Generates an address by suffixing ``prefix`` with zeros:: + + > mkAddress('0x1234') + '0x1234000000000000000000000000000000000000' + +* ``mkHash(prefix)``: Generates a hash by suffixing ``prefix`` with zeros:: + + > mkHash('0xab') + '0xab00000000000000000000000000000000000000000000000000000000000000' diff --git a/modules/client/env-vars b/modules/client/env-vars new file mode 100644 index 0000000000..c799f53ca1 --- /dev/null +++ b/modules/client/env-vars @@ -0,0 +1,44 @@ +HUB_ADDRESS = 0x627306090abab3a6e1400e9345bc60c78a8bef57 +CONTRACT_ADDRESS = +TOKEN_ADDRESS = +HUB_URL = + +Ganache: +Available Accounts +================== +(0) 0x627306090abab3a6e1400e9345bc60c78a8bef57 +(1) 0xf17f52151ebef6c7334fad080c5704d77216b732 +(2) 0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef +(3) 0x821aea9a577a9b44299b9c15c88cf3087f3b5544 +(4) 0x0d1d4e623d10f9fba5db95830f7d3839406c6af2 +(5) 0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e +(6) 0x2191ef87e392377ec08e7c08eb105ef5448eced5 +(7) 0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5 +(8) 0x6330a553fc93768f612722bb8c2ec78ac90b3bbc +(9) 0x5aeda56215b167893e80b4fe645ba6d5bab767de + +Private Keys +================== +(0) c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3 +(1) ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f +(2) 0dbbe8e4ae425a6d2687f1a7e3ba17bc98c673636790f1b8ad91193c05875ef1 +(3) c88b703fb08cbea894b6aeff5a544fb92e78a18e19814cd85da83b71trf772aa6c +(4) 388c684f0ba1ef5017716adb5d21a053ea8e90277d0868337519f97bede61418 +(5) 659cbb0e2411a44db63778987b1e22153c086a95eb6b18bdf89de078917abc63 +(6) 82d052c865f5763aad42add438569276c00d3d88a2d062d36b2bae914d58b8c8 +(7) aa3680d5d48a8283413f7a108367c7299ca73f553735860a87b08f39395618b7 +(8) 0f62d96d6675f32685bbdb8ac13cda7c23436f63efbb9d07700d8669ff12b7c4 +(9) 8d5366123cb560bb606379f90a0bfd4769eecc0557f1b362dcae9012b548b1e5 + +HD Wallet +================== +Mnemonic: candy maple cake sugar pudding cream honey rich smooth crumble sweet treat +Base HD Path: m/44'/60'/0'/0/{account_index} + +Gas Price +================== +20000000000 + +Gas Limit +================== +6721975 \ No newline at end of file diff --git a/modules/client/notes.md b/modules/client/notes.md new file mode 100644 index 0000000000..7ec00413ee --- /dev/null +++ b/modules/client/notes.md @@ -0,0 +1,6 @@ +1. Utils is not a static library because we want to mock it, which means it + needs to be aligned with our dependency injection framework. + - mocking it in the hub test to generate fake state updates that pass + validation +2. Validate exchange BN division and rounding between Hub and Client +3. ExchangeArgumentsBN -> ExchangeArgsBN diff --git a/modules/client/package-lock.json b/modules/client/package-lock.json new file mode 100644 index 0000000000..612b42c4cc --- /dev/null +++ b/modules/client/package-lock.json @@ -0,0 +1,3480 @@ +{ + "name": "@spankchain/connext-client", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@sinonjs/commons": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.3.0.tgz", + "integrity": "sha512-j4ZwhaHmwsCb4DlDOIWnI5YyKDNMoNThsmwEpfHx6a1EpsGZ9qYLxP++LMlmBRjtGptGHFsGItJ768snllFWpA==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.1.0.tgz", + "integrity": "sha512-ZAR2bPHOl4Xg6eklUGpsdiIJ4+J1SNag1DHHrG/73Uz/nVwXqjgUtRPLoS+aVyieN9cSbc0E4LsU984tWcDyNg==", + "dev": true, + "requires": { + "@sinonjs/samsam": "3.0.2" + } + }, + "@sinonjs/samsam": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.0.2.tgz", + "integrity": "sha512-m08g4CS3J6lwRQk1pj1EO+KEVWbrbXsmi9Pw0ySmrIbcVxVaedoFgLvFsV8wHLwh01EpROVz3KvVcD1Jmks9FQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "1.3.0", + "array-from": "2.1.1", + "lodash.get": "4.4.2" + } + }, + "@types/bn.js": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.4.tgz", + "integrity": "sha512-AO8WW+aRcKWKQAYTfKLzwnpL6U+TfPqS+haRrhCy5ff04Da8WZud3ZgVjspQXaEXJDcTlsjUEVvL39wegDek5w==", + "dev": true, + "requires": { + "@types/node": "10.12.19" + } + }, + "@types/chai": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", + "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", + "dev": true + }, + "@types/chai-as-promised": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.0.tgz", + "integrity": "sha512-MFiW54UOSt+f2bRw8J7LgQeIvE/9b4oGvwU7XW30S9QGAiHGnU/fmiOprsyMkdmH2rl8xSPc0/yrQw8juXU6bQ==", + "dev": true, + "requires": { + "@types/chai": "4.1.7" + } + }, + "@types/chai-subset": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.1.tgz", + "integrity": "sha512-Aof+FLfWzBPzDgJ2uuBuPNOBHVx9Siyw4vmOcsMgsuxX1nfUWSlzpq4pdvQiaBgGjGS7vP/Oft5dpJbX4krT1A==", + "dev": true, + "requires": { + "@types/chai": "4.1.7" + } + }, + "@types/ethereumjs-util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@types/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", + "integrity": "sha512-qwQgQqXXTRv2h2AlJef+tMEszLFkCB9dWnrJYIdAwqjubERXEc/geB+S3apRw0yQyTVnsBf8r6BhlrE8vx+3WQ==", + "dev": true, + "requires": { + "@types/bn.js": "4.11.4", + "@types/node": "10.12.19" + } + }, + "@types/mkdirp": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-0.5.2.tgz", + "integrity": "sha512-U5icWpv7YnZYGsN4/cmh3WD2onMY0aJIiTE6+51TwJCttdHvtCYmkBNOobHlXwrJRL0nkH9jH4kD+1FAdMN4Tg==", + "dev": true, + "requires": { + "@types/node": "10.12.19" + } + }, + "@types/mocha": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.5.tgz", + "integrity": "sha512-lAVp+Kj54ui/vLUFxsJTMtWvZraZxum3w3Nwkble2dNuV5VnPA+Mi2oGX9XYJAaIvZi3tn3cbjS/qcJXRb6Bww==", + "dev": true + }, + "@types/node": { + "version": "10.12.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.19.tgz", + "integrity": "sha512-2NVovndCjJQj6fUUn9jCgpP4WSqr+u1SoUZMZyJkhGeBFsm6dE46l31S7lPUYt9uQ28XI+ibrJA1f5XyH5HNtA==" + }, + "@types/prettier": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.15.2.tgz", + "integrity": "sha512-XIB0ZCaFZmWUHAa9dBqP5UKXXHwuukmVlP+XcyU94dui2k+l2lG+CHAbt2ffenHPUqoIs5Beh8Pdf2YEq/CZ7A==", + "dev": true + }, + "@types/redux-mock-store": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.0.tgz", + "integrity": "sha512-7+H3+O8VX4Mx2HNdDLP1MSNoWp+FXfq3HDGc08kY5vxyuml7OAudO4CAQFsKsDvbU5spApJMZ6buEi/c3hKjtQ==", + "dev": true, + "requires": { + "redux": "4.0.1" + } + }, + "@types/resolve": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", + "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", + "dev": true, + "requires": { + "@types/node": "10.12.19" + } + }, + "@types/semaphore": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/semaphore/-/semaphore-1.1.0.tgz", + "integrity": "sha512-YD+lyrPhrsJdSOaxmA9K1lzsCoN0J29IsQGMKd67SbkPDXxJPdwdqpok1sytD19NEozUaFpjIsKOWnJDOYO/GA==", + "dev": true + }, + "@types/sinon": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-5.0.7.tgz", + "integrity": "sha512-opwMHufhUwkn/UUDk35LDbKJpA2VBsZT8WLU8NjayvRLGPxQkN+8XmfC2Xl35MAscBE8469koLLBjaI3XLEIww==", + "dev": true + }, + "@types/underscore": { + "version": "1.8.9", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.8.9.tgz", + "integrity": "sha512-vfzZGgZKRFy7KEWcBGfIFk+h6B+thDCLfkD1exMBMRlUsx2icA+J6y4kAbZs/TjSTeY1duw89QUU133TSzr60Q==", + "dev": true + }, + "@types/web3": { + "version": "1.0.18", + "resolved": "https://registry.npmjs.org/@types/web3/-/web3-1.0.18.tgz", + "integrity": "sha512-uXQL0LDszt2f476LEmYM6AvSv9F4vU4hWQvlUhwfLHNlIB6OyBXoYsCzWAIhhnc5U0HA7ZBcPybxRJ/yfA6THg==", + "dev": true, + "requires": { + "@types/bn.js": "4.11.4", + "@types/underscore": "1.8.9" + } + }, + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "2.1.21", + "negotiator": "0.6.1" + } + }, + "aes-js": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-0.2.4.tgz", + "integrity": "sha1-lLiBq3FyhtAV+iGeCPtmcJ3aWj0=" + }, + "ajv": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz", + "integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==", + "requires": { + "fast-deep-equal": "2.0.1", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.4.1", + "uri-js": "4.2.2" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "1.9.3" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, + "array-back": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", + "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", + "dev": true, + "requires": { + "typical": "2.6.1" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "2.1.2" + } + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "requires": { + "bn.js": "4.11.8", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base-x": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-1.1.0.tgz", + "integrity": "sha1-QtPXF0dPnqAiB/bRqh9CaRPut6w=" + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "0.14.5" + } + }, + "bignumber.js": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", + "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==" + }, + "bindings": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.4.0.tgz", + "integrity": "sha512-7znEVX22Djn+nYjxCWKDne0RRloa9XfYa84yk3s+HkE3LpDYZmhArYr9O9huBoHY3/oXispx5LorIX7Sl2CgSQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bip66": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "bl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "requires": { + "readable-stream": "2.3.6", + "safe-buffer": "5.1.2" + } + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "requires": { + "inherits": "2.0.3" + } + }, + "bluebird": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", + "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==" + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" + }, + "body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "requires": { + "bytes": "3.0.0", + "content-type": "1.0.4", + "debug": "2.6.9", + "depd": "1.1.2", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "1.6.16" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "requires": { + "buffer-xor": "1.0.3", + "cipher-base": "1.0.4", + "create-hash": "1.2.0", + "evp_bytestokey": "1.0.3", + "inherits": "2.0.3", + "safe-buffer": "5.1.2" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "requires": { + "browserify-aes": "1.2.0", + "browserify-des": "1.0.2", + "evp_bytestokey": "1.0.3" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "requires": { + "cipher-base": "1.0.4", + "des.js": "1.0.0", + "inherits": "2.0.3", + "safe-buffer": "5.1.2" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "requires": { + "bn.js": "4.11.8", + "randombytes": "2.0.6" + } + }, + "browserify-sha3": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/browserify-sha3/-/browserify-sha3-0.0.4.tgz", + "integrity": "sha1-CGxHuMgjFsnUcCLCYYWVRXbdjiY=", + "requires": { + "js-sha3": "0.6.1", + "safe-buffer": "5.1.2" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.2.0", + "create-hmac": "1.1.7", + "elliptic": "6.4.1", + "inherits": "2.0.3", + "parse-asn1": "5.1.3" + } + }, + "bs58": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-3.1.0.tgz", + "integrity": "sha1-1MJjiL9IBMrHFBQbGUWqR+XrJI4=", + "requires": { + "base-x": "1.1.0" + } + }, + "bs58check": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-1.3.4.tgz", + "integrity": "sha1-xSVABzdJEXcU+gQsMEfrj5FRy/g=", + "requires": { + "bs58": "3.1.0", + "create-hash": "1.2.0" + } + }, + "buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", + "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", + "requires": { + "base64-js": "1.3.0", + "ieee754": "1.1.12" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "requires": { + "buffer-alloc-unsafe": "1.1.0", + "buffer-fill": "1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-to-arraybuffer": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/buffer-to-arraybuffer/-/buffer-to-arraybuffer-0.0.5.tgz", + "integrity": "sha1-YGSkD6dutDxyOrqe+PbhIW0QURo=" + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "1.1.0", + "check-error": "1.0.2", + "deep-eql": "3.0.1", + "get-func-name": "2.0.0", + "pathval": "1.1.0", + "type-detect": "4.0.8" + } + }, + "chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "requires": { + "check-error": "1.0.2" + } + }, + "chai-subset": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/chai-subset/-/chai-subset-1.6.0.tgz", + "integrity": "sha1-pdDKFOMpp5WW7XAFi2ZGvWmIz+k=", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.2" + } + }, + "coinstring": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/coinstring/-/coinstring-2.3.0.tgz", + "integrity": "sha1-zbYzY6lhUCQEolr7gsLibV/2J6Q=", + "requires": { + "bs58": "2.0.1", + "create-hash": "1.2.0" + }, + "dependencies": { + "bs58": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-2.0.1.tgz", + "integrity": "sha1-VZCNWPGYKrogCPob7Y+RmYopv40=" + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "requires": { + "delayed-stream": "1.0.0" + } + }, + "command-line-args": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-4.0.7.tgz", + "integrity": "sha512-aUdPvQRAyBvQd2n7jXcsMDz68ckBJELXNzBybCHOibUWEg0mWTnaYCSRU8h9R+aNRSvDihJtssSRCiDRpLaezA==", + "dev": true, + "requires": { + "array-back": "2.0.0", + "find-replace": "1.0.3", + "typical": "2.6.1" + } + }, + "commander": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "requires": { + "graceful-readlink": "1.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "4.1.1", + "vary": "1.1.2" + } + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "requires": { + "bn.js": "4.11.8", + "elliptic": "6.4.1" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "1.0.4", + "inherits": "2.0.3", + "md5.js": "1.3.5", + "ripemd160": "2.0.2", + "sha.js": "2.4.11" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "1.0.4", + "create-hash": "1.2.0", + "inherits": "2.0.3", + "ripemd160": "2.0.2", + "safe-buffer": "5.1.2", + "sha.js": "2.4.11" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "requires": { + "browserify-cipher": "1.0.1", + "browserify-sign": "4.0.4", + "create-ecdh": "4.0.3", + "create-hash": "1.2.0", + "create-hmac": "1.1.7", + "diffie-hellman": "5.0.3", + "inherits": "2.0.3", + "pbkdf2": "3.0.17", + "public-encrypt": "4.0.3", + "randombytes": "2.0.6", + "randomfill": "1.0.4" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "decompress": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.0.tgz", + "integrity": "sha1-eu3YVCflqS2s/lVnSnxQXpbQH50=", + "requires": { + "decompress-tar": "4.1.1", + "decompress-tarbz2": "4.1.1", + "decompress-targz": "4.1.1", + "decompress-unzip": "4.0.1", + "graceful-fs": "4.1.15", + "make-dir": "1.3.0", + "pify": "2.3.0", + "strip-dirs": "2.1.0" + } + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "1.0.1" + } + }, + "decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "requires": { + "file-type": "5.2.0", + "is-stream": "1.1.0", + "tar-stream": "1.6.2" + } + }, + "decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "requires": { + "decompress-tar": "4.1.1", + "file-type": "6.2.0", + "is-stream": "1.1.0", + "seek-bzip": "1.0.5", + "unbzip2-stream": "1.3.1" + }, + "dependencies": { + "file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==" + } + } + }, + "decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "requires": { + "decompress-tar": "4.1.1", + "file-type": "5.2.0", + "is-stream": "1.1.0" + } + }, + "decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", + "requires": { + "file-type": "3.9.0", + "get-stream": "2.3.1", + "pify": "2.3.0", + "yauzl": "2.10.0" + }, + "dependencies": { + "file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=" + }, + "get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", + "requires": { + "object-assign": "4.1.1", + "pinkie-promise": "2.0.1" + } + } + } + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "requires": { + "bn.js": "4.11.8", + "miller-rabin": "4.0.1", + "randombytes": "2.0.6" + } + }, + "dom-walk": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz", + "integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=" + }, + "dotenv": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz", + "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==", + "dev": true + }, + "drbg.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", + "integrity": "sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs=", + "requires": { + "browserify-aes": "1.2.0", + "create-hash": "1.2.0", + "create-hmac": "1.1.7" + } + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "0.1.1", + "safer-buffer": "2.1.2" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "elliptic": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", + "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0", + "hash.js": "1.1.7", + "hmac-drbg": "1.0.1", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "dev": true, + "requires": { + "iconv-lite": "0.4.23" + } + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "1.4.0" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "eth-ens-namehash": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/eth-ens-namehash/-/eth-ens-namehash-2.0.8.tgz", + "integrity": "sha1-IprEbsqG1S4MmR58sq74P/D2i88=", + "requires": { + "idna-uts46-hx": "2.3.1", + "js-sha3": "0.5.7" + }, + "dependencies": { + "js-sha3": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz", + "integrity": "sha1-DU/9gALVMzqrr0oj7tL2N0yfKOc=" + } + } + }, + "eth-lib": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.1.27.tgz", + "integrity": "sha512-B8czsfkJYzn2UIEMwjc7Mbj+Cy72V+/OXH/tb44LV8jhrjizQJJ325xMOMyk3+ETa6r6oi0jsUY14+om8mQMWA==", + "requires": { + "bn.js": "4.11.8", + "elliptic": "6.4.1", + "keccakjs": "0.2.3", + "nano-json-stream-parser": "0.1.2", + "servify": "0.1.12", + "ws": "3.3.3", + "xhr-request-promise": "0.1.2" + } + }, + "ethereumjs-util": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.0.0.tgz", + "integrity": "sha512-E3yKUyl0Fs95nvTFQZe/ZSNcofhDzUsDlA5y2uoRmf1+Ec7gpGhNCsgKkZBRh7Br5op8mJcYF/jFbmjj909+nQ==", + "requires": { + "bn.js": "4.11.8", + "create-hash": "1.2.0", + "ethjs-util": "0.1.6", + "keccak": "1.4.0", + "rlp": "2.2.2", + "safe-buffer": "5.1.2", + "secp256k1": "3.6.2" + } + }, + "ethereumjs-wallet": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/ethereumjs-wallet/-/ethereumjs-wallet-0.6.0.tgz", + "integrity": "sha1-gnY7Fpfuenlr5xVdqd+0my+Yz9s=", + "requires": { + "aes-js": "0.2.4", + "bs58check": "1.3.4", + "ethereumjs-util": "4.5.0", + "hdkey": "0.7.1", + "scrypt.js": "0.2.1", + "utf8": "2.1.2", + "uuid": "2.0.3" + }, + "dependencies": { + "ethereumjs-util": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz", + "integrity": "sha1-PpQosxfuvaPXJg2FT93alUsfG8Y=", + "requires": { + "bn.js": "4.11.8", + "create-hash": "1.2.0", + "keccakjs": "0.2.3", + "rlp": "2.2.2", + "secp256k1": "3.6.2" + } + } + } + }, + "ethers": { + "version": "4.0.0-beta.1", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-4.0.0-beta.1.tgz", + "integrity": "sha512-SoYhktEbLxf+fiux5SfCEwdzWENMvgIbMZD90I62s4GZD9nEjgEWy8ZboI3hck193Vs0bDoTohDISx84f2H2tw==", + "requires": { + "@types/node": "10.12.19", + "aes-js": "3.0.0", + "bn.js": "4.11.8", + "elliptic": "6.3.3", + "hash.js": "1.1.3", + "js-sha3": "0.5.7", + "scrypt-js": "2.0.3", + "setimmediate": "1.0.4", + "uuid": "2.0.1", + "xmlhttprequest": "1.8.0" + }, + "dependencies": { + "aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha1-4h3xCtbCBTKVvLuNq0Cwnb6ofk0=" + }, + "elliptic": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.3.3.tgz", + "integrity": "sha1-VILZZG1UvLif19mU/J4ulWiHbj8=", + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0", + "hash.js": "1.1.3", + "inherits": "2.0.3" + } + }, + "hash.js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", + "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1" + } + }, + "js-sha3": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz", + "integrity": "sha1-DU/9gALVMzqrr0oj7tL2N0yfKOc=" + }, + "setimmediate": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.4.tgz", + "integrity": "sha1-IOgd5iLUoCWIzgyNqJc8vPHTE48=" + }, + "uuid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.1.tgz", + "integrity": "sha1-wqMN7bPlNdcsz4LjQ5QaULqFM6w=" + } + } + }, + "ethjs-provider-http": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ethjs-provider-http/-/ethjs-provider-http-0.1.6.tgz", + "integrity": "sha1-HsXZtL4lfvHValALIqdBmF6IlCA=", + "dev": true, + "requires": { + "xhr2": "0.1.3" + } + }, + "ethjs-unit": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ethjs-unit/-/ethjs-unit-0.1.6.tgz", + "integrity": "sha1-xmWSHkduh7ziqdWIpv4EBbLEFpk=", + "requires": { + "bn.js": "4.11.6", + "number-to-bn": "1.7.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU=" + } + } + }, + "ethjs-util": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.6.tgz", + "integrity": "sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==", + "requires": { + "is-hex-prefixed": "1.0.0", + "strip-hex-prefix": "1.0.0" + } + }, + "eventemitter3": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.1.1.tgz", + "integrity": "sha1-R3hr2qCHyvext15zq8XH1UAVjNA=" + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "requires": { + "md5.js": "1.3.5", + "safe-buffer": "5.1.2" + } + }, + "express": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", + "requires": { + "accepts": "1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.3", + "content-disposition": "0.5.2", + "content-type": "1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "1.1.2", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "etag": "1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "1.1.2", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "2.0.4", + "qs": "6.5.2", + "range-parser": "1.2.0", + "safe-buffer": "5.1.2", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "1.4.0", + "type-is": "1.6.16", + "utils-merge": "1.0.1", + "vary": "1.1.2" + }, + "dependencies": { + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "requires": { + "pend": "1.2.0" + } + }, + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=" + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "statuses": "1.4.0", + "unpipe": "1.0.0" + }, + "dependencies": { + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + } + } + }, + "find-replace": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-1.0.3.tgz", + "integrity": "sha1-uI5zZNLZyVlVnziMZmcNYTBEH6A=", + "dev": true, + "requires": { + "array-back": "1.0.4", + "test-value": "2.1.0" + }, + "dependencies": { + "array-back": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", + "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=", + "dev": true, + "requires": { + "typical": "2.6.1" + } + } + } + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "requires": { + "is-callable": "1.1.4" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.7", + "mime-types": "2.1.21" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=", + "dev": true + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "fs-extra": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-2.1.2.tgz", + "integrity": "sha1-BGxwFjzvmq1GsOSn+kZ/si1x3jU=", + "requires": { + "graceful-fs": "4.1.15", + "jsonfile": "2.4.0" + } + }, + "fs-promise": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/fs-promise/-/fs-promise-2.0.3.tgz", + "integrity": "sha1-9k5PhUvPaJqovdy6JokW2z20aFQ=", + "requires": { + "any-promise": "1.3.0", + "fs-extra": "2.1.2", + "mz": "2.7.0", + "thenify-all": "1.6.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "requires": { + "graceful-fs": "4.1.15", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.3" + } + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "global": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz", + "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=", + "requires": { + "min-document": "2.19.0", + "process": "0.5.2" + } + }, + "got": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/got/-/got-7.1.0.tgz", + "integrity": "sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==", + "requires": { + "decompress-response": "3.3.0", + "duplexer3": "0.1.4", + "get-stream": "3.0.0", + "is-plain-obj": "1.1.0", + "is-retry-allowed": "1.1.0", + "is-stream": "1.1.0", + "isurl": "1.0.0", + "lowercase-keys": "1.0.1", + "p-cancelable": "0.3.0", + "p-timeout": "1.2.1", + "safe-buffer": "5.1.2", + "timed-out": "4.0.1", + "url-parse-lax": "1.0.0", + "url-to-options": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "6.7.0", + "har-schema": "2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbol-support-x": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", + "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==" + }, + "has-to-string-tag-x": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", + "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", + "requires": { + "has-symbol-support-x": "1.4.2" + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.2" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1" + } + }, + "hdkey": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/hdkey/-/hdkey-0.7.1.tgz", + "integrity": "sha1-yu5L6BqneSHpCbjSKN0PKayu5jI=", + "requires": { + "coinstring": "2.3.0", + "secp256k1": "3.6.2" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "1.1.7", + "minimalistic-assert": "1.0.1", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": "1.5.0" + } + }, + "http-https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/http-https/-/http-https-1.0.0.tgz", + "integrity": "sha1-L5CN1fHbQGjAWM1ubUzjkskTOJs=" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.16.1" + } + }, + "human-standard-token-abi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/human-standard-token-abi/-/human-standard-token-abi-2.0.0.tgz", + "integrity": "sha512-m1f5DiIvqaNmpgphNqx2OziyTCj4Lvmmk28uMSxGWrOc9/lMpAKH8UcMPhvb13DMNZPzxn07WYFhxOGKuPLryg==" + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": "2.1.2" + } + }, + "idna-uts46-hx": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/idna-uts46-hx/-/idna-uts46-hx-2.3.1.tgz", + "integrity": "sha512-PWoF9Keq6laYdIRwwCdhTPl60xRqAloYNMQLiyUnG42VjT53oW07BXIRM+NK7eQjzXjAk2gUvX9caRxlnF9TAA==", + "requires": { + "punycode": "2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", + "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=" + } + } + }, + "ieee754": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", + "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "install": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/install/-/install-0.12.2.tgz", + "integrity": "sha512-+7thTb4Rpvs9mnlhHKGZFJbGOO6kyMgy+gg0sgM5vFzIFK0wrCYXqdlaM71Bi289DTuPHf61puMFsaZBcwDIrg==", + "dev": true + }, + "ipaddr.js": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", + "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" + }, + "is-function": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.1.tgz", + "integrity": "sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU=" + }, + "is-hex-prefixed": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz", + "integrity": "sha1-fY035q135dEnFIkTxXPggtd39VQ=" + }, + "is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=" + }, + "is-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", + "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=" + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + }, + "is-retry-allowed": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", + "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=" + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "isurl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", + "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", + "requires": { + "has-to-string-tag-x": "1.4.1", + "is-object": "1.0.1" + } + }, + "js-sha3": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.6.1.tgz", + "integrity": "sha1-W4n3enR3Z5h39YxKB1JAk0sflcA=" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "requires": { + "graceful-fs": "4.1.15" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "just-extend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", + "dev": true + }, + "keccak": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-1.4.0.tgz", + "integrity": "sha512-eZVaCpblK5formjPjeTBik7TAg+pqnDrMHIffSvi9Lh7PQgM1+hSzakUeZFCk9DVVG0dacZJuaz2ntwlzZUIBw==", + "requires": { + "bindings": "1.4.0", + "inherits": "2.0.3", + "nan": "2.12.1", + "safe-buffer": "5.1.2" + } + }, + "keccakjs": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/keccakjs/-/keccakjs-0.2.3.tgz", + "integrity": "sha512-BjLkNDcfaZ6l8HBG9tH0tpmDv3sS2mA7FNQxFHpCdzP3Gb2MVruXBSuoM66SnVxKJpAr5dKGdkHD+bDokt8fTg==", + "requires": { + "browserify-sha3": "0.0.4", + "sha3": "1.2.2" + } + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true + }, + "lolex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-3.0.0.tgz", + "integrity": "sha512-hcnW80h3j2lbUfFdMArd5UPA/vxZJ+G8vobd+wg3nVEQA0EigStbYcrG030FJxL6xiDDPEkoMatV9xIh5OecQQ==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "4.0.0" + } + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "requires": { + "pify": "3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + } + } + }, + "make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "dev": true + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "requires": { + "hash-base": "3.0.4", + "inherits": "2.0.3", + "safe-buffer": "5.1.2" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0" + } + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + }, + "mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" + }, + "mime-types": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "requires": { + "mime-db": "1.37.0" + } + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, + "min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", + "requires": { + "dom-walk": "0.1.1" + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "mkdirp-promise": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mkdirp-promise/-/mkdirp-promise-5.0.1.tgz", + "integrity": "sha1-6bj2jlUsaKnBcTuEiD96HdA5uKE=", + "requires": { + "mkdirp": "0.5.1" + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + } + } + }, + "mock-fs": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-4.7.0.tgz", + "integrity": "sha512-WlQNtUlzMRpvLHf8dqeUmNqfdPjGY29KrJF50Ldb4AcL+vQeR8QH3wQcFMgrhTwb1gHjZn9xggho+84tBskLgA==" + }, + "mout": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/mout/-/mout-0.11.1.tgz", + "integrity": "sha1-ujYR318OWx/7/QEWa48C0fX6K5k=" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "requires": { + "any-promise": "1.3.0", + "object-assign": "4.1.1", + "thenify-all": "1.6.0" + } + }, + "nan": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz", + "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==" + }, + "nano-json-stream-parser": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/nano-json-stream-parser/-/nano-json-stream-parser-0.1.2.tgz", + "integrity": "sha1-DMj20OK2IrR5xA1JnEbWS3Vcb18=" + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "nise": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.8.tgz", + "integrity": "sha512-kGASVhuL4tlAV0tvA34yJYZIVihrUt/5bDwpp4tTluigxUr2bBlJeDXmivb6NuEdFkqvdv/Ybb9dm16PSKUhtw==", + "dev": true, + "requires": { + "@sinonjs/formatio": "3.1.0", + "just-extend": "4.0.2", + "lolex": "2.7.5", + "path-to-regexp": "1.7.0", + "text-encoding": "0.6.4" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "lolex": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", + "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", + "dev": true + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "node-fetch-polyfill": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/node-fetch-polyfill/-/node-fetch-polyfill-2.0.6.tgz", + "integrity": "sha1-BzzjrWgmvbmVqHKM/E44I/IEQHo=", + "dev": true, + "requires": { + "encoding": "0.1.12", + "is-stream": "1.1.0", + "node-web-streams": "0.2.2" + } + }, + "node-web-streams": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/node-web-streams/-/node-web-streams-0.2.2.tgz", + "integrity": "sha1-CH52u+t+jcVmhrJdtOYMX/nbCR8=", + "dev": true, + "requires": { + "is-stream": "1.1.0", + "web-streams-polyfill": "git://github.com/gwicke/web-streams-polyfill.git#42c488428adea1dc0c0245014e4896ad456b1ded" + } + }, + "number-to-bn": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/number-to-bn/-/number-to-bn-1.7.0.tgz", + "integrity": "sha1-uzYjWS9+X54AMLGXe9QaDFP+HqA=", + "requires": { + "bn.js": "4.11.6", + "strip-hex-prefix": "1.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU=" + } + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "oboe": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/oboe/-/oboe-2.1.3.tgz", + "integrity": "sha1-K0hl29Rr6BIlcT9Om/5Lz09oCk8=", + "requires": { + "http-https": "1.0.0" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "p-cancelable": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz", + "integrity": "sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw==" + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-timeout": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", + "integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=", + "requires": { + "p-finally": "1.0.0" + } + }, + "parse-asn1": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.3.tgz", + "integrity": "sha512-VrPoetlz7B/FqjBLD2f5wBVZvsZVLnRUrxVLfRYhGXCODa/NWE4p3Wp+6+aV3ZPL3KM7/OZmxDIwwijD7yuucg==", + "requires": { + "asn1.js": "4.10.1", + "browserify-aes": "1.2.0", + "create-hash": "1.2.0", + "evp_bytestokey": "1.0.3", + "pbkdf2": "3.0.17", + "safe-buffer": "5.1.2" + } + }, + "parse-headers": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.1.tgz", + "integrity": "sha1-aug6eqJanZtwCswoaYzR8e1+lTY=", + "requires": { + "for-each": "0.3.3", + "trim": "0.0.1" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "requires": { + "create-hash": "1.2.0", + "create-hmac": "1.1.7", + "ripemd160": "2.0.2", + "safe-buffer": "5.1.2", + "sha.js": "2.4.11" + } + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "2.0.4" + } + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" + }, + "prettier": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.16.2.tgz", + "integrity": "sha512-vBMdCn1LjrFi2CpBsiWVKOq+WP9poXDTIGPe2sG3eE33LQ3b6IUgmaMjLZKKY+frD/8FqPeEK1qAx9mOV8iruA==", + "dev": true + }, + "process": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", + "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "proxy-addr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", + "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", + "requires": { + "forwarded": "0.1.2", + "ipaddr.js": "1.8.0" + } + }, + "psl": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.2.0", + "parse-asn1": "5.1.3", + "randombytes": "2.0.6", + "safe-buffer": "5.1.2" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "requires": { + "decode-uri-component": "0.2.0", + "object-assign": "4.1.1", + "strict-uri-encode": "1.1.0" + } + }, + "randombytes": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", + "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "requires": { + "randombytes": "2.0.6", + "safe-buffer": "5.1.2" + } + }, + "randomhex": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/randomhex/-/randomhex-0.1.5.tgz", + "integrity": "sha1-us7vmCMpCRQA8qKRLGzQLxCU9YU=" + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "redux": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.1.tgz", + "integrity": "sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==", + "requires": { + "loose-envify": "1.4.0", + "symbol-observable": "1.2.0" + } + }, + "redux-mock-store": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.3.tgz", + "integrity": "sha512-ryhkkb/4D4CUGpAV2ln1GOY/uh51aczjcRz9k2L2bPx/Xja3c5pSGJJPyR25GNVRXtKIExScdAgFdiXp68GmJA==", + "dev": true, + "requires": { + "lodash.isplainobject": "4.0.6" + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.8.0", + "caseless": "0.12.0", + "combined-stream": "1.0.7", + "extend": "3.0.2", + "forever-agent": "0.6.1", + "form-data": "2.3.3", + "har-validator": "5.1.3", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.21", + "oauth-sign": "0.9.0", + "performance-now": "2.1.0", + "qs": "6.5.2", + "safe-buffer": "5.1.2", + "tough-cookie": "2.4.3", + "tunnel-agent": "0.6.0", + "uuid": "3.3.2" + }, + "dependencies": { + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, + "resolve": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", + "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", + "dev": true, + "requires": { + "path-parse": "1.0.6" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "requires": { + "glob": "7.1.3" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "3.0.4", + "inherits": "2.0.3" + } + }, + "rlp": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/rlp/-/rlp-2.2.2.tgz", + "integrity": "sha512-Ng2kJEN731Sfv4ZAY2i0ytPMc0BbJKBsVNl0QZY8LxOWSwd+1xpg+fpSRfaMn0heHU447s6Kgy8qfHZR0XTyVw==", + "requires": { + "bn.js": "4.11.8", + "safe-buffer": "5.1.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "scrypt": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/scrypt/-/scrypt-6.0.3.tgz", + "integrity": "sha1-BOAUpWgrU/pQwtXM4WfXGcBthw0=", + "requires": { + "nan": "2.12.1" + } + }, + "scrypt-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-2.0.3.tgz", + "integrity": "sha1-uwBAvgMEPamgEqLOqfyfhSz8h9Q=" + }, + "scrypt.js": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/scrypt.js/-/scrypt.js-0.2.1.tgz", + "integrity": "sha512-XMoqxwABdotuW+l+qACmJ/h0kVSCgMPZXpbncA/zyBO90z/NnDISzVw+xJ4tUY+X/Hh0EFT269OYHm26VCPgmA==", + "requires": { + "scrypt": "6.0.3", + "scryptsy": "1.2.1" + } + }, + "scryptsy": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/scryptsy/-/scryptsy-1.2.1.tgz", + "integrity": "sha1-oyJfpLJST4AnAHYeKFW987LZIWM=", + "requires": { + "pbkdf2": "3.0.17" + } + }, + "secp256k1": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.6.2.tgz", + "integrity": "sha512-90nYt7yb0LmI4A2jJs1grglkTAXrBwxYAjP9bpeKjvJKOjG2fOeH/YI/lchDMIvjrOasd5QXwvV2jwN168xNng==", + "requires": { + "bindings": "1.4.0", + "bip66": "1.1.5", + "bn.js": "4.11.8", + "create-hash": "1.2.0", + "drbg.js": "1.0.1", + "elliptic": "6.4.1", + "nan": "2.12.1", + "safe-buffer": "5.1.2" + } + }, + "seek-bzip": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", + "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", + "requires": { + "commander": "2.8.1" + } + }, + "semaphore": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz", + "integrity": "sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA==" + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "requires": { + "debug": "2.6.9", + "depd": "1.1.2", + "destroy": "1.0.4", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "etag": "1.8.1", + "fresh": "0.5.2", + "http-errors": "1.6.3", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "2.3.0", + "range-parser": "1.2.0", + "statuses": "1.4.0" + }, + "dependencies": { + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + } + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "requires": { + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "parseurl": "1.3.2", + "send": "0.16.2" + } + }, + "servify": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/servify/-/servify-0.1.12.tgz", + "integrity": "sha512-/xE6GvsKKqyo1BAY+KxOWXcLpPsUUyji7Qg3bVD7hh1eRze5bR1uYiuDA/k3Gof1s9BTzQZEJK8sNcNGFIzeWw==", + "requires": { + "body-parser": "1.18.3", + "cors": "2.8.5", + "express": "4.16.4", + "request": "2.88.0", + "xhr": "2.5.0" + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.2" + } + }, + "sha3": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/sha3/-/sha3-1.2.2.tgz", + "integrity": "sha1-pmxQmN5MJbyIM27ItIF9AFvKe6k=", + "requires": { + "nan": "2.10.0" + }, + "dependencies": { + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" + } + } + }, + "simple-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", + "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" + }, + "simple-get": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.1.tgz", + "integrity": "sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==", + "requires": { + "decompress-response": "3.3.0", + "once": "1.4.0", + "simple-concat": "1.0.0" + } + }, + "sinon": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.2.3.tgz", + "integrity": "sha512-i6j7sqcLEqTYqUcMV327waI745VASvYuSuQMCjbAwlpAeuCgKZ3LtrjDxAbu+GjNQR0FEDpywtwGCIh8GicNyg==", + "dev": true, + "requires": { + "@sinonjs/commons": "1.3.0", + "@sinonjs/formatio": "3.1.0", + "@sinonjs/samsam": "3.0.2", + "diff": "3.5.0", + "lolex": "3.0.0", + "nise": "1.4.8", + "supports-color": "5.5.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.10.tgz", + "integrity": "sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ==", + "dev": true, + "requires": { + "buffer-from": "1.1.1", + "source-map": "0.6.1" + } + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "0.2.4", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.2", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.2", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "safer-buffer": "2.1.2", + "tweetnacl": "0.14.5" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "requires": { + "is-natural-number": "4.0.1" + } + }, + "strip-hex-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", + "integrity": "sha1-DF8VX+8RUTczd96du1iNoFUA428=", + "requires": { + "is-hex-prefixed": "1.0.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + }, + "swarm-js": { + "version": "0.1.37", + "resolved": "https://registry.npmjs.org/swarm-js/-/swarm-js-0.1.37.tgz", + "integrity": "sha512-G8gi5fcXP/2upwiuOShJ258sIufBVztekgobr3cVgYXObZwJ5AXLqZn52AI+/ffft29pJexF9WNdUxjlkVehoQ==", + "requires": { + "bluebird": "3.5.3", + "buffer": "5.2.1", + "decompress": "4.2.0", + "eth-lib": "0.1.27", + "fs-extra": "2.1.2", + "fs-promise": "2.0.3", + "got": "7.1.0", + "mime-types": "2.1.21", + "mkdirp-promise": "5.0.1", + "mock-fs": "4.7.0", + "setimmediate": "1.0.5", + "tar.gz": "1.0.7", + "xhr-request-promise": "0.1.2" + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "requires": { + "bl": "1.2.2", + "buffer-alloc": "1.2.0", + "end-of-stream": "1.4.1", + "fs-constants": "1.0.0", + "readable-stream": "2.3.6", + "to-buffer": "1.1.1", + "xtend": "4.0.1" + } + }, + "tar.gz": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tar.gz/-/tar.gz-1.0.7.tgz", + "integrity": "sha512-uhGatJvds/3diZrETqMj4RxBR779LKlIE74SsMcn5JProZsfs9j0QBwWO1RW+IWNJxS2x8Zzra1+AW6OQHWphg==", + "requires": { + "bluebird": "2.11.0", + "commander": "2.8.1", + "fstream": "1.0.11", + "mout": "0.11.1", + "tar": "2.2.1" + }, + "dependencies": { + "bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" + } + } + }, + "test-value": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/test-value/-/test-value-2.1.0.tgz", + "integrity": "sha1-Edpv9nDzRxpztiXKTz/c97t0gpE=", + "dev": true, + "requires": { + "array-back": "1.0.4", + "typical": "2.6.1" + }, + "dependencies": { + "array-back": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", + "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=", + "dev": true, + "requires": { + "typical": "2.6.1" + } + } + } + }, + "text-encoding": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", + "dev": true + }, + "thenify": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", + "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", + "requires": { + "any-promise": "1.3.0" + } + }, + "thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", + "requires": { + "thenify": "3.3.0" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" + }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "1.1.31", + "punycode": "1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "trim": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", + "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=" + }, + "ts-essentials": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-1.0.4.tgz", + "integrity": "sha512-q3N1xS4vZpRouhYHDPwO0bDW3EZ6SK9CrrDHxi/D6BPReSjpVgWIOpLS2o0gSBZm+7q/wyKp6RVM1AeeW7uyfQ==", + "dev": true + }, + "ts-generator": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ts-generator/-/ts-generator-0.0.8.tgz", + "integrity": "sha512-Gi+aZCELpVL7Mqb+GuMgM+n8JZ/arZZib1iD/R9Ok8JDjOCOCrqS9b1lr72ku7J45WeDCFZxyJoRsiQvhokCnw==", + "dev": true, + "requires": { + "@types/mkdirp": "0.5.2", + "@types/prettier": "1.15.2", + "@types/resolve": "0.0.8", + "chalk": "2.4.2", + "glob": "7.1.3", + "mkdirp": "0.5.1", + "prettier": "1.16.2", + "resolve": "1.10.0", + "ts-essentials": "1.0.4" + } + }, + "ts-node": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz", + "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==", + "dev": true, + "requires": { + "arrify": "1.0.1", + "buffer-from": "1.1.1", + "diff": "3.5.0", + "make-error": "1.3.5", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "source-map-support": "0.5.10", + "yn": "2.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.21" + } + }, + "typechain": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/typechain/-/typechain-0.3.11.tgz", + "integrity": "sha512-Hz2EAc168F3z2uYS9KlDU7aPEi0pRWbuykAvIf9Ke7dRDtqETpdg4G7q5TKTkIdhBRQv4jk48HmSEMc21gB7iA==", + "dev": true, + "requires": { + "command-line-args": "4.0.7", + "debug": "3.2.6", + "fs-extra": "7.0.1", + "ts-generator": "0.0.8" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "2.1.1" + } + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "4.1.15", + "jsonfile": "4.0.0", + "universalify": "0.1.2" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "4.1.15" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "1.0.0" + } + }, + "typescript": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.2.tgz", + "integrity": "sha512-VCj5UiSyHBjwfYacmDuc/NOk4QQixbE+Wn7MFJuS0nRuPQbof132Pw4u53dm264O8LPc2MVsc7RJNml5szurkg==", + "dev": true + }, + "typescript-fsa": { + "version": "3.0.0-beta-2", + "resolved": "https://registry.npmjs.org/typescript-fsa/-/typescript-fsa-3.0.0-beta-2.tgz", + "integrity": "sha512-qXkHih2XAtpxqd3AEVgFtCDwvBlAMl2miVy1TmS+wXgiRlVw7orf2qIh3C8faANhD34n75Ui8x5ydQ40uzuskA==" + }, + "typescript-fsa-reducers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/typescript-fsa-reducers/-/typescript-fsa-reducers-1.2.0.tgz", + "integrity": "sha512-JGIqBDf4SV+pE8CDenTqtr13cts7kwS3/YbYhLoKZZMZWSIhwn+xwwbrg/oEfvftLykyE9PXrySy6A62m6gEaw==" + }, + "typical": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", + "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=", + "dev": true + }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" + }, + "unbzip2-stream": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.1.tgz", + "integrity": "sha512-fIZnvdjblYs7Cru/xC6tCPVhz7JkYcVQQkePwMLyQELzYTds2Xn8QefPVnvdVhhZqubxNA1cASXEH5wcK0Bucw==", + "requires": { + "buffer": "3.6.0", + "through": "2.3.8" + }, + "dependencies": { + "base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha1-EQHpVE9KdrG8OybUUsqW16NeeXg=" + }, + "buffer": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-3.6.0.tgz", + "integrity": "sha1-pyyTb3e5a/UvX357RnGAYoVR3vs=", + "requires": { + "base64-js": "0.0.8", + "ieee754": "1.1.12", + "isarray": "1.0.0" + } + } + } + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "2.1.1" + } + }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "requires": { + "prepend-http": "1.0.4" + } + }, + "url-set-query": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-set-query/-/url-set-query-1.0.0.tgz", + "integrity": "sha1-AW6M/Xwg7gXK/neV6JK9BwL6ozk=" + }, + "url-to-options": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", + "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=" + }, + "utf8": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.2.tgz", + "integrity": "sha1-H6DZJw6b6FDZsFAn9jUZv0ZFfZY=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, + "web-streams-polyfill": { + "version": "git://github.com/gwicke/web-streams-polyfill.git#42c488428adea1dc0c0245014e4896ad456b1ded", + "dev": true + }, + "web3": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3/-/web3-1.0.0-beta.37.tgz", + "integrity": "sha512-8XLgUspdzicC/xHG82TLrcF/Fxzj2XYNJ1KTYnepOI77bj5rvpsxxwHYBWQ6/JOjk0HkZqoBfnXWgcIHCDhZhQ==", + "requires": { + "web3-bzz": "1.0.0-beta.37", + "web3-core": "1.0.0-beta.37", + "web3-eth": "1.0.0-beta.37", + "web3-eth-personal": "1.0.0-beta.37", + "web3-net": "1.0.0-beta.37", + "web3-shh": "1.0.0-beta.37", + "web3-utils": "1.0.0-beta.37" + } + }, + "web3-bzz": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.0.0-beta.37.tgz", + "integrity": "sha512-E+dho49Nsm/QpQvYWOF35YDsQrMvLB19AApENxhlQsu6HpWQt534DQul0t3Y/aAh8rlKD6Kanxt8LhHDG3vejQ==", + "requires": { + "got": "7.1.0", + "swarm-js": "0.1.37", + "underscore": "1.8.3" + } + }, + "web3-core": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.0.0-beta.37.tgz", + "integrity": "sha512-cIwEqCj7OJyefQNauI0HOgW4sSaOQ98V99H2/HEIlnCZylsDzfw7gtQUdwnRFiIyIxjbWy3iWsjwDPoXNPZBYg==", + "requires": { + "web3-core-helpers": "1.0.0-beta.37", + "web3-core-method": "1.0.0-beta.37", + "web3-core-requestmanager": "1.0.0-beta.37", + "web3-utils": "1.0.0-beta.37" + } + }, + "web3-core-helpers": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.0.0-beta.37.tgz", + "integrity": "sha512-efaLOzN28RMnbugnyelgLwPWWaSwElQzcAJ/x3PZu+uPloM/lE5x0YuBKvIh7/PoSMlHqtRWj1B8CpuQOUQ5Ew==", + "requires": { + "underscore": "1.8.3", + "web3-eth-iban": "1.0.0-beta.37", + "web3-utils": "1.0.0-beta.37" + } + }, + "web3-core-method": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.0.0-beta.37.tgz", + "integrity": "sha512-pKWFUeqnVmzx3VrZg+CseSdrl/Yrk2ioid/HzolNXZE6zdoITZL0uRjnsbqXGEzgRRd1Oe/pFndpTlRsnxXloA==", + "requires": { + "underscore": "1.8.3", + "web3-core-helpers": "1.0.0-beta.37", + "web3-core-promievent": "1.0.0-beta.37", + "web3-core-subscriptions": "1.0.0-beta.37", + "web3-utils": "1.0.0-beta.37" + } + }, + "web3-core-promievent": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.0.0-beta.37.tgz", + "integrity": "sha512-GTF2r1lP8nJBeA5Gxq5yZpJy9l8Fb9CXGZPfF8jHvaRdQHtm2Z+NDhqYmF833lcdkokRSyfPcXlz1mlWeClFpg==", + "requires": { + "any-promise": "1.3.0", + "eventemitter3": "1.1.1" + } + }, + "web3-core-requestmanager": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.0.0-beta.37.tgz", + "integrity": "sha512-66VUqye5BGp1Zz1r8psCxdNH+GtTjaFwroum2Osx+wbC5oRjAiXkkadiitf6wRb+edodjEMPn49u7B6WGNuewQ==", + "requires": { + "underscore": "1.8.3", + "web3-core-helpers": "1.0.0-beta.37", + "web3-providers-http": "1.0.0-beta.37", + "web3-providers-ipc": "1.0.0-beta.37", + "web3-providers-ws": "1.0.0-beta.37" + } + }, + "web3-core-subscriptions": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.0.0-beta.37.tgz", + "integrity": "sha512-FdXl8so9kwkRRWziuCSpFsAuAdg9KvpXa1fQlT16uoGcYYfxwFO/nkwyBGQzkZt7emShI2IRugcazyPCZDwkOA==", + "requires": { + "eventemitter3": "1.1.1", + "underscore": "1.8.3", + "web3-core-helpers": "1.0.0-beta.37" + } + }, + "web3-eth": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.0.0-beta.37.tgz", + "integrity": "sha512-Eb3aGtkz3G9q+Z9DKgSQNbn/u8RtcZQQ0R4sW9hy5KK47GoT6vab5c6DiD3QWzI0BzitHzR5Ji+3VHf/hPUGgw==", + "requires": { + "underscore": "1.8.3", + "web3-core": "1.0.0-beta.37", + "web3-core-helpers": "1.0.0-beta.37", + "web3-core-method": "1.0.0-beta.37", + "web3-core-subscriptions": "1.0.0-beta.37", + "web3-eth-abi": "1.0.0-beta.37", + "web3-eth-accounts": "1.0.0-beta.37", + "web3-eth-contract": "1.0.0-beta.37", + "web3-eth-ens": "1.0.0-beta.37", + "web3-eth-iban": "1.0.0-beta.37", + "web3-eth-personal": "1.0.0-beta.37", + "web3-net": "1.0.0-beta.37", + "web3-utils": "1.0.0-beta.37" + } + }, + "web3-eth-abi": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.0.0-beta.37.tgz", + "integrity": "sha512-g9DKZGM2OqwKp/tX3W/yihcj7mQCtJ6CXyZXEIZfuDyRBED/iSEIFfieDOd+yo16sokLMig6FG7ADhhu+19hdA==", + "requires": { + "ethers": "4.0.0-beta.1", + "underscore": "1.8.3", + "web3-utils": "1.0.0-beta.37" + } + }, + "web3-eth-accounts": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.0.0-beta.37.tgz", + "integrity": "sha512-uvbHL62/zwo4GDmwKdqH9c/EgYd8QVnAfpVw8D3epSISpgbONNY7Hr4MRMSd/CqAP12l2Ls9JVQGLhhC83bW6g==", + "requires": { + "any-promise": "1.3.0", + "crypto-browserify": "3.12.0", + "eth-lib": "0.2.7", + "scrypt.js": "0.2.0", + "underscore": "1.8.3", + "uuid": "2.0.1", + "web3-core": "1.0.0-beta.37", + "web3-core-helpers": "1.0.0-beta.37", + "web3-core-method": "1.0.0-beta.37", + "web3-utils": "1.0.0-beta.37" + }, + "dependencies": { + "eth-lib": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.2.7.tgz", + "integrity": "sha1-L5Pxex4jrsN1nNSj/iDBKGo/wco=", + "requires": { + "bn.js": "4.11.8", + "elliptic": "6.4.1", + "xhr-request-promise": "0.1.2" + } + }, + "scrypt.js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/scrypt.js/-/scrypt.js-0.2.0.tgz", + "integrity": "sha1-r40UZbcemZARC+38WTuUeeA6ito=", + "requires": { + "scrypt": "6.0.3", + "scryptsy": "1.2.1" + } + }, + "uuid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.1.tgz", + "integrity": "sha1-wqMN7bPlNdcsz4LjQ5QaULqFM6w=" + } + } + }, + "web3-eth-contract": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.0.0-beta.37.tgz", + "integrity": "sha512-h1B3A8Z/C7BlnTCHkrWbXZQTViDxfR12lKMeTkT8Sqj5phFmxrBlPE4ORy4lf1Dk5b23mZYE0r/IRACx4ThCrQ==", + "requires": { + "underscore": "1.8.3", + "web3-core": "1.0.0-beta.37", + "web3-core-helpers": "1.0.0-beta.37", + "web3-core-method": "1.0.0-beta.37", + "web3-core-promievent": "1.0.0-beta.37", + "web3-core-subscriptions": "1.0.0-beta.37", + "web3-eth-abi": "1.0.0-beta.37", + "web3-utils": "1.0.0-beta.37" + } + }, + "web3-eth-ens": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.0.0-beta.37.tgz", + "integrity": "sha512-dR3UkrVzdRrJhfP57xBPx0CMiVnCcYFvh+u2XMkGydrhHgupSUkjqGr89xry/j1T0BkuN9mikpbyhdCVMXqMbg==", + "requires": { + "eth-ens-namehash": "2.0.8", + "underscore": "1.8.3", + "web3-core": "1.0.0-beta.37", + "web3-core-helpers": "1.0.0-beta.37", + "web3-core-promievent": "1.0.0-beta.37", + "web3-eth-abi": "1.0.0-beta.37", + "web3-eth-contract": "1.0.0-beta.37", + "web3-utils": "1.0.0-beta.37" + } + }, + "web3-eth-iban": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.0.0-beta.37.tgz", + "integrity": "sha512-WQRniGJFxH/XCbd7miO6+jnUG+6bvuzfeufPIiOtCbeIC1ypp1kSqER8YVBDrTyinU1xnf1U5v0KBZ2yiWBJxQ==", + "requires": { + "bn.js": "4.11.6", + "web3-utils": "1.0.0-beta.37" + }, + "dependencies": { + "bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU=" + } + } + }, + "web3-eth-personal": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.0.0-beta.37.tgz", + "integrity": "sha512-B4dZpGbD+nGnn48i6nJBqrQ+HB7oDmd+Q3wGRKOsHSK5HRWO/KwYeA7wgwamMAElkut50lIsT9EJl4Apfk3G5Q==", + "requires": { + "web3-core": "1.0.0-beta.37", + "web3-core-helpers": "1.0.0-beta.37", + "web3-core-method": "1.0.0-beta.37", + "web3-net": "1.0.0-beta.37", + "web3-utils": "1.0.0-beta.37" + } + }, + "web3-net": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.0.0-beta.37.tgz", + "integrity": "sha512-xG/uBtMdDa1UMXw9KjDUgf3fXA/fDEJUYUS0TDn+U9PMgngA+UVECHNNvQTrVVDxEky38V3sahwIDiopNsQdsw==", + "requires": { + "web3-core": "1.0.0-beta.37", + "web3-core-method": "1.0.0-beta.37", + "web3-utils": "1.0.0-beta.37" + } + }, + "web3-providers-http": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.0.0-beta.37.tgz", + "integrity": "sha512-FM/1YDB1jtZuTo78habFj7S9tNHoqt0UipdyoQV29b8LkGKZV9Vs3is8L24hzuj1j/tbwkcAH+ewIseHwu0DTg==", + "requires": { + "web3-core-helpers": "1.0.0-beta.37", + "xhr2-cookies": "1.1.0" + } + }, + "web3-providers-ipc": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.0.0-beta.37.tgz", + "integrity": "sha512-NdRPRxYMIU0C3u18NI8u4bwbhI9pCg5nRgDGYcmSAx5uOBxiYcQy+hb0WkJRRhBoyIXJmy+s26FoH8904+UnPg==", + "requires": { + "oboe": "2.1.3", + "underscore": "1.8.3", + "web3-core-helpers": "1.0.0-beta.37" + } + }, + "web3-providers-ws": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.0.0-beta.37.tgz", + "integrity": "sha512-8p6ZLv+1JYa5Vs8oBn33Nn3VGFBbF+wVfO+b78RJS1Qf1uIOzjFVDk3XwYDD7rlz9G5BKpxhaQw+6EGQ7L02aw==", + "requires": { + "underscore": "1.8.3", + "web3-core-helpers": "1.0.0-beta.37", + "websocket": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2" + } + }, + "web3-shh": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.0.0-beta.37.tgz", + "integrity": "sha512-h5STG/xqZNQWtCLYOu7NiMqwqPea8SfkKQUPUFxXKIPVCFVKpHuQEwW1qcPQRJMLhlQIv17xuoUe1A+RzDNbrw==", + "requires": { + "web3-core": "1.0.0-beta.37", + "web3-core-method": "1.0.0-beta.37", + "web3-core-subscriptions": "1.0.0-beta.37", + "web3-net": "1.0.0-beta.37" + } + }, + "web3-utils": { + "version": "1.0.0-beta.37", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.0.0-beta.37.tgz", + "integrity": "sha512-kA1fyhO8nKgU21wi30oJQ/ssvu+9srMdjOTKbHYbQe4ATPcr5YNwwrxG3Bcpbu1bEwRUVKHCkqi+wTvcAWBdlQ==", + "requires": { + "bn.js": "4.11.6", + "eth-lib": "0.1.27", + "ethjs-unit": "0.1.6", + "number-to-bn": "1.7.0", + "randomhex": "0.1.5", + "underscore": "1.8.3", + "utf8": "2.1.1" + }, + "dependencies": { + "bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU=" + }, + "utf8": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.1.tgz", + "integrity": "sha1-LgHbAvfY0JRPdxBPFgnrDDBM92g=" + } + } + }, + "websocket": { + "version": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2", + "requires": { + "debug": "2.6.9", + "nan": "2.12.1", + "typedarray-to-buffer": "3.1.5", + "yaeti": "0.0.6" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "requires": { + "async-limiter": "1.0.0", + "safe-buffer": "5.1.2", + "ultron": "1.1.1" + } + }, + "xhr": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.5.0.tgz", + "integrity": "sha512-4nlO/14t3BNUZRXIXfXe+3N6w3s1KoxcJUUURctd64BLRe67E4gRwp4PjywtDY72fXpZ1y6Ch0VZQRY/gMPzzQ==", + "requires": { + "global": "4.3.2", + "is-function": "1.0.1", + "parse-headers": "2.0.1", + "xtend": "4.0.1" + } + }, + "xhr-request": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xhr-request/-/xhr-request-1.1.0.tgz", + "integrity": "sha512-Y7qzEaR3FDtL3fP30k9wO/e+FBnBByZeybKOhASsGP30NIkRAAkKD/sCnLvgEfAIEC1rcmK7YG8f4oEnIrrWzA==", + "requires": { + "buffer-to-arraybuffer": "0.0.5", + "object-assign": "4.1.1", + "query-string": "5.1.1", + "simple-get": "2.8.1", + "timed-out": "4.0.1", + "url-set-query": "1.0.0", + "xhr": "2.5.0" + } + }, + "xhr-request-promise": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/xhr-request-promise/-/xhr-request-promise-0.1.2.tgz", + "integrity": "sha1-NDxE0e53JrhkgGloLQ+EDIO0Jh0=", + "requires": { + "xhr-request": "1.1.0" + } + }, + "xhr2": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.1.3.tgz", + "integrity": "sha1-y/xHWaabSoiOeM9PILBRA4dXvRE=", + "dev": true + }, + "xhr2-cookies": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xhr2-cookies/-/xhr2-cookies-1.1.0.tgz", + "integrity": "sha1-fXdEnQmZGX8VXLc7I99yUF7YnUg=", + "requires": { + "cookiejar": "2.1.2" + } + }, + "xmlhttprequest": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", + "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "yaeti": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=" + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "requires": { + "buffer-crc32": "0.2.13", + "fd-slicer": "1.1.0" + } + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "dev": true + } + } +} diff --git a/modules/client/package.json b/modules/client/package.json new file mode 100644 index 0000000000..6acabbfb21 --- /dev/null +++ b/modules/client/package.json @@ -0,0 +1,58 @@ +{ + "name": "@spankchain/connext-client", + "description": "Shared code between wallet and hub", + "version": "0.0.1", + "main": "dist", + "devDependencies": { + "@types/chai": "^4.1.6", + "@types/chai-as-promised": "^7.1.0", + "@types/chai-subset": "^1.3.1", + "@types/ethereumjs-util": "^5.2.0", + "@types/mocha": "^5.2.5", + "@types/node": "^10.12.0", + "@types/redux-mock-store": "^1.0.0", + "@types/semaphore": "^1.1.0", + "@types/sinon": "^5.0.7", + "@types/web3": "^1.0.11", + "bluebird": "^3.5.3", + "bn.js": "^4.11.8", + "chai": "^4.2.0", + "chai-as-promised": "^7.1.1", + "chai-subset": "^1.6.0", + "dotenv": "^6.1.0", + "ethjs-provider-http": "^0.1.6", + "fs": "^0.0.1-security", + "install": "^0.12.2", + "mocha": "^5.2.0", + "node-fetch-polyfill": "^2.0.6", + "redux-mock-store": "^1.5.3", + "sinon": "^7.1.0", + "ts-node": "^7.0.1", + "typechain": "^0.3.8", + "typescript": "=3.2.2" + }, + "dependencies": { + "bignumber.js": "^7.2.1", + "ethereumjs-util": "^6.0.0", + "ethereumjs-wallet": "0.6.0", + "human-standard-token-abi": "^2.0.0", + "redux": "^4.0.1", + "semaphore": "^1.1.0", + "typescript-fsa": "^3.0.0-beta-2", + "typescript-fsa-reducers": "^1.1.0", + "web3": "1.0.0-beta.37" + }, + "engines": { + "node": ">=9", + "npm": ">=5" + }, + "scripts": { + "clean": "rm -rf ./dist", + "build": "npm run clean && npm run compile", + "compile": "node_modules/.bin/tsc", + "prepare": "npm run build", + "test": "mocha -r ts-node/register/type-check -r ./src/register/common.ts -r ./src/register/testing.ts \"src/**/*.test.ts\" --exit", + "generate": "node_modules/.bin/typechain --target=web3-1.0.0 --outDir=./src/typechain/ './.yalc/contracts/build/contracts/*.json'", + "postinstall": "rm -f node_modules/web3/index.d.ts" + } +} diff --git a/modules/client/src/.Connext.ts.swo b/modules/client/src/.Connext.ts.swo new file mode 100644 index 0000000000000000000000000000000000000000..e8f7fcd8872ed8326b97bfea5dee8d0bc6d8071d GIT binary patch literal 24576 zcmeI4e~et$RmaBxTAEVQm>+^aK-X@RnRT}_PAeeYdhObd8yn&u^6t8dT+91r=I-vZ zXWtv%d$a3hv#Dg25;aJrQvShi3#pJAAZhx8KOhnR5JFohpazLQfT~a`RZu}iNGlq( zhR?Y_-jA7yUGf({-l*T~%>8lhx#ymH?m6dPx4Zw`iIeKy_B{2;3AQKYYA?JT`1&APIdKrMmqK?0*}PL1jeGeV$ zCba}=3Dgp(B~VMCmOw3mS^~8MY6<*bkw7+lZR5R6<~BE>i|+5PvA_T6K384&@>u;1 zSN}d&{*|%%@4EU&T=|L{z>o9&t~_*q|8=bX2V6NR3iDZV4Ez4y=gQBxzx}a#S6u(8 zB~VMCmOw3mS^~8MY6;X5s3lNKpq4-_fm#CpFC`H48;!lp{22+P39fzo|HV5RjgN!( zg0tY~z@6ZwA8s`M8axjoFbiIOL!l=+Pf#<=e!DHYNumQT@Bsc)}fu96lyB+#q9dy9!!Sk^-7r_Pa9xxC7<2B3^d>;Hh_$YV;B;Y}CA24|16Mo_ybvsTYq*MN{xp7 zAk)W3-Io29&Z#9+bXNc3NT=EP3$tnvq}k$HqJzGzI?R3BK_ zih5^tIvUXBAcXtznNiYP3sT)023zaA%7C;lI%7+O_`1#lSP14+5N(y{jWvZss1M^P z)j~b7+#QVv1K3Z3feaExt33KhN9#IaEX(1n%Hk{-Oersob)UQ;3A4k=YI@{UiTtrE zOdwj-txX*+#tV8dm{Vz%FpiMDtfRDAS|aJYPSAXJn634bU=x-%^RIKNC_WP;0YYI8 zC2~de1VP7qt8XWPkgafG@ygM76lH}dUe!XDxENn!rCxDq4>&GPvb>91Lw%67X5Exo z??MrLwbXS-TL=8A`|^2|V%E>7TzIfWk5!#{IVWe)gS1s**4LN{S=1B_Z8@3TKZv^l ztLc^GRY`{7?Y20w#iC^)<0rbOL!La%sl)w#qSJI%r8?^Ckd04&DN4iDNcU|=Q=fO`4GWo zTJB@bfMeQKltu4~%*6DSVVqhiln$d_qBVDn3@{q>6)(+LXGJqJnrk9{Wwea#C?(NM<$k1ni)WXPE}lNC z79TuwY$5O5=-i6RJ`UZESo|zp*D71nvZg9b&2C`_s1aY|b)Burk1STA%q)>Ev=F!yNhTsph@9HNthHsN3prmxVxM z%vy55=tS+P6^=R8Y4L}C$p_87O%v!9T(!FXvPwBDot~)3lb$xrqA6IC>oZiWbu(xrSaGFV90US=|-hC zVfwC7WCS#;IN9UDh7P&|?c5398wm`;ZfBH*gS0%=ME6HMXqMX9S^A1{<9z0EqGx4` z=5pX@fk>FLcdcM1=%wsOT_)DoD*;Z9tnUNy>T01Jj#XPok3w#uaB0)pA`~}sC$^k< z70*KodT}I8it}Cyi(mHimomQqMTtK zqcHdKt8fzJSJ3Y-gsY43I9>0g6y1$t+LNV3)RfNpt+HK`>P9dKaUA1BBzw;PC^gMD z^pa?gUs?9B`JJm`A>2Zjr*8LY4Rb0b?r%Y6YRiHSPRgwaRGQ>Hohsd;^F*t>|B=zw zE&5hkxMipG%(HGz$j(H&oWi!( zIY&cwqj;I7Z?#*ArGk33NmI@u^ZHy_ok_jEi7&trA<;&d=vGragyxK|#mJTFx>5U5 zT2x|7H?S(l*24_XC+C9q@I*q*MozLHV|+H6Gwog+t%R$socgxv;VNW$K`hX$I&5sE zr?Xe=+?BFjszhzR8OrXAPrezvWpMG`O_2zubH;4R=gxc|e`*QT5~w9mOQ4p(tB`;sR(8lP^O3Le*%8;;e(*Plx-|dnZnd?1*LFnmWoK@A zR<+yhfJEqwYZyq{4DSdZ{_rqtxmuI0J%3?FZM$Y^Otx2(P=kyeBhjQgBGZ6ul?{tp zQtdN5B$sR{ZJ=UVbiGQh4>>g(>yb*60NOIi9$wZw~Mlj_v^f19~>}8S*G3$ebjh|S_!udcK|MB1y8JpoQroje| zJ0#6q$HL)-U_VWR>CMu*JjCD zliXa5kg=q*lui;7c=THjSJyI?1jwLa7<8ke2&Hvqf<`y@w2aQ$+$5Q;mgU+rQ&l9p zaHVPPF(YiQg#(Q~CL`??aG+tqOt!eFfy;yHAv0G|N~TQjaeWfC&=17%5W!SdJItD? zOQ)ID;XAoamRS;;qQ!QR$+Zm=y%ENe$CCvxxjmz5+$TX-aWN4Ntmc?{lPD!sM+nD_ zi!zguY3)--bT1g8NoHs+YSNHA5k1NIiK1EZM$HgY7Dgi~jrxqtMOHtjRn;Od+nrf; zkN=);MV3(=Vn&x*e49~oB1O07;mNL`kBgh1Wi6B4S^k({dR%b~ z>5A!MD(k^zKU-PPbkt4dW>5B+(F(hSskI_l1oA!WVbACogF)?dG}v-`GS`J*MI!D1 zyCeGHHZIMEZmnX>O<+^AYp8ic_%0hm*%sJpmx^q00AbURuCWp3tSYtFg$BsZtD1B} zy0A>LV_b8);uAZWq0Pig=h;mumW+U!_r|W&dF+VE`PnnY1_?`3?ZAR8g|$d?l@m;C zgT()T%YE~8?xQ9C51pg(PsH_q4kQooEVu@?!6JAwxIujXm*8XIaWDcJ^ubB63;aDX zzQq3WHXsBCz}?^mas5AnFM+=U9|8A)qhLSyGO_(N@Bp|Se1;hQL2w(mL5%)g@TcID z;8#Ew90NZKc7tz7Yz|%k&w$?o9|a!-kAe??%wVBO(g1T;oIuuznD-m3?F^W)f~9UH-KKBu-x+NjJHXJ~iu*V%>uePm zCo<&rp7SJ8d)P2sJGpDL8N1O6Q#@Dp?IiQp=C;Y~tvCt>ZF3_fw>&&sXz|VgUqGl9 z{SR5AVjRc3yY!mUnrZjAEpC$vY~Oe6oOT!ovP1N_%* z({I*vGR59O=4Lt^%KteA-HOdG9Ghlhq{8$R-hdN_6&Gvcy3N>$`BO!X+RC+RL36Ci zM;tQL+}O~*j;O+zJ^R~yg-tm$>>(mjb2;0JF`9O3@d|g=6%W`jYn2;-2bAixm#*wKPTB{mirg9|vb|P}`FuBcbb=yRRa(3I6B}T>3>Lp=Ux61xm(IB@i zrl^Q65r*55va%*PpvbC}RSeT%Gk@7s=rX3nyeJbv6juwp+v2&C$u?NRMORmK%ePTJ zCe0af;WWpJSv#-rq7i9(I7-)A+#P!1S;JSb>C63JrD$x93YnQ9sQ47u*Fx+(+aw+~ z$%ov1_f480vavQoJMUuGF-!|)SO#m{y?K}RlnEOZxFNCHxDCPrPP~aYf1zw`%&PaWS#;+$US$S0?=e}` zYEFC5fN~45ipE0JUT;m%HM?TGuGO4cJW09GwF$z7h89(==CsmDvOHufIw4yTK;`bV z)vWxSq|mo-`6j4L+}^gD6GfBMBttYoO0MaPLPS>iJxN5nw~$32H#7ebp{QszC(9}X zvA;!Y{_nXIAE!a?ZPL8FflB30)V@{1A2MoWAFjDu-&XEg%~qS|9$ikDePbUBIHu3} z-xD+nZ)?mUGz^*%^T%r5eG%4*OYl-g5Mn$roAK=m5^|c@Qd34d7&FA>tDEid(nGnz z$MZSc=29w21ll9pyz3#=#FyL%NqVa87Gb`;Uuu%TSSGx_YrEipez3N)>}F{y%&8#~ z!VPrh#jFkMHPFPalGieIdGf}ld*nb}G~u_Tg3>j45oe!W$+2k|!BQ~<{JMORFeX2j zImzT8A@Q8Xey|hrSP&mulp);ybf;WTPfuZH-)PHIT)DoyAP)neeKrMk<0<{Ec3Dgp(B~VM?f0_j9 uOTqsQmxA`6rSbx#bPZ@TaD@I`BgjpruQUJbs%_umm9O89)5g6}YWx?CKHq2n literal 0 HcmV?d00001 diff --git a/modules/client/src/Connext.test.ts.todo b/modules/client/src/Connext.test.ts.todo new file mode 100644 index 0000000000..d16524a14f --- /dev/null +++ b/modules/client/src/Connext.test.ts.todo @@ -0,0 +1,621 @@ +require('dotenv').config() +const Web3 = require('web3') +const HttpProvider = require(`ethjs-provider-http`) +const sinon = require('sinon') +import mocha = require('mocha') +import BN = require('bn.js') +import { expect } from 'chai' +import { UnsignedChannelState, Address, ChannelStateUpdate, channelNumericFields, ChannelUpdateReason, SyncResult, ChannelState, ChannelRow } from './types' +import { getConnextClient, ConnextInternal, ConnextClient } from './Connext' +import { Networking } from './helpers/networking' +import * as t from './testing' +import { Lock } from "./lib/utils"; +import { reducers } from './state/reducers' +import { assert } from "./testing"; +import { setterAction } from "./state/actions"; +import { MockConnextInternal } from './testing/mocks' +import { PersistentState } from './state/store' + +// // global.fetch = fetch + +let sandbox = sinon.createSandbox() + +let web3: any +let accounts: Address[] +let connext: ConnextClient +let sender: Address +let receiver: Address + +// deploy contracts to network +// and update the .env file before testing + +type HubResponse = { + data: Object + statusCode: number +} + +type ContractResponse = { + transactionHash: string + status: boolean +} + +const updateHubBehavior = (responses: HubResponse[]) => { + let fn = sinon.stub() + for (let i = 0; i < responses.length; i++) { + responses[i].statusCode === 200 + ? fn.onCall(i).resolves(responses[i]) + : fn.onCall(i).rejects(responses[i]) + } + return fn +} + +const updateContractBehavior = (responses: ContractResponse[]) => { + let fn = sinon.stub() + for (let i = 0; i < responses.length; i++) { + responses[i].status + ? fn.onCall(i).resolves(responses[i]) + : fn.onCall(i).rejects(responses[i]) + } + return fn +} + + +// create mocked hub +const setNetworking = (responses: HubResponse[]) => { + + const stub = updateHubBehavior(responses) + // console.log(Object.keys(stub)) + // console.log(stub.behaviors[0].stub) + connext.networking.get = stub + connext.networking.post = stub + connext.networking.request = stub +} + +const setChannelManager = (responses?: ContractResponse[]) => { + if (!responses) { + // set to transaction result + responses = [{ + transactionHash: t.mkHash('0xTTT'), + status: true + }] + } + const stub = updateContractBehavior(responses) + connext.channelManager = { + methods: { + userAuthorizedUpdate: () => ({ send: stub }), + hubAuthorizedUpdate: () => ({ send: stub }), + }, + } as any +} + +const removeNetworking = (connext: Connext) => { + delete connext.networking +} + +const removeChannelManager = (connext: Connext) => { + delete connext.channelManager +} + +/* +describe('Connext::requestExchange', () => { + describe('mocked hub', () => { + beforeEach('instantiate web3/client, create mock', async () => { + web3 = new Web3(new HttpProvider(process.env.ETH_NODE_URL)) + accounts = await web3.eth.getAccounts() + sender = accounts[1] + receiver = accounts[2] + + // instantiate client + connext = new Connext({ + web3, + hubUrl: process.env.HUB_URL || '', + contractAddress: process.env.CONTRACT_ADDRESS || '', + hubAddress: process.env.HUB_ADDRESS || '', + tokenAddress: process.env.TOKEN_ADDRESS, + tokenName: process.env.TOKEN_NAME || 'BOOTY', + }) + + // set networking to default response + setNetworking([{ data: {}, statusCode: 200 }]) + }) + + it('should hit the expected url', async () => { + // assume the channel has been opened and collateralized + // by the user + const response = await connext.requestExchange( + { wei: '10', token: '10' }, + 'WEI', + sender, + ) + sandbox.assert.calledOnce(connext.networking.post) + }) + + afterEach('restore hub sandbox', () => { + removeNetworking(connext) + removeChannelManager(connext) + }) + }) +}) + +describe.skip('Connext::verifyAndCosignAndSubmit', () => { + describe('mocked hub', async () => { + beforeEach('instantiate web3/client and create hub mock', async () => { + web3 = new Web3(new HttpProvider(process.env.ETH_NODE_URL)) + accounts = await web3.eth.getAccounts() + sender = accounts[1] + receiver = accounts[2] + + // instantiate client + connext = new Connext({ + web3, + hubUrl: process.env.HUB_URL || '', + contractAddress: process.env.CONTRACT_ADDRESS || '', + hubAddress: process.env.HUB_ADDRESS || '', + tokenAddress: process.env.TOKEN_ADDRESS, + tokenName: process.env.TOKEN_NAME || 'BOOTY', + }) + + // set networking to default response + setNetworking([{ data: {}, statusCode: 200 }]) + }) + + it('should work if user proposed a deposit, and hub returns confirmation', async () => { + // set sync response to confirmation and exchange + // represents proposed deposits, latest update user signed + let state1 = t.getChannelState('empty', { + contractAddress: process.env.CONTRACT_ADDRESS, + user: sender, + recipient: sender, + pendingDepositToken: ['10', '0'], + pendingDepositWei: ['0', '10'], + timeout: Math.floor(Date.now() / 1000) + 15, + txCount: [1, 1] + }) + const hash1 = connext.utils.createChannelStateHash(state1) + state1.sigHub = await web3.eth.sign(hash1, connext.hubAddress) + state1.sigUser = await web3.eth.sign(hash1, state1.user) + const latestUpdate: ChannelStateUpdate = { + reason: "ProposePending", + state: state1, + metadata: { originator: state1.user } + } + // represents confirmed deposits + let state2 = t.getChannelState('empty', { + contractAddress: process.env.CONTRACT_ADDRESS, + user: sender, + recipient: sender, + balanceToken: ['10', '0'], + balanceWei: ['0', '10'], + txCount: [2, 1], + }) + const hash2 = connext.utils.createChannelStateHash(state2) + state2.sigHub = await web3.eth.sign(hash2, connext.hubAddress) + + const actionItems = [ + { + type: "channel", + state: { + reason: "ConfirmPending", + state: state2, + metadata: { originator: connext.hubAddress } + }, + } + ] as SyncResult[] + + // add user sigs to state for tests/update response + state2.sigUser = await web3.eth.sign(hash2, state2.user) + + // call connext function + const resp = await connext.verifyAndCosignAndSubmit( + latestUpdate, + actionItems, + 0, + sender + ) + // validate states were signed + sandbox.assert.calledOnce(connext.networking.request) + expect(resp.length).to.equal(actionItems.length) + t.assertChannelStateEqual(resp[0].update.state as ChannelState, state2) + }) + + it('should work if hub returns proposed token deposit, confirm deposit, and exchange in existing channel', async () => { + // assume latest confirmed states are user deposit, confirmation + let state0 = t.getChannelState('empty', { + contractAddress: process.env.CONTRACT_ADDRESS, + user: receiver, + recipient: receiver, + balanceWei: ['0', '10'], + timeout: 0, + txCount: [2, 1] + }) + const hash0 = connext.utils.createChannelStateHash(state0) + state0.sigHub = await web3.eth.sign(hash0, connext.hubAddress) + state0.sigUser = await web3.eth.sign(hash0, receiver) + const latestUpdate = { + reason: "ConfirmPending" as ChannelUpdateReason, + state: state0, + metadata: { originator: receiver } + } + // set sync response to confirmation and exchange + // represents proposed deposits, latest update user signed + let state1 = t.getChannelState('empty', { + contractAddress: process.env.CONTRACT_ADDRESS, + user: receiver, + recipient: receiver, + balanceWei: ['0', '10'], + pendingDepositToken: ['10', '0'], + timeout: Math.floor(Date.now() / 1000) + 15, + txCount: [3, 2] + }) + const hash1 = connext.utils.createChannelStateHash(state1) + state1.sigHub = await web3.eth.sign(hash1, connext.hubAddress) + // represents confirmed deposits + let state2 = t.getChannelState('empty', { + contractAddress: process.env.CONTRACT_ADDRESS, + user: receiver, + recipient: receiver, + balanceWei: ['0', '10'], + balanceToken: ['10', '0'], + txCount: [4, 2], + }) + const hash2 = connext.utils.createChannelStateHash(state2) + state2.sigHub = await web3.eth.sign(hash2, connext.hubAddress) + // represents proposed exchange + let state3 = t.getChannelState('empty', { + contractAddress: process.env.CONTRACT_ADDRESS, + user: receiver, + recipient: receiver, + balanceToken: ['0', '10'], + balanceWei: ['10', '0'], + txCount: [5, 2], + }) + const hash3 = connext.utils.createChannelStateHash(state3) + state3.sigHub = await web3.eth.sign(hash3, connext.hubAddress) + const actionItems = [ + { + type: "channel", + state: { + reason: "ProposePending", + state: state1, + metadata: { originator: sender } + }, + }, + { + type: "channel", + state: { + reason: "ConfirmPending", + state: state2, + metadata: { originator: sender } + } + }, + { + type: "channel", + state: { + reason: "Exchange", + state: state3, + metadata: { originator: sender } + }, + } + ] as SyncResult[] + + // add user sigs to state for tests/update response + state1.sigUser = await web3.eth.sign(hash1, state1.user) + state2.sigUser = await web3.eth.sign(hash2, state2.user) + state3.sigUser = await web3.eth.sign(hash3, state3.user) + // call connext function + const resp = await connext.verifyAndCosignAndSubmit(latestUpdate, actionItems, 0, receiver) + // validate states were signed + sandbox.assert.calledOnce(connext.networking.post) + expect(resp.length).to.equal(actionItems.length) + t.assertChannelStateEqual(resp[0].update.state as ChannelState, state1) + t.assertChannelStateEqual(resp[1].update.state as ChannelState, state2) + t.assertChannelStateEqual(resp[2].update.state as ChannelState, state3) + }) + + afterEach('restore hub sandbox', () => { + removeNetworking(connext) + removeChannelManager(connext) + }) + }) +}) + +describe('Connext::verifyAndCosign', () => { + describe.skip('mocked hub', async () => { + beforeEach('instantiate web3/client and create hub mock', async () => { + web3 = new Web3(new HttpProvider(process.env.ETH_NODE_URL)) + accounts = await web3.eth.getAccounts() + sender = accounts[1] + receiver = accounts[2] + + // instantiate client + connext = new Connext({ + web3, + hubUrl: process.env.HUB_URL || '', + contractAddress: process.env.CONTRACT_ADDRESS || '', + hubAddress: process.env.HUB_ADDRESS || '', + tokenAddress: process.env.TOKEN_ADDRESS, + tokenName: process.env.TOKEN_NAME || 'BOOTY', + }) + + // set networking to default response + setNetworking([{ data: {}, statusCode: 200 }]) + }) + + it('should work if user proposed a deposit, and hub returns confirmation', async () => { + // set sync response to confirmation and exchange + // represents proposed deposits, latest update user signed + let state1 = t.getChannelState('empty', { + contractAddress: process.env.CONTRACT_ADDRESS, + user: sender, + recipient: sender, + pendingDepositToken: ['10', '0'], + pendingDepositWei: ['0', '10'], + timeout: Math.floor(Date.now() / 1000) + 15, + txCount: [1, 1] + }) + const hash1 = connext.utils.createChannelStateHash(state1) + state1.sigHub = await web3.eth.sign(hash1, connext.hubAddress) + state1.sigUser = await web3.eth.sign(hash1, state1.user) + const latestUpdate: ChannelStateUpdate = { + reason: "ProposePending", + state: state1, + metadata: { originator: state1.user } + } + // represents confirmed deposits + let state2 = t.getChannelState('empty', { + contractAddress: process.env.CONTRACT_ADDRESS, + user: sender, + recipient: sender, + balanceToken: ['10', '0'], + balanceWei: ['0', '10'], + txCount: [2, 1], + }) + const hash2 = connext.utils.createChannelStateHash(state2) + state2.sigHub = await web3.eth.sign(hash2, connext.hubAddress) + + const actionItems = [ + { + type: "channel", + state: { + reason: "ConfirmPending", + state: state2, + metadata: { originator: connext.hubAddress } + }, + } + ] as SyncResult[] + + // call connext function + const resp = await connext.verifyAndCosign(latestUpdate, actionItems, sender) + // add user sigs to state for tests + state2.sigUser = await web3.eth.sign(hash2, state2.user) + // validate states were signed + expect(resp.length).to.equal(actionItems.length) + t.assertChannelStateEqual(resp[0].state as ChannelState, state2) + }) + + it('should work if hub returns proposed token deposit, confirm deposit, and exchange in existing channel', async () => { + // assume latest confirmed states are user deposit, confirmation + let state0 = t.getChannelState('empty', { + contractAddress: process.env.CONTRACT_ADDRESS, + user: receiver, + recipient: receiver, + balanceWei: ['0', '10'], + timeout: 0, + txCount: [2, 1] + }) + const hash0 = connext.utils.createChannelStateHash(state0) + state0.sigHub = await web3.eth.sign(hash0, connext.hubAddress) + state0.sigUser = await web3.eth.sign(hash0, receiver) + const latestUpdate = { + reason: "ConfirmPending" as ChannelUpdateReason, + state: state0, + metadata: { originator: receiver } + } + // set sync response to confirmation and exchange + // represents proposed deposits, latest update user signed + let state1 = t.getChannelState('empty', { + contractAddress: process.env.CONTRACT_ADDRESS, + user: receiver, + recipient: receiver, + balanceWei: ['0', '10'], + pendingDepositToken: ['10', '0'], + timeout: Math.floor(Date.now() / 1000) + 15, + txCount: [3, 2] + }) + const hash1 = connext.utils.createChannelStateHash(state1) + state1.sigHub = await web3.eth.sign(hash1, connext.hubAddress) + // represents confirmed deposits + let state2 = t.getChannelState('empty', { + contractAddress: process.env.CONTRACT_ADDRESS, + user: receiver, + recipient: receiver, + balanceWei: ['0', '10'], + balanceToken: ['10', '0'], + txCount: [4, 2], + }) + const hash2 = connext.utils.createChannelStateHash(state2) + state2.sigHub = await web3.eth.sign(hash2, connext.hubAddress) + // represents proposed exchange + let state3 = t.getChannelState('empty', { + contractAddress: process.env.CONTRACT_ADDRESS, + user: receiver, + recipient: receiver, + balanceToken: ['0', '10'], + balanceWei: ['10', '0'], + txCount: [5, 2], + }) + const hash3 = connext.utils.createChannelStateHash(state3) + state3.sigHub = await web3.eth.sign(hash3, connext.hubAddress) + const actionItems = [ + { + type: "channel", + state: { + reason: "ProposePending", + state: state1, + metadata: { originator: connext.hubAddress } + } + }, + { + type: "channel", + state: { + reason: "ConfirmPending", + state: state2, + metadata: { originator: connext.hubAddress } + } + }, + { + type: "channel", + state: { + reason: "Exchange", + state: state3, + metadata: { originator: connext.hubAddress } + }, + } + ] as SyncResult[] + + // call connext function + const resp = await connext.verifyAndCosign(latestUpdate, actionItems, receiver) + // add user sigs to state for tests + state1.sigUser = await web3.eth.sign(hash1, state1.user) + state2.sigUser = await web3.eth.sign(hash2, state2.user) + state3.sigUser = await web3.eth.sign(hash3, state3.user) + + // validate states were signed + expect(resp.length).to.equal(actionItems.length) + t.assertChannelStateEqual(resp[0].state as ChannelState, state1) + t.assertChannelStateEqual(resp[1].state as ChannelState, state2) + t.assertChannelStateEqual(resp[2].state as ChannelState, state3) + }) + + afterEach('restore hub sandbox', () => { + removeNetworking(connext) + removeChannelManager(connext) + }) + }) +}) + +describe('Connext::sync', () => { + describe('mocked hub', async () => { + beforeEach('instantiate web3/client and create hub mock', async () => { + web3 = new Web3(new HttpProvider(process.env.ETH_NODE_URL)) + accounts = await web3.eth.getAccounts() + sender = accounts[1] + + // instantiate client + connext = new Connext({ + web3, + hubUrl: process.env.HUB_URL || '', + contractAddress: process.env.CONTRACT_ADDRESS || '', + hubAddress: process.env.HUB_ADDRESS || '', + tokenAddress: process.env.TOKEN_ADDRESS, + tokenName: process.env.TOKEN_NAME || 'BOOTY', + }) + + // set networking to default response + setNetworking([{ data: {}, statusCode: 200 }]) + }) + + it('should call the hub sync endpoint and return an array of channel states', async () => { + const responses = [{ statusCode: 200, data: {} }] + setNetworking(responses) + const ans = await connext.sync(0, 0, sender) + sandbox.assert.calledOnce(connext.networking.get) + }) + + afterEach('restore hub sandbox', () => { + removeNetworking(connext) + removeChannelManager(connext) + }) + }) +}) + +describe('Connext::openThread', () => { }) + +describe('Connext::getChannel', () => { + describe('mocked hub', async () => { + beforeEach('instantiate web3/client and create hub mock', async () => { + web3 = new Web3(new HttpProvider(process.env.ETH_NODE_URL)) + accounts = await web3.eth.getAccounts() + sender = accounts[1] + + // instantiate client + connext = new Connext({ + web3, + hubUrl: process.env.HUB_URL || '', + contractAddress: process.env.CONTRACT_ADDRESS || '', + hubAddress: process.env.HUB_ADDRESS || '', + tokenAddress: process.env.TOKEN_ADDRESS, + tokenName: process.env.TOKEN_NAME || 'BOOTY', + }) + + // set networking to default response + setNetworking([{ data: {}, statusCode: 200 }]) + }) + + it('should work when you dont have channels', async () => { + // create mocked networking instance + // to intercept hub requests + // should return 404 + setNetworking([{ statusCode: 404, data: {} }]) + try { + await connext.getChannel(sender) + } catch (e) { + expect(e.message).to.equal(`Channel not found for user ${sender}`) + } + sandbox.assert.calledOnce(connext.networking.get) + }) + + it('should work when you do have a channel', async () => { + // create mocked networking instance + // to intercept hub requests + const state = t.getChannelState('full') + const expected = { + state, + status: "CS_OPEN", + id: 1, + } + setNetworking([{ data: expected, statusCode: 200 }]) + const response = await connext.getChannel(sender) + t.assertChannelStateEqual(state, response.state) + sandbox.assert.calledOnce(connext.networking.get) + }) + + afterEach('restore hub sandbox', () => { + removeNetworking(connext) + removeChannelManager(connext) + }) + }) +}) +*/ + +describe('ConnextClient', () => { + it('should persist the persistent state', async () => { + let saved: any = null + const action = setterAction('persistent.bar') + reducers.case(action, action.handler) + + const connext = new MockConnextInternal({ + loadState: async () => JSON.stringify({ ...new PersistentState(), foo: 42 }), + saveState: async (state: string) => saved = state, + }) + + await connext.start() + connext.dispatch(action(69)) + await connext.savePersistentState() + + const savedVal = await saved as any + assert.containSubset(JSON.parse(savedVal), { + foo: 42, + bar: 69, + }) + + assert.containSubset(connext.store.getState(), { + persistent: { + foo: 42, + bar: 69, + }, + }) + + + }) +}) diff --git a/modules/client/src/Connext.ts b/modules/client/src/Connext.ts new file mode 100644 index 0000000000..bfa11240d8 --- /dev/null +++ b/modules/client/src/Connext.ts @@ -0,0 +1,759 @@ +import { WithdrawalParameters, ChannelManagerChannelDetails, Sync } from './types' +import { DepositArgs, SignedDepositRequestProposal, Omit } from './types' +import { PurchaseRequest } from './types' +import { UpdateRequest } from './types' +import { createStore, Action, applyMiddleware } from 'redux' +require('dotenv').config() +import { EventEmitter } from 'events' +import Web3 = require('web3') +// local imports +import { ChannelManager as TypechainChannelManager } from './contract/ChannelManager' +import { default as ChannelManagerAbi } from './contract/ChannelManagerAbi' +import { Networking } from './helpers/networking' +import BuyController from './controllers/BuyController' +import DepositController from './controllers/DepositController' +import SyncController from './controllers/SyncController' +import StateUpdateController from './controllers/StateUpdateController' +import WithdrawalController from './controllers/WithdrawalController' +import { Utils } from './Utils' +import { + Validator, +} from './validator' +import { + ChannelState, + ChannelStateUpdate, + Payment, + addSigToChannelState, + SyncResult, + ChannelRow, + ThreadRow, + UnsignedThreadState, + UnsignedChannelState, + PurchasePayment, + PurchasePaymentHubResponse, +} from './types' +import { default as Logger } from "./lib/Logger"; +import { ConnextStore, ConnextState, PersistentState } from "./state/store"; +import { handleStateFlags } from './state/middleware' +import { reducers } from "./state/reducers"; +import { isFunction, ResolveablePromise, timeoutPromise } from "./lib/utils"; +import { toBN, mul } from './helpers/bn' +import { ExchangeController } from './controllers/ExchangeController' +import { ExchangeRates } from './state/ConnextState/ExchangeRates' +import CollateralController from "./controllers/CollateralController"; +import * as actions from './state/actions'; +import { AbstractController } from './controllers/AbstractController' +import { EventLog } from 'web3/types'; +import { getChannel } from './lib/getChannel'; + +type Address = string +// anytime the hub is sending us something to sign we need a verify method that verifies that the hub isn't being a jerk + +/********************************* + ****** CONSTRUCTOR TYPES ******** + *********************************/ +// contract constructor options +export interface ContractOptions { + hubAddress: string + tokenAddress: string +} + +// connext constructor options +// NOTE: could extend ContractOptions, doesnt for future readability +export interface ConnextOptions { + web3: Web3 + hubUrl: string + contractAddress: string + hubAddress: Address + hub?: IHubAPIClient + tokenAddress?: Address + tokenName?: string +} + +export interface IHubAPIClient { + getChannel(): Promise + getChannelStateAtNonce(txCountGlobal: number): Promise + getThreadInitialStates(): Promise + getIncomingThreads(): Promise + getThreadByParties(receiver: Address, sender?: Address): Promise + sync(txCountGlobal: number, lastThreadUpdateId: number): Promise + getExchangerRates(): Promise + buy( + meta: PurchaseMetaType, + payments: PurchasePayment[], + ): Promise + requestDeposit(deposit: SignedDepositRequestProposal, txCount: number, lastThreadUpdateId: number): Promise + requestWithdrawal(withdrawal: WithdrawalParameters, txCountGlobal: number): Promise + requestExchange(weiToSell: string, tokensToSell: string, txCountGlobal: number): Promise + requestCollateral(txCountGlobal: number): Promise + updateHub(updates: UpdateRequest[], lastThreadUpdateId: number): Promise<{ + error: string | null + updates: Sync + }> +} + +class HubAPIClient implements IHubAPIClient { + private user: Address + private networking: Networking + private tokenName?: string + + constructor(user: Address, networking: Networking, tokenName?: string) { + this.user = user + this.networking = networking + this.tokenName = tokenName + } + + async getChannel(): Promise { + // get the current channel state and return it + try { + const res = await this.networking.get(`channel/${this.user}`) + return res.data + } catch (e) { + if (e.statusCode === 404) { + throw new Error(`Channel not found for user ${this.user}`) + } + throw e + } + } + + // return state at specified global nonce + async getChannelStateAtNonce( + txCountGlobal: number, + ): Promise { + // get the channel state at specified nonce + try { + const response = await this.networking.get( + `channel/${this.user}/update/${txCountGlobal}` + ) + return response.data + } catch (e) { + throw new Error( + `Cannot find update for user ${this.user} at nonce ${txCountGlobal}, ${e.toString()}` + ) + } + } + + async getThreadInitialStates(): Promise { + // get the current channel state and return it + const response = await this.networking.get( + `thread/${this.user}/initial-states`, + ) + if (!response.data) { + return [] + } + return response.data + } + + async getIncomingThreads(): Promise { + // get the current channel state and return it + const response = await this.networking.get( + `thread/${this.user}/incoming`, + ) + if (!response.data) { + return [] + } + return response.data + } + + // return all threads bnetween 2 addresses + async getThreadByParties( + receiver: Address, + ): Promise { + // get receiver threads + const response = await this.networking.get( + `thread/${this.user}/to/${receiver}`, + ) + if (!response.data) { + return [] as any + } + return response.data + } + + // hits the hubs sync endpoint to return all actionable states + async sync( + txCountGlobal: number, + lastThreadUpdateId: number + ): Promise { + try { + const res = await this.networking.get( + `channel/${this.user}/sync?lastChanTx=${txCountGlobal}&lastThreadUpdateId=${lastThreadUpdateId}`, + ) + return res.data + } catch (e) { + if (e.status === 404) { + return null + } + throw e + } + } + + async getExchangerRates(): Promise { + const { data } = await this.networking.get('exchangeRate') + return data.rates + } + + async buy( + meta: PurchaseMetaType, + payments: PurchasePayment[], + ): Promise { + const { data } = await this.networking.post('payments/purchase', { meta, payments }) + return data + } + + // post to hub telling user wants to deposit + requestDeposit = async ( + deposit: SignedDepositRequestProposal, + txCount: number, + lastThreadUpdateId: number, + ): Promise => { + if (!deposit.sigUser) { + throw new Error(`No signature detected on the deposit request. Deposit: ${deposit}, txCount: ${txCount}, lastThreadUpdateId: ${lastThreadUpdateId}`) + } + const response = await this.networking.post( + `channel/${this.user}/request-deposit`, + { + depositWei: deposit.amountWei, + depositToken: deposit.amountToken, + sigUser: deposit.sigUser, + lastChanTx: txCount, + lastThreadUpdateId, + }, + ) + return response.data + } + + // post to hub telling user wants to withdraw + requestWithdrawal = async ( + withdrawal: WithdrawalParameters, + txCountGlobal: number + ): Promise => { + const response = await this.networking.post( + `channel/${this.user}/request-withdrawal`, + { ...withdrawal, lastChanTx: txCountGlobal }, + ) + return response.data + } + + async requestExchange(weiToSell: string, tokensToSell: string, txCountGlobal: number): Promise { + const { data } = await this.networking.post( + `channel/${this.user}/request-exchange`, + { weiToSell, tokensToSell, lastChanTx: txCountGlobal } + ) + return data + } + + // performer calls this when they wish to start a show + // return the proposed deposit fro the hub which should then be verified and cosigned + requestCollateral = async (txCountGlobal: number): Promise => { + // post to hub + const response = await this.networking.post( + `channel/${this.user}/request-collateralization`, + { + lastChanTx: txCountGlobal + }, + ) + return response.data + } + + // post to hub to batch verify state updates + updateHub = async ( + updates: UpdateRequest[], + lastThreadUpdateId: number, + ): Promise<{ error: string | null, updates: Sync }> => { + // post to hub + const response = await this.networking.post( + `channel/${this.user}/update`, + { + lastThreadUpdateId, + updates, + }, + ) + return response.data + } +} + +// connext constructor options +// NOTE: could extend ContractOptions, doesnt for future readability +export interface ConnextOptions { + web3: Web3 + hubUrl: string + contractAddress: string + hubAddress: Address + hub?: IHubAPIClient + tokenAddress?: Address + tokenName?: string +} + +export abstract class IWeb3TxWrapper { + abstract awaitEnterMempool(): Promise + + abstract awaitFirstConfirmation(): Promise +} + +/** + * A wrapper around the Web3 PromiEvent + * (https://web3js.readthedocs.io/en/1.0/callbacks-promises-events.html#promievent) + * that makes the different `await` behaviors explicit. + * + * For example: + * + * > const tx = channelManager.userAuthorizedUpdate(...) + * > await tx.awaitEnterMempool() + */ +export class Web3TxWrapper extends IWeb3TxWrapper { + private tx: any + + private address: string + private name: string + private onTxHash = new ResolveablePromise() + private onFirstConfirmation = new ResolveablePromise() + + constructor(address: string, name: string, tx: any) { + super() + + this.address = address + this.name = name + this.tx = tx + + tx.once('transactionHash', (hash: string) => { + console.log(`Sending ${this.name} to ${this.address}: in mempool: ${hash}`) + this.onTxHash.resolve() + }) + + tx.once('confirmation', (confirmation: number, receipt: any) => { + console.log(`Sending ${this.name} to ${this.address}: confirmed:`, receipt) + this.onFirstConfirmation.resolve() + }) + } + + awaitEnterMempool(): Promise { + return this.onTxHash as any + } + + awaitFirstConfirmation(): Promise { + return this.onFirstConfirmation as any + } +} + +export interface IChannelManager { + gasMultiple: number + userAuthorizedUpdate(state: ChannelState): Promise + getPastEvents(user: Address, eventName: string, fromBlock: number): Promise + getChannelDetails(user: string): Promise +} + +export class ChannelManager implements IChannelManager { + address: string + cm: TypechainChannelManager + gasMultiple: number + + constructor(web3: any, address: string, gasMultiple: number) { + this.address = address + this.cm = new web3.eth.Contract(ChannelManagerAbi.abi, address) as any + this.gasMultiple = gasMultiple + } + + async getPastEvents(user: Address, eventName: string, fromBlock: number) { + const events = await this.cm.getPastEvents( + eventName, + { + filter: { user }, + fromBlock, + toBlock: "latest", + } + ) + return events + } + + async userAuthorizedUpdate(state: ChannelState) { + // deposit on the contract + const call = this.cm.methods.userAuthorizedUpdate( + state.recipient, // recipient + [ + state.balanceWeiHub, + state.balanceWeiUser, + ], + [ + state.balanceTokenHub, + state.balanceTokenUser, + ], + [ + state.pendingDepositWeiHub, + state.pendingWithdrawalWeiHub, + state.pendingDepositWeiUser, + state.pendingWithdrawalWeiUser, + ], + [ + state.pendingDepositTokenHub, + state.pendingWithdrawalTokenHub, + state.pendingDepositTokenUser, + state.pendingWithdrawalTokenUser, + ], + [state.txCountGlobal, state.txCountChain], + state.threadRoot, + state.threadCount, + state.timeout, + state.sigHub!, + ) + + const sendArgs = { + from: state.user, + value: state.pendingDepositWeiUser, + } as any + const gasEstimate = await call.estimateGas(sendArgs) + + sendArgs.gas = toBN(Math.ceil(gasEstimate * this.gasMultiple)) + return new Web3TxWrapper(this.address, 'userAuthorizedUpdate', call.send(sendArgs)) + } + + async getChannelDetails(user: string): Promise { + const res = await this.cm.methods.getChannelDetails(user).call({ from: user }) + return { + txCountGlobal: +res[0], + txCountChain: +res[1], + threadRoot: res[2], + threadCount: +res[3], + exitInitiator: res[4], + channelClosingTime: +res[5], + status: res[6], + } + } + +} + +export interface ConnextClientOptions { + web3: Web3 + hubUrl: string + contractAddress: string + hubAddress: Address + tokenAddress: Address + tokenName: string + user: string + gasMultiple?: number + + // Clients should pass in these functions which the ConnextClient will use + // to save and load the persistent portions of its internal state (channels, + // threads, etc). + loadState?: () => Promise + saveState?: (state: string) => Promise + + getLogger?: (name: string) => Logger + + // Optional, useful for dependency injection + hub?: IHubAPIClient + store?: ConnextStore + contract?: IChannelManager +} + + +/** + * Used to get an instance of ConnextClient. + */ +export function getConnextClient(opts: ConnextClientOptions): ConnextClient { + return new ConnextInternal(opts) +} + +/** + * The external interface to the Connext client, used by the Wallet. + * + * Create an instance with: + * + * > const client = getConnextClient({...}) + * > client.start() // start polling + * > client.on('onStateChange', state => { + * . console.log('Connext state changed:', state) + * . }) + * + */ +export abstract class ConnextClient extends EventEmitter { + opts: ConnextClientOptions + internal: ConnextInternal + + constructor(opts: ConnextClientOptions) { + super() + + this.opts = opts + this.internal = this as any + } + + /** + * Starts the stateful portions of the Connext client. + * + * Note: the full implementation lives in ConnextInternal. + */ + async start() { + } + + /** + * Stops the stateful portions of the Connext client. + * + * Note: the full implementation lives in ConnextInternal. + */ + async stop() { + } + + async deposit(payment: Payment): Promise { + await this.internal.depositController.requestUserDeposit(payment) + } + + async exchange(toSell: string, currency: "wei" | "token"): Promise { + await this.internal.exchangeController.exchange(toSell, currency) + } + + async buy(purchase: PurchaseRequest): Promise<{ purchaseId: string }> { + return await this.internal.buyController.buy(purchase) + } + + async withdraw(withdrawal: WithdrawalParameters): Promise { + await this.internal.withdrawalController.requestUserWithdrawal(withdrawal) + } + + async requestCollateral(): Promise { + await this.internal.collateralController.requestCollateral() + } +} + +/** + * The "actual" implementation of the Connext client. Internal components + * should use this type, as it provides access to the various controllers, etc. + */ +export class ConnextInternal extends ConnextClient { + store: ConnextStore + hub: IHubAPIClient + utils = new Utils() + validator: Validator + contract: IChannelManager + + // Controllers + syncController: SyncController + buyController: BuyController + depositController: DepositController + exchangeController: ExchangeController + withdrawalController: WithdrawalController + stateUpdateController: StateUpdateController + collateralController: CollateralController + + constructor(opts: ConnextClientOptions) { + super(opts) + + // Internal things + // The store shouldn't be used by anything before calling `start()`, so + // leave it null until then. + this.store = null as any + + console.log('Using hub', opts.hub ? 'provided by caller' : `at ${this.opts.hubUrl}`) + this.hub = opts.hub || new HubAPIClient( + this.opts.user, + new Networking(this.opts.hubUrl), + this.opts.tokenName, + ) + + this.validator = new Validator(opts.web3, opts.hubAddress) + this.contract = opts.contract || new ChannelManager(opts.web3, opts.contractAddress, opts.gasMultiple || 1.5) + + // Controllers + this.exchangeController = new ExchangeController('ExchangeController', this) + this.syncController = new SyncController('SyncController', this) + this.depositController = new DepositController('DepositController', this) + this.buyController = new BuyController('BuyController', this) + this.withdrawalController = new WithdrawalController('WithdrawalController', this) + this.stateUpdateController = new StateUpdateController('StateUpdateController', this) + this.collateralController = new CollateralController('CollateralController', this) + } + + private getControllers(): AbstractController[] { + const res: any[] = [] + for (let key of Object.keys(this)) { + const val = (this as any)[key] + const isController = ( + val && + isFunction(val['start']) && + isFunction(val['stop']) && + val !== this + ) + if (isController) + res.push(val) + } + return res + } + + async withdrawal(params: WithdrawalParameters): Promise { + await this.withdrawalController.requestUserWithdrawal(params) + } + + async start() { + this.store = await this.getStore() + this.store.subscribe(() => { + const state = this.store.getState() + this.emit('onStateChange', state) + this._saveState(state) + }) + + // Start all controllers + for (let controller of this.getControllers()) { + console.log('Starting:', controller.name) + await controller.start() + console.log('Done!', controller.name, 'started.') + } + + await super.start() + } + + async stop() { + // Stop all controllers + for (let controller of this.getControllers()) + await controller.stop() + + await super.stop() + } + + + dispatch(action: Action): void { + this.store.dispatch(action) + } + + async signChannelState(state: UnsignedChannelState): Promise { + if ( + state.user != this.opts.user || + state.contractAddress != this.opts.contractAddress + ) { + throw new Error( + `Refusing to sign state update which changes user or contract: ` + + `expected user: ${this.opts.user}, expected contract: ${this.opts.contractAddress} ` + + `actual state: ${JSON.stringify(state)}` + ) + } + + const hash = this.utils.createChannelStateHash(state) + + const sig = await ( + process.env.DEV + ? this.opts.web3.eth.sign(hash, this.opts.user) + : (this.opts.web3.eth.personal.sign as any)(hash, this.opts.user) + ) + + console.log(`Signing channel state ${state.txCountGlobal}: ${sig}`, state) + return addSigToChannelState(state, sig, true) + } + + public async signDepositRequestProposal(args: Omit, ): Promise { + const hash = this.utils.createDepositRequestProposalHash(args) + const sig = await ( + process.env.DEV + ? this.opts.web3.eth.sign(hash, this.opts.user) + : (this.opts.web3.eth.personal.sign as any)(hash, this.opts.user) + ) + + console.log(`Signing deposit request ${args}. Sig: ${sig}`) + return { ...args, sigUser: sig } + } + + public async getContractEvents(eventName: string, fromBlock: number) { + return this.contract.getPastEvents(this.opts.user, eventName, fromBlock) + } + + protected _latestState: PersistentState | null = null + protected _saving: Promise = Promise.resolve() + protected _savePending = false + + protected async _saveState(state: ConnextState) { + if (!this.opts.saveState) + return + + if (this._latestState === state.persistent) + return + + this._latestState = state.persistent + if (this._savePending) + return + + this._savePending = true + + this._saving = new Promise((res, rej) => { + // Only save the state after all the currently pending operations have + // completed to make sure that subsequent state updates will be atomic. + setTimeout(async () => { + let err = null + try { + await this._saveLoop() + } catch (e) { + err = e + } + // Be sure to set `_savePending` to `false` before resolve/reject + // in case the state changes during res()/rej() + this._savePending = false + return err ? rej(err) : res() + }, 1) + }) + } + + /** + * Because it's possible that the state will continue to be updated while + * a previous state is saving, loop until the state doesn't change while + * it's being saved before we return. + */ + protected async _saveLoop() { + let result: Promise | null = null + while (true) { + const state = this._latestState! + result = this.opts.saveState!(JSON.stringify(state)) + + // Wait for any current save to finish, but ignore any error it might raise + const [timeout, _] = await timeoutPromise( + result.then(null, () => null), + 10 * 1000, + ) + if (timeout) { + console.warn( + 'Timeout (10 seconds) while waiting for state to save. ' + + 'This error will be ignored (which may cause data loss). ' + + 'User supplied function that has not returned:', + this.opts.saveState + ) + } + + if (this._latestState == state) + break + } + } + + /** + * Waits for any persistent state to be saved. + * + * If the save fails, the promise will reject. + */ + awaitPersistentStateSaved(): Promise { + return this._saving + } + + protected async getStore(): Promise { + if (this.opts.store) + return this.opts.store + + const state = new ConnextState() + state.persistent.channel = { + ...state.persistent.channel, + contractAddress: this.opts.contractAddress, + user: this.opts.user, + recipient: this.opts.user, + } + state.persistent.latestValidState = state.persistent.channel + + if (this.opts.loadState) { + const loadedState = await this.opts.loadState() + if (loadedState) + state.persistent = JSON.parse(loadedState) + } + return createStore(reducers, state, applyMiddleware(handleStateFlags)) + } + + getLogger(name: string): Logger { + return { + source: name, + async logToApi(...args: any[]) { + console.log(`${name}:`, ...args) + }, + } + + } +} diff --git a/modules/client/src/StateGenerator.test.ts b/modules/client/src/StateGenerator.test.ts new file mode 100644 index 0000000000..9e44b575ea --- /dev/null +++ b/modules/client/src/StateGenerator.test.ts @@ -0,0 +1,694 @@ +import { assert } from './testing/index' +import * as t from './testing/index' +import { StateGenerator, calculateExchange } from './StateGenerator'; +import { Utils } from './Utils'; +import { convertChannelState, convertPayment, ChannelStateBN, convertThreadState, ThreadStateBN, convertExchange, convertDeposit, convertWithdrawal, convertThreadPayment, ChannelState, WithdrawalArgs, InvalidationArgs, VerboseChannelEvent, VerboseChannelEventBN, UnsignedChannelState, convertVerboseEvent, EmptyChannelArgs } from './types'; +import { getChannelState, getWithdrawalArgs } from './testing' +import { toBN } from './helpers/bn'; + + +const sg = new StateGenerator() +const utils = new Utils() + +function createHigherNoncedChannelState( + prev: ChannelStateBN, + ...overrides: t.PartialSignedOrSuccinctChannel[] +): UnsignedChannelState { + const state = t.getChannelState('empty', { + recipient: prev.user, + ...overrides[0], + txCountGlobal: prev.txCountGlobal + 1, + }) + return convertChannelState("str-unsigned", state) +} + +function createConfirmPendingArgs( + prev: ChannelStateBN, + ...overrides: Partial[] +) { + const event = Object.assign({ + ...convertChannelState("bn-unsigned", prev), + sender: prev.user, + }, overrides) + return { transactionHash: t.mkAddress('0xTTTT'), event } +} + +function createHigherNoncedThreadState( + prev: ThreadStateBN, + ...overrides: t.PartialSignedOrSuccinctThread[] +) { + const state = t.getThreadState('empty', { + ...prev, // for address vars + ...overrides[0], + txCount: prev.txCount + 1, + }) + return convertThreadState("str-unsigned", state) +} + +function createPreviousChannelState(...overrides: t.PartialSignedOrSuccinctChannel[]) { + const state = t.getChannelState('empty', { + user: t.mkAddress('0xAAA'), + recipient: t.mkAddress('0xAAA'), + ...overrides[0], + sigUser: t.mkHash('booty'), + sigHub: t.mkHash('errywhere'), + }) + return convertChannelState("bn", state) +} + +function createPreviousThreadState(...overrides: t.PartialSignedOrSuccinctThread[]) { + const state = t.getThreadState('empty', { + ...overrides[0], + sigA: t.mkHash('peachy'), + }) + return convertThreadState("bn", state) +} + +describe('StateGenerator', () => { + describe('channel payment', () => { + it('should generate a channel payment', async () => { + const prev = createPreviousChannelState({ + balanceWei: ['3', '0'], + balanceToken: ['2', '0'], + }) + + const payment = { + amountToken: '2', + amountWei: '3', + } + + const curr = sg.channelPayment(prev, convertPayment("bn", { ...payment, recipient: "user" })) + + assert.deepEqual(curr, createHigherNoncedChannelState(prev, { + balanceWei: [0, 3], + balanceToken: [0, 2], + })) + }) + }) + + describe('exchange', () => { + it('should create a wei for tokens exchange update', async () => { + const prev = createPreviousChannelState({ + balanceToken: [12, 5], + balanceWei: [5, 3], + }) + + const args = convertExchange('bn', { + exchangeRate: '4', + tokensToSell: 0, + weiToSell: 3, + seller: "user" + }) + + const curr = sg.exchange(prev, args) + + assert.deepEqual(curr, createHigherNoncedChannelState(prev, { + balanceToken: [0, 17], + balanceWei: [8, 0] + })) + }) + + it('should create a tokens for wei exchange update', async () => { + const prev = createPreviousChannelState({ + balanceToken: [17, 50], + balanceWei: [25, 3], + }) + + const args = convertExchange('bn', { + exchangeRate: '5', + tokensToSell: '25', + weiToSell: '0', + seller: "user" + }) + + const curr = sg.exchange(prev, args) + + assert.deepEqual(curr, createHigherNoncedChannelState(prev, { + balanceToken: [42, 25], + balanceWei: [20, 8] + })) + }) + }) + + describe('deposit', () => { + it('should create a propose pending deposit update', async () => { + const prev = createPreviousChannelState() + + const args = convertDeposit('bn', { + depositWeiHub: '5', + depositWeiUser: '3', + depositTokenHub: '1', + depositTokenUser: '9', + sigUser: t.mkHash('0xsigUser'), + timeout: 600 + }) + + const curr = sg.proposePendingDeposit(prev, args) + + assert.deepEqual(curr, createHigherNoncedChannelState(prev, { + pendingDepositToken: [1, 9], + pendingDepositWei: [5, 3], + timeout: 600, + txCountChain: prev.txCountChain + 1, + })) + }) + }) + + type WDTest = { + name: string + prev: Partial> + args: Partial> + expected: Partial> + } + + const exchangeRate = 5 + const withdrawalTests: WDTest[] = [ + { + name: 'Simple sell tokens', + prev: { + balanceTokenUser: 7, + }, + args: { + tokensToSell: 5, + }, + expected: { + balanceTokenUser: 2, + pendingWithdrawalTokenHub: 5, + pendingDepositWeiUser: 1, + pendingWithdrawalWeiUser: 1, + }, + }, + + { + name: 'Sell tokens, withdraw wei, hub deposit tokens', + prev: { + balanceTokenUser: 7, + balanceWeiUser: 6, + }, + args: { + tokensToSell: 5, + targetWeiUser: 4, + targetTokenHub: 20, + }, + expected: { + balanceTokenHub: 5, + balanceWeiUser: 4, + balanceTokenUser: 2, + pendingDepositTokenHub: 15, + pendingWithdrawalWeiUser: 3, + pendingDepositWeiUser: 1, + }, + }, + + { + name: 'Sell tokens, increase tokens balance, add some wei to balance', + prev: { + balanceTokenUser: 3, + }, + args: { + tokensToSell: 1, + targetWeiUser: 11, + targetTokenUser: 5, + }, + expected: { + pendingDepositWeiUser: 11, + pendingDepositTokenUser: 2, + }, + }, + + { + name: 'Hub deposit and withdrawal simplification', + prev: { + balanceTokenHub: 11, + balanceWeiHub: 8, + }, + args: { + targetTokenHub: 24, + targetWeiHub: 20, + }, + expected: { + balanceTokenHub: 11, + balanceWeiHub: 8, + pendingDepositTokenHub: 13, + pendingDepositWeiHub: 12, + }, + }, + + { + name: 'Hub send additional wei + tokens', + prev: { + balanceTokenHub: 11, + balanceWeiHub: 11, + balanceTokenUser: 5, + }, + args: { + targetTokenHub: 12, + targetWeiHub: 13, + tokensToSell: 5, + additionalTokenHubToUser: 3, + additionalWeiHubToUser: 4, + }, + expected: { + balanceTokenHub: 12, + pendingWithdrawalTokenHub: 4, + + balanceWeiHub: 10, + pendingDepositWeiHub: 3, + + balanceTokenUser: 0, + pendingDepositTokenUser: 3, + pendingWithdrawalTokenUser: 3, + + balanceWeiUser: 0, + pendingDepositWeiUser: 4, + pendingWithdrawalWeiUser: 5, + }, + }, + + { + name: 'User withdrawal and hub recollatoralize', + prev: { + balanceWeiHub: 10, + balanceTokenHub: 3, + balanceWeiUser: 7, + balanceTokenUser: 5, + }, + args: { + targetWeiHub: 0, + targetTokenHub: 6, + targetWeiUser: 5, + tokensToSell: 5, + }, + expected: { + balanceWeiHub: 0, + balanceWeiUser: 5, + balanceTokenUser: 0, + balanceTokenHub: 6, + pendingWithdrawalWeiHub: 9, + pendingWithdrawalWeiUser: 3, + pendingWithdrawalTokenHub: 2, + }, + }, + + { + name: 'Token sale and hub adds additional tokens', + prev: { + balanceTokenUser: 17, + }, + args: { + tokensToSell: 15, + targetWeiUser: 1, + targetTokenHub: 20, + }, + expected: { + balanceWeiUser: 0, + balanceTokenUser: 2, + pendingDepositWeiUser: 3, + pendingWithdrawalWeiUser: 2, + balanceTokenHub: 15, + pendingDepositTokenHub: 5, + }, + }, + ] + + const args2Str = (args: any) => { + return Object.entries(args).map((x: any) => `${x[0]}: ${x[1]}`).join(', ') + } + + describe('Withdrawal states', () => { + withdrawalTests.forEach(tc => { + it(tc.name + ': ' + args2Str(tc.args), () => { + const prev = convertChannelState('bn', getChannelState('empty', tc.prev)) + const args = convertWithdrawal('bn', getWithdrawalArgs('empty', tc.args, { + exchangeRate: exchangeRate.toString(), + })) + const s = convertChannelState('str-unsigned', sg.proposePendingWithdrawal(prev, args)) + + const expected = { + ...prev, + ...tc.expected, + txCountGlobal: 2, + txCountChain: 2, + timeout: 6969, + } + + assert.deepEqual(s, convertChannelState('str-unsigned', expected)) + }) + }) + }) + + describe('invalidation', () => { + it('should work', async () => { + const prev = createPreviousChannelState({ + txCount: [3, 2], + }) + const args: InvalidationArgs = { + previousValidTxCount: prev.txCountGlobal, + lastInvalidTxCount: 7, + reason: "CU_INVALID_ERROR", + } + + const curr = sg.invalidation(prev, args) + assert.deepEqual(curr, { ...convertChannelState("str-unsigned", prev), txCountGlobal: 8, }) + }) + }) + + describe('emptyChannel', () => { + it('should work', async () => { + const prev = createPreviousChannelState({ + pendingDepositWei: [0, 0,], + pendingDepositToken: [0, 0], + pendingWithdrawalWei: [0, 0], + pendingWithdrawalToken: [0, 0], + txCount: [3, 2], + }) + const event: VerboseChannelEventBN = { ...convertChannelState("bn-unsigned", prev), sender: prev.user } + const args: EmptyChannelArgs = { + transactionHash: t.mkHash('0xTTT') + } + + const curr = sg.emptyChannel(event) + assert.deepEqual(curr, { ...convertChannelState("str-unsigned", prev), txCountGlobal: prev.txCountGlobal + 1, }) + }) + }) + + describe('calculateExchange', () => { + type ExchangeTest = { + seller: 'user' | 'hub' + exchangeRate: number + tokensToSell: number + weiToSell: number + expected: Partial<{ + ws: number + ts: number + wr: number + tr: number + }> + } + + const exchangeTests: Partial[] = [ + { tokensToSell: 10, expected: { ts: 10, wr: 2 } }, + { tokensToSell: 4, expected: { tr: 4 } }, + { weiToSell: 1, expected: { tr: 5, ws: 1 } }, + { weiToSell: 2, expected: { tr: 10, ws: 2 } }, + { weiToSell: 3, expected: { tr: 3 * 5, ws: 3 } }, + ] + + exchangeTests.forEach(_t => { + const t: ExchangeTest = { + exchangeRate: 5, + tokensToSell: 0, + weiToSell: 0, + ...(_t as any), + } + t.expected = { + ws: 0, + ts: 0, + wr: 0, + tr: 0, + ...t.expected, + } + + for (const seller of ['user', 'hub']) { + const flip = (x: number | undefined) => seller == 'hub' ? -x! : x + it(seller + ': ' + JSON.stringify(_t), () => { + const actual = calculateExchange({ + exchangeRate: '' + t.exchangeRate, + seller: seller as any, + tokensToSell: toBN(t.tokensToSell), + weiToSell: toBN(t.weiToSell), + }) + + assert.deepEqual({ + weiSold: actual.weiSold.toString(), + weiReceived: actual.weiReceived.toString(), + tokensSold: actual.tokensSold.toString(), + tokensReceived: actual.tokensReceived.toString(), + }, { + weiSold: '' + flip(t.expected.ws), + weiReceived: '' + flip(t.expected.wr), + tokensSold: '' + flip(t.expected.ts), + tokensReceived: '' + flip(t.expected.tr), + }) + + }) + } + }) + }) + + describe('openThread', () => { + it('should create an open thread update with user as sender', async () => { + const prev = createPreviousChannelState({ + balanceToken: [10, 10], + balanceWei: [10, 10] + }) + + const args = createPreviousThreadState({ + sender: prev.user, + balanceWei: [10, 0], + balanceToken: [10, 0], + }) + + const curr = sg.openThread(prev, [], args) + + assert.deepEqual(curr, createHigherNoncedChannelState(prev, { + balanceToken: [10, 0], + balanceWei: [10, 0], + threadCount: 1, + threadRoot: utils.generateThreadRootHash([convertThreadState("str", args)]), + })) + }) + + it('should create an open thread update with user as receiver', async () => { + const prev = createPreviousChannelState({ + balanceToken: [10, 10], + balanceWei: [10, 10] + }) + + const args = createPreviousThreadState({ + receiver: prev.user, + balanceWei: [10, 0], + balanceToken: [10, 0], + }) + + const curr = sg.openThread(prev, [], args) + + assert.deepEqual(curr, createHigherNoncedChannelState(prev, { + balanceToken: [0, 10], + balanceWei: [0, 10], + threadCount: 1, + threadRoot: utils.generateThreadRootHash([convertThreadState("str", args)]), + })) + }) + }) + + describe('closeThread', () => { + it('should create a close thread update with user as sender', async () => { + const prev = createPreviousChannelState({ + balanceToken: [10, 0], + balanceWei: [10, 0] + }) + + const initialThread = createPreviousThreadState({ + sender: prev.user, + balanceWei: [10, 0], + balanceToken: [10, 0], + }) + + const currThread = createHigherNoncedThreadState(initialThread, { + balanceToken: [9, 1], + balanceWei: [9, 1], + }) + + const curr = sg.closeThread(prev, [convertThreadState("str", initialThread)], convertThreadState("bn-unsigned", currThread)) + + assert.deepEqual(curr, createHigherNoncedChannelState(prev, { + balanceToken: [11, 9], + balanceWei: [11, 9], + })) + }) + + it('should create a close thread update with user as receiver', async () => { + const prev = createPreviousChannelState({ + balanceToken: [0, 10], + balanceWei: [0, 10] + }) + + const initialThread = createPreviousThreadState({ + receiver: prev.user, + balanceWei: [10, 0], + balanceToken: [10, 0], + }) + + const currThread = createHigherNoncedThreadState(initialThread, { + balanceToken: [9, 1], + balanceWei: [9, 1], + }) + + const curr = sg.closeThread(prev, [convertThreadState("str", initialThread)], convertThreadState("bn-unsigned", currThread)) + + assert.deepEqual(curr, createHigherNoncedChannelState(prev, { + balanceToken: [9, 11], + balanceWei: [9, 11], + })) + }) + }) + + describe('thread payment', () => { + it('should create a thread payment', async () => { + const prev = createPreviousThreadState({ + balanceWei: [10, 0], + balanceToken: [10, 0], + }) + + const payment = { + amountToken: '10', + amountWei: '10', + } + + const curr = sg.threadPayment(prev, convertThreadPayment("bn", payment)) + + assert.deepEqual(curr, createHigherNoncedThreadState(prev, { + balanceToken: [0, 10], + balanceWei: [0, 10], + })) + }) + }) + + describe('confirmPending', () => { + it('should confirm a pending deposit', async () => { + const prev: ChannelStateBN = createPreviousChannelState({ + pendingDepositToken: [8, 4], + pendingDepositWei: [1, 6], + recipient: t.mkHash('0x222') + }) + + // For the purposes of these tests, ensure that the recipient is not the + // same as the user so we can verify that `confirmPending` will change it + // back to the user. + assert.notEqual(prev.recipient, prev.user) + + const expected = createHigherNoncedChannelState(prev, { + balanceToken: [8, 4], + balanceWei: [1, 6], + recipient: prev.user, + }) + + const curr = sg.confirmPending(prev) + + assert.deepEqual(curr, expected) + }) + + it('should confirm a pending withdrawal', async () => { + const prev = createPreviousChannelState({ + pendingWithdrawalToken: [8, 4], + pendingWithdrawalWei: [1, 6], + }) + + const expected = createHigherNoncedChannelState(prev, { + recipient: prev.user, + }) + + const curr = sg.confirmPending(prev) + + + assert.deepEqual(curr, expected) + }) + + it('should confirm a pending withdrawal with a hub deposit into user channel equal to withdrawal wei', async () => { + const prev = createPreviousChannelState({ + pendingDepositWei: [0, 7], + pendingWithdrawalWei: [0, 7], + pendingWithdrawalToken: [7, 0], + }) + + const expected = createHigherNoncedChannelState(prev, { + recipient: prev.user, + }) + + const curr = sg.confirmPending(prev) + + assert.deepEqual(curr, expected) + }) + + it('should confirm a pending withdrawal with a hub deposit into user channel equal to withdrawal token', async () => { + const prev = createPreviousChannelState({ + pendingDepositToken: [0, 7], + pendingWithdrawalToken: [0, 7], + pendingWithdrawalWei: [7, 0], + }) + + const expected = createHigherNoncedChannelState(prev, { + recipient: prev.user, + }) + + const curr = sg.confirmPending(prev) + + assert.deepEqual(curr, expected) + }) + + it('should confirm a pending withdrawal with a hub deposit into user channel less than withdrawal wei', async () => { + const prev = createPreviousChannelState({ + pendingDepositWei: [0, 10], + pendingWithdrawalWei: [0, 15], + pendingWithdrawalToken: [60, 0], + }) + + const expected = createHigherNoncedChannelState(prev, { + recipient: prev.user, + }) + + const curr = sg.confirmPending(prev) + + assert.deepEqual(curr, expected) + }) + + it('should confirm a pending withdrawal with a hub deposit into user channel less than withdrawal token', async () => { + const prev = createPreviousChannelState({ + pendingDepositToken: [0, 3], + pendingWithdrawalToken: [0, 15], + pendingWithdrawalWei: [3, 0], + }) + + const expected = createHigherNoncedChannelState(prev, { + recipient: prev.user, + }) + + const curr = sg.confirmPending(prev) + + assert.deepEqual(curr, expected) + }) + + it('should confirm a pending withdrawal with a hub deposit into user channel greater than withdrawal wei', async () => { + const prev = createPreviousChannelState({ + pendingDepositWei: [0, 12], + pendingWithdrawalWei: [10, 7], + }) + + const expected = createHigherNoncedChannelState(prev, { + balanceWei: [0, 5], + recipient: prev.user, + }) + + const curr = sg.confirmPending(prev) + + assert.deepEqual(curr, expected) + }) + + it('should confirm a pending withdrawal with a hub deposit into user channel greater than withdrawal token', async () => { + const prev = createPreviousChannelState({ + pendingDepositToken: [0, 12], + pendingWithdrawalToken: [10, 7], + }) + + const expected = createHigherNoncedChannelState(prev, { + recipient: prev.user, + balanceToken: [0, 5] + }) + + const curr = sg.confirmPending(prev) + + assert.deepEqual(curr, expected) + }) + }) +}) diff --git a/modules/client/src/StateGenerator.ts b/modules/client/src/StateGenerator.ts new file mode 100644 index 0000000000..435c65becd --- /dev/null +++ b/modules/client/src/StateGenerator.ts @@ -0,0 +1,636 @@ +import { Utils } from "./Utils"; +import { + ChannelStateBN, + PaymentArgsBN, + UnsignedChannelState, + ExchangeArgsBN, + DepositArgsBN, + WithdrawalArgsBN, + UnsignedThreadState, + UnsignedThreadStateBN, + convertThreadState, + ThreadStateBN, + convertChannelState, + PaymentBN, + UnsignedChannelStateBN, + PendingArgsBN, + PendingExchangeArgsBN, + ChannelUpdateReason, + UpdateRequestBN, + InvalidationArgs, + VerboseChannelEventBN, +} from "./types"; +import { toBN, mul, minBN, maxBN } from "./helpers/bn"; +import BN = require('bn.js') + +// this constant is used to not lose precision on exchanges +// the BN library does not handle non-integers appropriately +const EXCHANGE_MULTIPLIER = 1000000000 +const EXCHANGE_MULTIPLIER_BN = toBN(EXCHANGE_MULTIPLIER) + +/** + * Calculate the amount of wei/tokens to sell/recieve from the perspective of + * the user. + * + * If the 'seller' is the hub, the amounts will be multiplied by -1 so callers + * can apply the values as if they were from the perspective of the user. + * + * Note: because the number of tokens being sold may not cleanly divide into + * the exchange rate, the number of tokens sold (ie, 'res.tokensSold') may be + * slightly lower than the number of tokens reqested to sell (ie, + * 'args.tokensToSell'). For this reason, it's important that callers use the + * 'tokensSold' field to calculate how many tokens are being transferred:: + * + * const exchange = calculateExchange(...) + * state.balanceWeiUser -= exchange.weiSold + * state.balanceTokenUser -= exchange.tokensSold + * state.balanceWeiHub += exchange.weiReceived + * state.balanceTokenHub += exchange.tokensReceived + * + */ +export function calculateExchange(args: ExchangeArgsBN) { + // Assume the exchange is done from the perspective of the user. If it's + // the hub, multiply all the values by -1 so the math will still work. + if (args.seller == 'hub') { + const neg1 = toBN(-1) + args = { + ...args, + weiToSell: args.weiToSell.mul(neg1), + tokensToSell: args.tokensToSell.mul(neg1), + } + } + + const exchangeRate = toBN(mul(args.exchangeRate, EXCHANGE_MULTIPLIER)) + const [weiReceived, tokenRemainder] = divmod(args.tokensToSell.mul(EXCHANGE_MULTIPLIER_BN), exchangeRate) + const tokensReceived = args.weiToSell.mul(exchangeRate).div(EXCHANGE_MULTIPLIER_BN) + + return { + weiSold: args.weiToSell, + weiReceived: weiReceived, + tokensSold: args.tokensToSell.sub(tokenRemainder.div(EXCHANGE_MULTIPLIER_BN)), + tokensReceived: tokensReceived.add(tokenRemainder.div(EXCHANGE_MULTIPLIER_BN)), + } +} + +function divmod(num: BN, div: BN): [BN, BN] { + return [ + safeDiv(num, div), + safeMod(num, div), + ] +} + +function safeMod(num: BN, div: BN) { + if (div.isZero()) + return div + return num.mod(div) +} + +function safeDiv(num: BN, div: BN) { + if (div.isZero()) + return div + return num.div(div) +} + +export function objMap(obj: T, func: (val: T[F], field: F) => R): { [key in keyof T]: R } { + const res: any = {} + for (let key in obj) + res[key] = func(key as any, obj[key] as any) + return res +} + +function coalesce(...vals: (T | null | undefined)[]): T | undefined { + for (let v of vals) { + if (v !== null && v !== undefined) + return v + } + return undefined +} + +/** + * Subtracts the arguments, returning either the value (if greater than zero) + * or zero. + */ +export function subOrZero(a: (BN | undefined), ...args: (BN | undefined)[]): BN { + let res = a! + for (let arg of args) + res = res.sub(arg!) + return maxBN(toBN(0), res) +} + +/** + * Returns 'a' if a > 0 else 0. + */ +function ifPositive(a: BN) { + const zero = toBN(0) + return a.gt(zero) ? a : zero +} + +/** + * Returns 'a.abs()' if a < 0 else 0. + */ +function ifNegative(a: BN) { + const zero = toBN(0) + return a.lt(zero) ? a.abs() : zero +} + +export class StateGenerator { + private utils: Utils + + stateTransitionHandlers: { [name in ChannelUpdateReason]: any } + + constructor() { + this.utils = new Utils() + this.stateTransitionHandlers = { + 'Payment': this.channelPayment.bind(this), + 'Exchange': this.exchange.bind(this), + 'ProposePendingDeposit': this.proposePendingDeposit.bind(this), + 'ProposePendingWithdrawal': this.proposePendingWithdrawal.bind(this), + 'ConfirmPending': this.confirmPending.bind(this), + 'Invalidation': this.invalidation.bind(this), + 'EmptyChannel': this.emptyChannel.bind(this), + 'OpenThread': () => { throw new Error('REB-36: enbable threads!') }, + 'CloseThread': () => { throw new Error('REB-36: enbable threads!') }, + } + } + + public createChannelStateFromRequest(prev: ChannelStateBN, request: UpdateRequestBN): UnsignedChannelState { + return this.stateTransitionHandlers[request.reason](prev, request.args) + } + + public channelPayment(prev: ChannelStateBN, args: PaymentArgsBN): UnsignedChannelState { + return convertChannelState("str-unsigned", { + ...prev, + balanceWeiHub: args.recipient === 'hub' ? prev.balanceWeiHub.add(args.amountWei) : prev.balanceWeiHub.sub(args.amountWei), + balanceWeiUser: args.recipient === 'user' ? prev.balanceWeiUser.add(args.amountWei) : prev.balanceWeiUser.sub(args.amountWei), + balanceTokenHub: args.recipient === 'hub' ? prev.balanceTokenHub.add(args.amountToken) : prev.balanceTokenHub.sub(args.amountToken), + balanceTokenUser: args.recipient === 'user' ? prev.balanceTokenUser.add(args.amountToken) : prev.balanceTokenUser.sub(args.amountToken), + txCountGlobal: prev.txCountGlobal + 1, + timeout: 0, + }) + } + + public exchange(prev: ChannelStateBN, args: ExchangeArgsBN): UnsignedChannelState { + return convertChannelState("str-unsigned", { + ...this.applyInChannelExchange(prev, args), + txCountGlobal: prev.txCountGlobal + 1, + timeout: 0, + }) + } + + public proposePendingDeposit(prev: ChannelStateBN, args: DepositArgsBN): UnsignedChannelState { + return convertChannelState("str-unsigned", { + ...prev, + recipient: prev.user, // set explicitly for case of 1st deposit + pendingDepositWeiHub: args.depositWeiHub, + pendingDepositWeiUser: args.depositWeiUser, + pendingDepositTokenHub: args.depositTokenHub, + pendingDepositTokenUser: args.depositTokenUser, + txCountGlobal: prev.txCountGlobal + 1, + txCountChain: prev.txCountChain + 1, + timeout: args.timeout, + }) + } + + /** + * Apply an exchange to the state, assuming that there is sufficient blance + * (otherwise the result may have negative balances; see also: + * applyCollateralizedExchange). + */ + public applyInChannelExchange(state: UnsignedChannelStateBN, exchangeArgs: ExchangeArgsBN): UnsignedChannelStateBN { + const exchange = calculateExchange(exchangeArgs) + + const res = { + ...state, + + balanceWeiUser: state.balanceWeiUser + .add(exchange.weiReceived) + .sub(exchange.weiSold), + + balanceTokenUser: state.balanceTokenUser + .add(exchange.tokensReceived) + .sub(exchange.tokensSold), + + balanceWeiHub: state.balanceWeiHub + .sub(exchange.weiReceived) + .add(exchange.weiSold), + + balanceTokenHub: state.balanceTokenHub + .sub(exchange.tokensReceived) + .add(exchange.tokensSold), + } + + return res + } + + /** + * Apply an exchange to the state, adding a pending deposit to the user if + * the hub doesn't have sufficient balance (note: collateral will only be + * added when the hub is selling to the user; collateral will never be + * deposited into the user's channel). + */ + public applyCollateralizedExchange(state: UnsignedChannelStateBN, exchangeArgs: ExchangeArgsBN): UnsignedChannelStateBN { + let res = this.applyInChannelExchange(state, exchangeArgs) + + function depositIfNegative(r: any, src: string, dst: string) { + // If `balance${src}` is negative, make it zero, remove that balance from + // `balance${dst}` and add the balance to the `pendingDeposit${dst}` + const bal = r['balance' + src] as BN + if (bal.lt(toBN(0))) { + r['balance' + src] = toBN(0) + r['balance' + dst] = r['balance' + dst].sub(bal.abs()) + r['pendingDeposit' + dst] = r['pendingDeposit' + dst].add(bal.abs()) + } + return res + } + + res = depositIfNegative(res, 'WeiHub', 'WeiUser') + res = depositIfNegative(res, 'TokenHub', 'TokenUser') + + return res + } + + public applyPending(state: UnsignedChannelStateBN, args: PendingArgsBN): UnsignedChannelStateBN { + const res = { + ...state, + + pendingDepositWeiHub: args.depositWeiHub.add(state.pendingDepositWeiHub), + pendingDepositWeiUser: args.depositWeiUser.add(state.pendingDepositWeiUser), + pendingDepositTokenHub: args.depositTokenHub.add(state.pendingDepositTokenHub), + pendingDepositTokenUser: args.depositTokenUser.add(state.pendingDepositTokenUser), + pendingWithdrawalWeiHub: args.withdrawalWeiHub.add(state.pendingWithdrawalWeiHub), + pendingWithdrawalWeiUser: args.withdrawalWeiUser.add(state.pendingWithdrawalWeiUser), + pendingWithdrawalTokenHub: args.withdrawalTokenHub.add(state.pendingWithdrawalTokenHub), + pendingWithdrawalTokenUser: args.withdrawalTokenUser.add(state.pendingWithdrawalTokenUser), + + recipient: args.recipient, + timeout: args.timeout, + } + + return { + ...res, + + balanceWeiHub: state.balanceWeiHub + .sub(subOrZero(res.pendingWithdrawalWeiHub, res.pendingDepositWeiHub)), + + balanceTokenHub: state.balanceTokenHub + .sub(subOrZero(res.pendingWithdrawalTokenHub, res.pendingDepositTokenHub)), + + balanceWeiUser: state.balanceWeiUser + .sub(subOrZero(res.pendingWithdrawalWeiUser, res.pendingDepositWeiUser)), + + balanceTokenUser: state.balanceTokenUser + .sub(subOrZero(res.pendingWithdrawalTokenUser, res.pendingDepositTokenUser)), + } + } + + public proposePending(prev: UnsignedChannelStateBN, args: PendingArgsBN): UnsignedChannelState { + const pending = this.applyPending(convertChannelState('bn-unsigned', prev), args) + return convertChannelState('str-unsigned', { + ...pending, + txCountChain: prev.txCountChain + 1, + txCountGlobal: prev.txCountGlobal + 1, + }) + } + + // Takes the pending update params as well as offchain exchange params, and + // applies the exchange params first + // + // This can result in negative balances - the validator for this will prevent + // this + public proposePendingExchange(prev: UnsignedChannelStateBN, args: PendingExchangeArgsBN): UnsignedChannelState { + const exchange = this.applyInChannelExchange(convertChannelState('bn-unsigned', prev), args) + const pending = this.applyPending(exchange, args) + return convertChannelState('str-unsigned', { + ...pending, + txCountChain: prev.txCountChain + 1, + txCountGlobal: prev.txCountGlobal + 1, + }) + } + + /** + * Any time there is a user deposit and a hub withdrawal, the state can be + * simplified so it becomes an in-channel exchange. + * + * For example: + * + * balanceUser: 0 + * balanceHub: 5 + * pendingDepositUser: 10 + * pendingWithdrawalHub: 7 + * + * Can be simplified to: + * + * balanceUser: 7 + * balanceHub: 5 + * pendingDepositUser: 3 + * pendingWithdrawalHub: 0 + * + * NOTE: This function is un-used. See comment at top of function. + */ + private _unused_applyInChannelTransferSimplifications(state: UnsignedChannelStateBN): UnsignedChannelStateBN { + state = { ...state } + + // !!! NOTE !!! + // This function is currently un-used because: + // 1. At present there isn't a need to optimize in-channel balance + // transfers, and + // 2. It has not been exhaustively tested. + // + // It is being left in place because: + // 1. In the future it may be desierable to optimize in-channel balance + // exchanges, and + // 2. There will likely be future discussions around "maybe we should + // optmize balance transfers!", and this comment will serve as a + // starting point to the discussion. + // !!! NOTE !!! + + const s = state as any + + // Hub is withdrawing from their balance and a balance is being sent from + // reserve to the user. Deduct from the hub's pending withdrawal, the + // user's pending deposit, and add to the user's balance: + // + // balanceUser: 0 + // pendingDepositUser: 4 + // pendingWithdrawalUser: 1 + // pendingWithdrawalHub: 9 + // + // Becomes: + // + // balanceUser: 3 + // pendingDepositUser: 0 + // pendingWithdrawalUser: 1 + // pendingWithdrawalHub: 5 + // + for (const type of ['Wei', 'Token']) { + // First, calculate how much can be added directly from the hub's + // withdrawal to the user's balance (this potentially leaves a deposit + // that will be immediately withdrawn, which is handled below): + // + // pendingWithdrawalUser: 1 + // pendingWithdrawalHub: 9 + // pendingDepositUser: 4 + // balanceUser: 0 + // + // Becomes: + // + // pendingWithdrawalUser: 1 + // pendingWithdrawalHub: 6 (9 - (4 - 1)) + // pendingDepositUser: 1 (4 - (4 - 1)) + // balanceUser: 3 (0 + (4 - 1)) + // + let delta = minBN( + // Amount being deducted from the hub's balance + subOrZero(s[`pendingWithdrawal${type}Hub`], s[`pendingDeposit${type}Hub`]), + // Amount being added to the user's balance + subOrZero(s[`pendingDeposit${type}User`], s[`pendingWithdrawal${type}User`]), + ) + s[`pendingWithdrawal${type}Hub`] = s[`pendingWithdrawal${type}Hub`].sub(delta) + s[`pendingDeposit${type}User`] = s[`pendingDeposit${type}User`].sub(delta) + s[`balance${type}User`] = s[`balance${type}User`].add(delta) + + // Second, calculate how much can be deducted from both the hub's + // withdrawal and the user deposit: + // + // pendingWithdrawalUser: 1 + // pendingWithdrawalHub: 6 + // pendingDepositUser: 1 + // balanceUser: 3 + // + // Becomes: + // + // pendingWithdrawalUser: 1 + // pendingWithdrawalHub: 5 (6 - 1) + // pendingDepositUser: 0 (1 - 1) + // balanceUser: 3 + // + delta = minBN( + // Amount being deducted from the hub's balance + subOrZero(s[`pendingWithdrawal${type}Hub`], s[`pendingDeposit${type}Hub`]), + // Amount being sent to the user for direct withdrawal + s[`pendingDeposit${type}User`], + ) + s[`pendingWithdrawal${type}Hub`] = s[`pendingWithdrawal${type}Hub`].sub(delta) + s[`pendingDeposit${type}User`] = s[`pendingDeposit${type}User`].sub(delta) + } + + // User is withdrawing from their balance and a deposit is being made from + // reserve into the hub's balance. Increase the user's pending deposit, + // decrease the hub's deposit, and add to the hub's balance: + // + // pendingWithdrawalUser: 5 + // pendingDepositHub: 3 + // balanceHub: 0 + // + // Becomes: + // + // pendingWithdrawalUser: 5 + // pendingDepositUser: 3 + // balanceHub: 3 + // + for (const type of ['Wei', 'Token']) { + let delta = minBN( + // Amount being deducted from the user's balance + subOrZero(s[`pendingWithdrawal${type}User`], s[`pendingDeposit${type}User`]), + // Amount being added from reserve to the hub's balance + subOrZero(s[`pendingDeposit${type}Hub`], s[`pendingWithdrawal${type}Hub`]), + ) + s[`pendingDeposit${type}User`] = s[`pendingDeposit${type}User`].add(delta) + s[`pendingDeposit${type}Hub`] = s[`pendingDeposit${type}Hub`].sub(delta) + s[`balance${type}Hub`] = s[`balance${type}Hub`].add(delta) + } + + return state + } + + /** + * Creates WithdrawalArgs based on more user-friendly inputs. + * + * See comments on the CreateWithdrawal type for a description. + */ + public proposePendingWithdrawal(prev: UnsignedChannelStateBN, args: WithdrawalArgsBN): UnsignedChannelState { + args = { + ...args, + targetWeiUser: coalesce( + args.targetWeiUser, + prev.balanceWeiUser.sub(args.weiToSell), + ), + targetTokenUser: coalesce( + args.targetTokenUser, + prev.balanceTokenUser.sub(args.tokensToSell), + ), + targetWeiHub: coalesce(args.targetWeiHub, prev.balanceWeiHub), + targetTokenHub: coalesce(args.targetTokenHub, prev.balanceTokenHub), + } + + const exchange = this.applyCollateralizedExchange(prev, args) + + const deltas = { + userWei: args.targetWeiUser!.sub(exchange.balanceWeiUser.add(exchange.pendingDepositWeiUser)), + userToken: args.targetTokenUser!.sub(exchange.balanceTokenUser.add(exchange.pendingDepositTokenUser)), + hubWei: args.targetWeiHub!.sub(exchange.balanceWeiHub.add(exchange.pendingDepositWeiHub)), + hubToken: args.targetTokenHub!.sub(exchange.balanceTokenHub.add(exchange.pendingDepositTokenHub)), + } + + const pending = this.applyPending(exchange, { + depositWeiUser: toBN(0) + .add(ifPositive(deltas.userWei)) + .add(args.additionalWeiHubToUser || toBN(0)), + + depositWeiHub: ifPositive(deltas.hubWei), + + depositTokenUser: toBN(0) + .add(ifPositive(deltas.userToken)) + .add(args.additionalTokenHubToUser || toBN(0)), + + depositTokenHub: ifPositive(deltas.hubToken), + + withdrawalWeiUser: toBN(0) + .add(ifNegative(deltas.userWei)) + .add(args.additionalWeiHubToUser || toBN(0)), + + withdrawalWeiHub: ifNegative(deltas.hubWei), + + withdrawalTokenUser: toBN(0) + .add(ifNegative(deltas.userToken)) + .add(args.additionalTokenHubToUser || toBN(0)), + + withdrawalTokenHub: ifNegative(deltas.hubToken), + + recipient: args.recipient, + timeout: args.timeout, + }) + + return convertChannelState('str-unsigned', { + ...pending, + txCountChain: prev.txCountChain + 1, + txCountGlobal: prev.txCountGlobal + 1, + }) + + } + + public confirmPending(prev: ChannelStateBN): UnsignedChannelState { + // consider case where confirmPending for a withdrawal with exchange: + // prev.pendingWeiUpdates = [0, 0, 5, 5] // i.e. hub deposits into user's channel for facilitating exchange + // generated.balanceWei = [0, 0] + // + // initial = [0, 2] + // prev.balance = [0, 1] + // prev.pending = [0, 0, 1, 2] + // final.balance = [0, 1] + + // the only event values used directly are the pending operations + // and the txCountChain + return convertChannelState("str-unsigned", { + ...prev, + balanceWeiHub: prev.pendingDepositWeiHub.gt(prev.pendingWithdrawalWeiHub) + ? prev.balanceWeiHub.add(prev.pendingDepositWeiHub).sub(prev.pendingWithdrawalWeiHub) + : prev.balanceWeiHub, + balanceWeiUser: prev.pendingDepositWeiUser.gt(prev.pendingWithdrawalWeiUser) + ? prev.balanceWeiUser.add(prev.pendingDepositWeiUser).sub(prev.pendingWithdrawalWeiUser) + : prev.balanceWeiUser, + balanceTokenHub: prev.pendingDepositTokenHub.gt(prev.pendingWithdrawalTokenHub) + ? prev.balanceTokenHub.add(prev.pendingDepositTokenHub).sub(prev.pendingWithdrawalTokenHub) + : prev.balanceTokenHub, + balanceTokenUser: prev.pendingDepositTokenUser.gt(prev.pendingWithdrawalTokenUser) + ? prev.balanceTokenUser.add(prev.pendingDepositTokenUser).sub(prev.pendingWithdrawalTokenUser) + : prev.balanceTokenUser, + // reset pending values + pendingDepositWeiHub: toBN(0), + pendingDepositWeiUser: toBN(0), + pendingDepositTokenHub: toBN(0), + pendingDepositTokenUser: toBN(0), + pendingWithdrawalWeiHub: toBN(0), + pendingWithdrawalWeiUser: toBN(0), + pendingWithdrawalTokenHub: toBN(0), + pendingWithdrawalTokenUser: toBN(0), + // account for offchain updates by using prev txCountGlobal + txCountGlobal: prev.txCountGlobal + 1, + // use chain tx count from event + txCountChain: prev.txCountChain, + // ^ enforced in validation to be equal + // reset recipient + timeout + recipient: prev.user, + timeout: 0, + }) + } + + ////////////////////////// + // UNILATERAL FUNCTIONS // + ////////////////////////// + // the transaction count in the args is used to ensure consistency + // between what is expected and what is emitted from the event during + // this state transition. `validator` ensures their truthfulness + public emptyChannel(event: VerboseChannelEventBN): UnsignedChannelState { + // state called to represent the channel being emptied + // should increase the global nonce + const { sender, ...channel } = event + return convertChannelState("str-unsigned", { + ...channel, + recipient: channel.user, + timeout: 0, + txCountGlobal: channel.txCountGlobal + 1, + }) + } + + + // TODO: should the args be a signed thread state or unsigned thread state? + public openThread(prev: ChannelStateBN, initialThreadStates: UnsignedThreadState[], args: UnsignedThreadStateBN): UnsignedChannelState { + initialThreadStates.push(convertThreadState("str-unsigned", args)) + return convertChannelState("str-unsigned", { + ...prev, + balanceWeiHub: args.sender === prev.user ? prev.balanceWeiHub : prev.balanceWeiHub.sub(args.balanceWeiSender), + balanceWeiUser: args.sender === prev.user ? prev.balanceWeiUser.sub(args.balanceWeiSender) : prev.balanceWeiUser, + balanceTokenHub: args.sender === prev.user ? prev.balanceTokenHub : prev.balanceTokenHub.sub(args.balanceTokenSender), + balanceTokenUser: args.sender === prev.user ? prev.balanceTokenUser.sub(args.balanceTokenSender) : prev.balanceTokenUser, + txCountGlobal: prev.txCountGlobal + 1, + threadRoot: this.utils.generateThreadRootHash(initialThreadStates), + threadCount: initialThreadStates.length, + timeout: 0, + }) + } + + // TODO: should the args be a signed thread state or unsigned thread state? + public closeThread(prev: ChannelStateBN, initialThreadStates: UnsignedThreadState[], args: UnsignedThreadStateBN): UnsignedChannelState { + initialThreadStates = initialThreadStates.filter(state => state.sender !== args.sender && state.receiver !== args.receiver) + const userIsSender = args.sender === prev.user + return convertChannelState("str-unsigned", { + ...prev, + balanceWeiHub: userIsSender + ? prev.balanceWeiHub.add(args.balanceWeiReceiver) + : prev.balanceWeiHub.add(args.balanceWeiSender), + balanceWeiUser: userIsSender + ? prev.balanceWeiUser.add(args.balanceWeiSender) + : prev.balanceWeiUser.add(args.balanceWeiReceiver), + balanceTokenHub: userIsSender + ? prev.balanceTokenHub.add(args.balanceTokenReceiver) + : prev.balanceTokenHub.add(args.balanceTokenSender), + balanceTokenUser: userIsSender + ? prev.balanceTokenUser.add(args.balanceTokenSender) + : prev.balanceTokenUser.add(args.balanceTokenReceiver), + txCountGlobal: prev.txCountGlobal + 1, + threadRoot: this.utils.generateThreadRootHash(initialThreadStates), + threadCount: initialThreadStates.length, + timeout: 0, + }) + } + + public threadPayment(prev: ThreadStateBN, args: PaymentBN): UnsignedThreadState { + return convertThreadState("str-unsigned", { + ...prev, + balanceTokenSender: prev.balanceTokenSender.sub(args.amountToken), + balanceTokenReceiver: prev.balanceTokenReceiver.add(args.amountToken), + balanceWeiSender: prev.balanceTokenSender.sub(args.amountWei), + balanceWeiReceiver: prev.balanceTokenReceiver.add(args.amountWei), + txCount: prev.txCount + 1, + }) + } + + public invalidation(latestValidState: ChannelStateBN, args: InvalidationArgs): UnsignedChannelState { + return convertChannelState("str-unsigned", { + ...latestValidState, + timeout: 0, + txCountGlobal: args.lastInvalidTxCount + 1, + }) + } +} diff --git a/modules/client/src/Utils.test.ts b/modules/client/src/Utils.test.ts new file mode 100644 index 0000000000..a03798dae5 --- /dev/null +++ b/modules/client/src/Utils.test.ts @@ -0,0 +1,87 @@ +require('dotenv').config() +const Web3 = require('web3') +const HttpProvider = require(`ethjs-provider-http`) + +import { expect } from 'chai' +import { Utils } from './Utils' +import { MerkleUtils } from './helpers/merkleUtils' +// import { MerkleTree } from './helpers/merkleTree' +import MerkleTree from './helpers/merkleTree' +import * as t from './testing/index' +import { assert } from './testing' + +const utils = new Utils() +describe('Utils', () => { + let web3: any + let accounts: string[] + let partyA: string + before('instantiate web3', async function () { + // instantiate web3 + web3 = new Web3(new HttpProvider('http://localhost:8545')) + try { + accounts = await web3.eth.getAccounts() + } catch (e) { + console.log('error fetching web3 accounts:', '' + e) + console.warn(`No web3 HTTP provider found at 'localhost:8545'; skipping tests which require web3`) + this.skip() + return + } + partyA = accounts[1] + }) + + it('should recover the signer from the channel update when there are no threads', async () => { + // create and sign channel state update + const channelStateFingerprint = t.getChannelState('full', { + balanceWei: [100, 200], + }) + // generate hash + const hash = utils.createChannelStateHash(channelStateFingerprint) + // sign + const sig = await web3.eth.sign(hash, partyA) + console.log(hash) // log harcode hash for other hash test + // recover signer + const signer = utils.recoverSignerFromChannelState( + channelStateFingerprint, + sig, + ) + expect(signer).to.equal(partyA.toLowerCase()) + }) + + it('should recover the signer from the thread state update', async () => { + // create and sign channel state update + const threadStateFingerprint = t.getThreadState('full', { + balanceWei: [100, 200], + }) + // generate hash + const hash = utils.createThreadStateHash(threadStateFingerprint) + // sign + const sig = await web3.eth.sign(hash, partyA) + console.log(hash) // log harcode hash for other hash test + // recover signer + const signer = utils.recoverSignerFromThreadState( + threadStateFingerprint, + sig, + ) + expect(signer).to.equal(partyA.toLowerCase()) + }) + + it('should return the correct root hash', async () => { + const threadStateFingerprint = t.getThreadState('empty', { + balanceWei: [100, 0], + }) + // TO DO: merkle tree class imports not working...? + // generate hash + const hash = utils.createThreadStateHash(threadStateFingerprint) + // construct elements + const elements = [ + MerkleUtils.hexToBuffer(hash), + MerkleUtils.hexToBuffer(utils.emptyRootHash), + ] + const merkle = new MerkleTree(elements) + const expectedRoot = MerkleUtils.bufferToHex(merkle.getRoot()) + const generatedRootHash = utils.generateThreadRootHash([ + threadStateFingerprint, + ]) + expect(generatedRootHash).to.equal(expectedRoot) + }) +}) diff --git a/modules/client/src/Utils.ts b/modules/client/src/Utils.ts new file mode 100644 index 0000000000..93e7d66bf1 --- /dev/null +++ b/modules/client/src/Utils.ts @@ -0,0 +1,298 @@ +import { convertChannelState } from './types' +/********************************* + *********** UTIL FNS ************ + *********************************/ +import util = require('ethereumjs-util') +import { MerkleUtils } from './helpers/merkleUtils' +import MerkleTree from './helpers/merkleTree' +import Web3 = require('web3') + +import { + UnsignedChannelState, + UnsignedThreadState, + ChannelState, + SignedDepositRequestProposal, + Payment, +} from './types' + +// import types from connext + +export const emptyAddress = '0x0000000000000000000000000000000000000000' +export const emptyRootHash = '0x0000000000000000000000000000000000000000000000000000000000000000' + +// define the utils functions +export class Utils { + emptyAddress = '0x0000000000000000000000000000000000000000' + emptyRootHash = '0x0000000000000000000000000000000000000000000000000000000000000000' + + public createDepositRequestProposalHash( + req: Payment, + ): string { + const { amountToken, amountWei } = req + // @ts-ignore + const hash = Web3.utils.soliditySha3( + { type: 'uint256', value: amountToken }, + { type: 'uint256', value: amountWei }, + ) + return hash + } + + public recoverSignerFromDepositRequest( + args: SignedDepositRequestProposal, + ): string { + const hash = this.createDepositRequestProposalHash(args) + return this.recoverSigner(hash, args.sigUser) + } + + public createChannelStateHash( + channelState: UnsignedChannelState, + ): string { + const { + contractAddress, + user, + recipient, + balanceWeiHub, + balanceWeiUser, + balanceTokenHub, + balanceTokenUser, + pendingDepositWeiHub, + pendingDepositWeiUser, + pendingDepositTokenHub, + pendingDepositTokenUser, + pendingWithdrawalWeiHub, + pendingWithdrawalWeiUser, + pendingWithdrawalTokenHub, + pendingWithdrawalTokenUser, + txCountGlobal, + txCountChain, + threadRoot, + threadCount, + timeout, + } = channelState + + // hash data + // @ts-ignore + const hash = Web3.utils.soliditySha3( + { type: 'address', value: contractAddress }, + // @ts-ignore TODO wtf??! + { type: 'address[2]', value: [user, recipient] }, + { + type: 'uint256[2]', + value: [balanceWeiHub, balanceWeiUser], + }, + { + type: 'uint256[2]', + value: [balanceTokenHub, balanceTokenUser], + }, + { + type: 'uint256[4]', + value: [ + pendingDepositWeiHub, + pendingWithdrawalWeiHub, + pendingDepositWeiUser, + pendingWithdrawalWeiUser, + ], + }, + { + type: 'uint256[4]', + value: [ + pendingDepositTokenHub, + pendingWithdrawalTokenHub, + pendingDepositTokenUser, + pendingWithdrawalTokenUser, + ], + }, + { + type: 'uint256[2]', + value: [txCountGlobal, txCountChain], + }, + { type: 'bytes32', value: threadRoot }, + { type: 'uint256', value: threadCount }, + { type: 'uint256', value: timeout }, + ) + return hash + } + + public recoverSignerFromChannelState( + channelState: UnsignedChannelState, + // could be hub or user + sig: string, + ): string { + const hash: any = this.createChannelStateHash(channelState) + return this.recoverSigner(hash, sig) + } + + public createThreadStateHash(threadState: UnsignedThreadState): string { + const { + contractAddress, + sender, + receiver, + threadId, + balanceWeiSender, + balanceWeiReceiver, + balanceTokenSender, + balanceTokenReceiver, + txCount, + } = threadState + // convert ChannelState to UnsignedChannelState + // @ts-ignore + const hash = Web3.utils.soliditySha3( + { type: 'address', value: contractAddress }, + { type: 'address', value: sender }, + { type: 'address', value: receiver }, + // @ts-ignore TODO wtf??! + { type: 'uint256', value: threadId }, + { + type: 'uint256', + value: [balanceWeiSender, balanceWeiReceiver], + }, + { + type: 'uint256', + value: [balanceTokenSender, balanceTokenReceiver], + }, + { type: 'uint256', value: txCount }, + ) + return hash + } + + public recoverSignerFromThreadState( + threadState: UnsignedThreadState, + sig: string, + ): string { + console.log('recovering signer from state:', threadState) + const hash: any = this.createThreadStateHash(threadState) + return this.recoverSigner(hash, sig) + } + + public generateThreadMerkleTree( + threadInitialStates: UnsignedThreadState[], + ): any { + // TO DO: should this just return emptyRootHash? + if (threadInitialStates.length === 0) { + throw new Error('Cannot create a Merkle tree with 0 leaves.') + } + let merkle + let elems = threadInitialStates.map(threadInitialState => { + // hash each initial state and convert hash to buffer + const hash = this.createThreadStateHash(threadInitialState) + const buf = MerkleUtils.hexToBuffer(hash) + return buf + }) + if (elems.length % 2 !== 0) { + // cant have odd number of leaves + elems.push(MerkleUtils.hexToBuffer(this.emptyRootHash)) + } + merkle = new MerkleTree(elems) + + return merkle + } + + public generateThreadRootHash( + threadInitialStates: UnsignedThreadState[], + ): string { + let threadRootHash + if (threadInitialStates.length === 0) { + // reset to initial value -- no open VCs + threadRootHash = this.emptyRootHash + } else { + const merkle = this.generateThreadMerkleTree(threadInitialStates) + threadRootHash = MerkleUtils.bufferToHex(merkle.getRoot()) + } + + return threadRootHash + } + + public generateThreadProof( + thread: UnsignedThreadState, + threads: UnsignedThreadState[], + ): any { + // generate hash + const hash = this.createThreadStateHash(thread) + // generate merkle tree + let merkle = this.generateThreadMerkleTree(threads) + let mproof = merkle.proof(MerkleUtils.hexToBuffer(hash)) + + let proof = [] + for (var i = 0; i < mproof.length; i++) { + proof.push(MerkleUtils.bufferToHex(mproof[i])) + } + + proof.unshift(hash) + + proof = MerkleUtils.marshallState(proof) + return proof + } + + public threadIsContained( + threadHash: string, + // TO DO: can we not pass in the thread array? + threads: UnsignedThreadState[], + threadMerkleRoot: string, + proof: any, + ) { + // TO DO: implement without rebuilding the thread tree? + // otherwise you will have to pass in threads to each one + // solidity code to satisfy: + // function _isContained(bytes32 _hash, bytes _proof, bytes32 _root) internal pure returns (bool) { + // bytes32 cursor = _hash; + // bytes32 proofElem; + // for (uint256 i = 64; i <= _proof.length; i += 32) { + // assembly { proofElem := mload(add(_proof, i)) } + // if (cursor < proofElem) { + // cursor = keccak256(abi.encodePacked(cursor, proofElem)); + // } else { + // cursor = keccak256(abi.encodePacked(proofElem, cursor)); + // } + // } + // return cursor == _root; + // } + // generate merkle tree + const mtree = this.generateThreadMerkleTree(threads) + if (mtree.getRoot() !== threadMerkleRoot) { + throw new Error(`Incorrect root provided`) + } + return mtree.verify(proof, threadHash) + } + + private recoverSigner(hash: string, sig: string) { + // let fingerprint: any = this.createChannelStateHash(channelState) + let fingerprint = util.toBuffer(String(hash)) + const prefix = util.toBuffer('\x19Ethereum Signed Message:\n') + // @ts-ignore + const prefixedMsg = util.keccak256( + // @ts-ignore + Buffer.concat([ + // @ts-ignore + prefix, + // @ts-ignore + util.toBuffer(String(fingerprint.length)), + // @ts-ignore + fingerprint, + ]), + ) + const res = util.fromRpcSig(sig) + const pubKey = util.ecrecover( + util.toBuffer(prefixedMsg), + res.v, + res.r, + res.s, + ) + const addrBuf = util.pubToAddress(pubKey) + const addr = util.bufferToHex(addrBuf) + console.log('recovered:', addr) + + return addr + } + + hasPendingOps(stateAny: ChannelState) { + const state = convertChannelState('str', stateAny) + for (let field in state) { + if (!field.startsWith('pending')) + continue + if ((state as any)[field] !== '0') + return true + } + return false + } + +} diff --git a/modules/client/src/contract/ChannelManager.d.ts b/modules/client/src/contract/ChannelManager.d.ts new file mode 100644 index 0000000000..61fc63bd1a --- /dev/null +++ b/modules/client/src/contract/ChannelManager.d.ts @@ -0,0 +1,267 @@ +/* Generated by ts-generator ver. 0.0.8 */ +/* tslint:disable */ + +import Contract, { CustomOptions, contractOptions } from "web3/eth/contract"; +import { TransactionObject, BlockType } from "web3/eth/types"; +import { Callback, EventLog } from "web3/types"; +import { EventEmitter } from "events"; +import { Provider } from "web3/providers"; + +export class ChannelManager { + constructor(jsonInterface: any[], address?: string, options?: CustomOptions); + _address: string; + options: contractOptions; + methods: { + channels( + arg0: string + ): TransactionObject<{ + 0: string; + 1: string; + 2: string; + 3: string; + 4: string; + }>; + + getChannelBalances( + user: string + ): TransactionObject<{ + 0: string; + 1: string; + 2: string; + 3: string; + 4: string; + 5: string; + }>; + + getChannelDetails( + user: string + ): TransactionObject<{ + 0: string; + 1: string; + 2: string; + 3: string; + 4: string; + 5: string; + 6: string; + }>; + + hubContractWithdraw( + weiAmount: number | string, + tokenAmount: number | string + ): TransactionObject; + + hubAuthorizedUpdate( + user: string, + recipient: string, + weiBalances: (number | string)[], + tokenBalances: (number | string)[], + pendingWeiUpdates: (number | string)[], + pendingTokenUpdates: (number | string)[], + txCount: (number | string)[], + threadRoot: string | number[], + threadCount: number | string, + timeout: number | string, + sigUser: string + ): TransactionObject; + + userAuthorizedUpdate( + recipient: string, + weiBalances: (number | string)[], + tokenBalances: (number | string)[], + pendingWeiUpdates: (number | string)[], + pendingTokenUpdates: (number | string)[], + txCount: (number | string)[], + threadRoot: string | number[], + threadCount: number | string, + timeout: number | string, + sigHub: string + ): TransactionObject; + + startExit(user: string): TransactionObject; + + startExitWithUpdate( + user: (string)[], + weiBalances: (number | string)[], + tokenBalances: (number | string)[], + pendingWeiUpdates: (number | string)[], + pendingTokenUpdates: (number | string)[], + txCount: (number | string)[], + threadRoot: string | number[], + threadCount: number | string, + timeout: number | string, + sigHub: string, + sigUser: string + ): TransactionObject; + + emptyChannelWithChallenge( + user: (string)[], + weiBalances: (number | string)[], + tokenBalances: (number | string)[], + pendingWeiUpdates: (number | string)[], + pendingTokenUpdates: (number | string)[], + txCount: (number | string)[], + threadRoot: string | number[], + threadCount: number | string, + timeout: number | string, + sigHub: string, + sigUser: string + ): TransactionObject; + + emptyChannel(user: string): TransactionObject; + + startExitThread( + user: string, + sender: string, + receiver: string, + threadId: number | string, + weiBalances: (number | string)[], + tokenBalances: (number | string)[], + proof: (string | number[])[], + sig: string + ): TransactionObject; + + startExitThreadWithUpdate( + user: string, + threadMembers: (string)[], + threadId: number | string, + weiBalances: (number | string)[], + tokenBalances: (number | string)[], + proof: (string | number[])[], + sig: string, + updatedWeiBalances: (number | string)[], + updatedTokenBalances: (number | string)[], + updatedTxCount: number | string, + updateSig: string + ): TransactionObject; + + challengeThread( + sender: string, + receiver: string, + threadId: number | string, + weiBalances: (number | string)[], + tokenBalances: (number | string)[], + txCount: number | string, + sig: string + ): TransactionObject; + + emptyThread( + user: string, + sender: string, + receiver: string, + threadId: number | string, + weiBalances: (number | string)[], + tokenBalances: (number | string)[], + proof: (string | number[])[], + sig: string + ): TransactionObject; + + nukeThreads(user: string): TransactionObject; + + totalChannelWei(): TransactionObject; + totalChannelToken(): TransactionObject; + hub(): TransactionObject; + NAME(): TransactionObject; + approvedToken(): TransactionObject; + challengePeriod(): TransactionObject; + VERSION(): TransactionObject; + getHubReserveWei(): TransactionObject; + getHubReserveTokens(): TransactionObject; + }; + deploy(options: { + data: string; + arguments: any[]; + }): TransactionObject; + events: { + DidHubContractWithdraw( + options?: { + filter?: object; + fromBlock?: BlockType; + topics?: string[]; + }, + cb?: Callback + ): EventEmitter; + + DidUpdateChannel( + options?: { + filter?: object; + fromBlock?: BlockType; + topics?: string[]; + }, + cb?: Callback + ): EventEmitter; + + DidStartExitChannel( + options?: { + filter?: object; + fromBlock?: BlockType; + topics?: string[]; + }, + cb?: Callback + ): EventEmitter; + + DidEmptyChannel( + options?: { + filter?: object; + fromBlock?: BlockType; + topics?: string[]; + }, + cb?: Callback + ): EventEmitter; + + DidStartExitThread( + options?: { + filter?: object; + fromBlock?: BlockType; + topics?: string[]; + }, + cb?: Callback + ): EventEmitter; + + DidChallengeThread( + options?: { + filter?: object; + fromBlock?: BlockType; + topics?: string[]; + }, + cb?: Callback + ): EventEmitter; + + DidEmptyThread( + options?: { + filter?: object; + fromBlock?: BlockType; + topics?: string[]; + }, + cb?: Callback + ): EventEmitter; + + DidNukeThreads( + options?: { + filter?: object; + fromBlock?: BlockType; + topics?: string[]; + }, + cb?: Callback + ): EventEmitter; + + allEvents: ( + options?: { + filter?: object; + fromBlock?: BlockType; + topics?: string[]; + }, + cb?: Callback + ) => EventEmitter; + }; + getPastEvents( + event: string, + options?: { + filter?: object; + fromBlock?: BlockType; + toBlock?: BlockType; + topics?: string[]; + }, + cb?: Callback + ): Promise; + setProvider(provider: Provider): void; +} diff --git a/modules/client/src/contract/ChannelManagerAbi.ts b/modules/client/src/contract/ChannelManagerAbi.ts new file mode 100644 index 0000000000..292ff7e975 --- /dev/null +++ b/modules/client/src/contract/ChannelManagerAbi.ts @@ -0,0 +1,1071 @@ +export default { + "contractName": "ChannelManager", + "abi": [ + { + "constant": true, + "inputs": [], + "name": "totalChannelWei", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalChannelToken", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "hub", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + } + ], + "name": "channels", + "outputs": [ + { + "name": "threadRoot", + "type": "bytes32" + }, + { + "name": "threadCount", + "type": "uint256" + }, + { + "name": "exitInitiator", + "type": "address" + }, + { + "name": "channelClosingTime", + "type": "uint256" + }, + { + "name": "status", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "NAME", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "approvedToken", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "challengePeriod", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "VERSION", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "name": "_hub", + "type": "address" + }, + { + "name": "_challengePeriod", + "type": "uint256" + }, + { + "name": "_tokenAddress", + "type": "address" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "payable": true, + "stateMutability": "payable", + "type": "fallback" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "weiAmount", + "type": "uint256" + }, + { + "indexed": false, + "name": "tokenAmount", + "type": "uint256" + } + ], + "name": "DidHubContractWithdraw", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "user", + "type": "address" + }, + { + "indexed": false, + "name": "senderIdx", + "type": "uint256" + }, + { + "indexed": false, + "name": "weiBalances", + "type": "uint256[2]" + }, + { + "indexed": false, + "name": "tokenBalances", + "type": "uint256[2]" + }, + { + "indexed": false, + "name": "pendingWeiUpdates", + "type": "uint256[4]" + }, + { + "indexed": false, + "name": "pendingTokenUpdates", + "type": "uint256[4]" + }, + { + "indexed": false, + "name": "txCount", + "type": "uint256[2]" + }, + { + "indexed": false, + "name": "threadRoot", + "type": "bytes32" + }, + { + "indexed": false, + "name": "threadCount", + "type": "uint256" + } + ], + "name": "DidUpdateChannel", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "user", + "type": "address" + }, + { + "indexed": false, + "name": "senderIdx", + "type": "uint256" + }, + { + "indexed": false, + "name": "weiBalances", + "type": "uint256[2]" + }, + { + "indexed": false, + "name": "tokenBalances", + "type": "uint256[2]" + }, + { + "indexed": false, + "name": "txCount", + "type": "uint256[2]" + }, + { + "indexed": false, + "name": "threadRoot", + "type": "bytes32" + }, + { + "indexed": false, + "name": "threadCount", + "type": "uint256" + } + ], + "name": "DidStartExitChannel", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "user", + "type": "address" + }, + { + "indexed": false, + "name": "senderIdx", + "type": "uint256" + }, + { + "indexed": false, + "name": "weiBalances", + "type": "uint256[2]" + }, + { + "indexed": false, + "name": "tokenBalances", + "type": "uint256[2]" + }, + { + "indexed": false, + "name": "txCount", + "type": "uint256[2]" + }, + { + "indexed": false, + "name": "threadRoot", + "type": "bytes32" + }, + { + "indexed": false, + "name": "threadCount", + "type": "uint256" + } + ], + "name": "DidEmptyChannel", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "user", + "type": "address" + }, + { + "indexed": true, + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "name": "receiver", + "type": "address" + }, + { + "indexed": false, + "name": "threadId", + "type": "uint256" + }, + { + "indexed": false, + "name": "senderAddress", + "type": "address" + }, + { + "indexed": false, + "name": "weiBalances", + "type": "uint256[2]" + }, + { + "indexed": false, + "name": "tokenBalances", + "type": "uint256[2]" + }, + { + "indexed": false, + "name": "txCount", + "type": "uint256" + } + ], + "name": "DidStartExitThread", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "name": "receiver", + "type": "address" + }, + { + "indexed": false, + "name": "threadId", + "type": "uint256" + }, + { + "indexed": false, + "name": "senderAddress", + "type": "address" + }, + { + "indexed": false, + "name": "weiBalances", + "type": "uint256[2]" + }, + { + "indexed": false, + "name": "tokenBalances", + "type": "uint256[2]" + }, + { + "indexed": false, + "name": "txCount", + "type": "uint256" + } + ], + "name": "DidChallengeThread", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "user", + "type": "address" + }, + { + "indexed": true, + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "name": "receiver", + "type": "address" + }, + { + "indexed": false, + "name": "threadId", + "type": "uint256" + }, + { + "indexed": false, + "name": "senderAddress", + "type": "address" + }, + { + "indexed": false, + "name": "channelWeiBalances", + "type": "uint256[2]" + }, + { + "indexed": false, + "name": "channelTokenBalances", + "type": "uint256[2]" + }, + { + "indexed": false, + "name": "channelTxCount", + "type": "uint256[2]" + }, + { + "indexed": false, + "name": "channelThreadRoot", + "type": "bytes32" + }, + { + "indexed": false, + "name": "channelThreadCount", + "type": "uint256" + } + ], + "name": "DidEmptyThread", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "user", + "type": "address" + }, + { + "indexed": false, + "name": "senderAddress", + "type": "address" + }, + { + "indexed": false, + "name": "weiAmount", + "type": "uint256" + }, + { + "indexed": false, + "name": "tokenAmount", + "type": "uint256" + }, + { + "indexed": false, + "name": "channelWeiBalances", + "type": "uint256[2]" + }, + { + "indexed": false, + "name": "channelTokenBalances", + "type": "uint256[2]" + }, + { + "indexed": false, + "name": "channelTxCount", + "type": "uint256[2]" + }, + { + "indexed": false, + "name": "channelThreadRoot", + "type": "bytes32" + }, + { + "indexed": false, + "name": "channelThreadCount", + "type": "uint256" + } + ], + "name": "DidNukeThreads", + "type": "event" + }, + { + "constant": false, + "inputs": [ + { + "name": "weiAmount", + "type": "uint256" + }, + { + "name": "tokenAmount", + "type": "uint256" + } + ], + "name": "hubContractWithdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getHubReserveWei", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getHubReserveTokens", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "user", + "type": "address" + }, + { + "name": "recipient", + "type": "address" + }, + { + "name": "weiBalances", + "type": "uint256[2]" + }, + { + "name": "tokenBalances", + "type": "uint256[2]" + }, + { + "name": "pendingWeiUpdates", + "type": "uint256[4]" + }, + { + "name": "pendingTokenUpdates", + "type": "uint256[4]" + }, + { + "name": "txCount", + "type": "uint256[2]" + }, + { + "name": "threadRoot", + "type": "bytes32" + }, + { + "name": "threadCount", + "type": "uint256" + }, + { + "name": "timeout", + "type": "uint256" + }, + { + "name": "sigUser", + "type": "string" + } + ], + "name": "hubAuthorizedUpdate", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "recipient", + "type": "address" + }, + { + "name": "weiBalances", + "type": "uint256[2]" + }, + { + "name": "tokenBalances", + "type": "uint256[2]" + }, + { + "name": "pendingWeiUpdates", + "type": "uint256[4]" + }, + { + "name": "pendingTokenUpdates", + "type": "uint256[4]" + }, + { + "name": "txCount", + "type": "uint256[2]" + }, + { + "name": "threadRoot", + "type": "bytes32" + }, + { + "name": "threadCount", + "type": "uint256" + }, + { + "name": "timeout", + "type": "uint256" + }, + { + "name": "sigHub", + "type": "string" + } + ], + "name": "userAuthorizedUpdate", + "outputs": [], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "user", + "type": "address" + } + ], + "name": "startExit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "user", + "type": "address[2]" + }, + { + "name": "weiBalances", + "type": "uint256[2]" + }, + { + "name": "tokenBalances", + "type": "uint256[2]" + }, + { + "name": "pendingWeiUpdates", + "type": "uint256[4]" + }, + { + "name": "pendingTokenUpdates", + "type": "uint256[4]" + }, + { + "name": "txCount", + "type": "uint256[2]" + }, + { + "name": "threadRoot", + "type": "bytes32" + }, + { + "name": "threadCount", + "type": "uint256" + }, + { + "name": "timeout", + "type": "uint256" + }, + { + "name": "sigHub", + "type": "string" + }, + { + "name": "sigUser", + "type": "string" + } + ], + "name": "startExitWithUpdate", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "user", + "type": "address[2]" + }, + { + "name": "weiBalances", + "type": "uint256[2]" + }, + { + "name": "tokenBalances", + "type": "uint256[2]" + }, + { + "name": "pendingWeiUpdates", + "type": "uint256[4]" + }, + { + "name": "pendingTokenUpdates", + "type": "uint256[4]" + }, + { + "name": "txCount", + "type": "uint256[2]" + }, + { + "name": "threadRoot", + "type": "bytes32" + }, + { + "name": "threadCount", + "type": "uint256" + }, + { + "name": "timeout", + "type": "uint256" + }, + { + "name": "sigHub", + "type": "string" + }, + { + "name": "sigUser", + "type": "string" + } + ], + "name": "emptyChannelWithChallenge", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "user", + "type": "address" + } + ], + "name": "emptyChannel", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "user", + "type": "address" + }, + { + "name": "sender", + "type": "address" + }, + { + "name": "receiver", + "type": "address" + }, + { + "name": "threadId", + "type": "uint256" + }, + { + "name": "weiBalances", + "type": "uint256[2]" + }, + { + "name": "tokenBalances", + "type": "uint256[2]" + }, + { + "name": "proof", + "type": "bytes" + }, + { + "name": "sig", + "type": "string" + } + ], + "name": "startExitThread", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "user", + "type": "address" + }, + { + "name": "threadMembers", + "type": "address[2]" + }, + { + "name": "threadId", + "type": "uint256" + }, + { + "name": "weiBalances", + "type": "uint256[2]" + }, + { + "name": "tokenBalances", + "type": "uint256[2]" + }, + { + "name": "proof", + "type": "bytes" + }, + { + "name": "sig", + "type": "string" + }, + { + "name": "updatedWeiBalances", + "type": "uint256[2]" + }, + { + "name": "updatedTokenBalances", + "type": "uint256[2]" + }, + { + "name": "updatedTxCount", + "type": "uint256" + }, + { + "name": "updateSig", + "type": "string" + } + ], + "name": "startExitThreadWithUpdate", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "sender", + "type": "address" + }, + { + "name": "receiver", + "type": "address" + }, + { + "name": "threadId", + "type": "uint256" + }, + { + "name": "weiBalances", + "type": "uint256[2]" + }, + { + "name": "tokenBalances", + "type": "uint256[2]" + }, + { + "name": "txCount", + "type": "uint256" + }, + { + "name": "sig", + "type": "string" + } + ], + "name": "challengeThread", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "user", + "type": "address" + }, + { + "name": "sender", + "type": "address" + }, + { + "name": "receiver", + "type": "address" + }, + { + "name": "threadId", + "type": "uint256" + }, + { + "name": "weiBalances", + "type": "uint256[2]" + }, + { + "name": "tokenBalances", + "type": "uint256[2]" + }, + { + "name": "proof", + "type": "bytes" + }, + { + "name": "sig", + "type": "string" + } + ], + "name": "emptyThread", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "user", + "type": "address" + } + ], + "name": "nukeThreads", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "user", + "type": "address" + } + ], + "name": "getChannelBalances", + "outputs": [ + { + "name": "weiHub", + "type": "uint256" + }, + { + "name": "weiUser", + "type": "uint256" + }, + { + "name": "weiTotal", + "type": "uint256" + }, + { + "name": "tokenHub", + "type": "uint256" + }, + { + "name": "tokenUser", + "type": "uint256" + }, + { + "name": "tokenTotal", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "user", + "type": "address" + } + ], + "name": "getChannelDetails", + "outputs": [ + { + "name": "txCountGlobal", + "type": "uint256" + }, + { + "name": "txCountChain", + "type": "uint256" + }, + { + "name": "threadRoot", + "type": "bytes32" + }, + { + "name": "threadCount", + "type": "uint256" + }, + { + "name": "exitInitiator", + "type": "address" + }, + { + "name": "channelClosingTime", + "type": "uint256" + }, + { + "name": "status", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + } + ], +} diff --git a/modules/client/src/contract/README.txt b/modules/client/src/contract/README.txt new file mode 100644 index 0000000000..75bf352f3f --- /dev/null +++ b/modules/client/src/contract/README.txt @@ -0,0 +1,4 @@ +To recreate this: +1. Build the contract with `npm run build` +2. Copy `build/ts/ChannelManager.d.ts` here +3. Copy `build/contracts/ChannelManager.json` here, rename it to ChannelManagerAbi.ts, add `export default` on the first line, remove the ABI diff --git a/modules/client/src/controllers/.DepositController.ts.swo b/modules/client/src/controllers/.DepositController.ts.swo new file mode 100644 index 0000000000000000000000000000000000000000..0ea9cfc8bf6c0ae9bcb21be13475165dc0caa76b GIT binary patch literal 16384 zcmeHOO^h5z6>f(l{5p_u0wPhnM46e*xW_+;z(z(gwn-etvE^N3gdH(8-8DPy?dcx6 zs&{r+Z)}BNfsjxlB^+Xqf&_|kKwOYGaEd{2;e=!<61l;JAORPGgoMQRs$T!>kAEZ( zAk?FL*_rODSFgT$^{VRY_F(@PPMj2<_78aYe8}_mJahkrkAC+#@7ZgfH%();rZT#7 z%g@n~Otl}UfsAii?pZe2TbjsZ6}6(Im#Imrdx0G5$f#ZrM=CKrodvxhO-z=?vC4Gs z7!``l(ejdS^etOzZZ-@w4D4iJuXpI+f&Fy-6Z3Q8!w;O@S){qrFwij2Fwij2Fwij2 zFwij2FwijYf6IWG+~r+H@9yFrp5woJw*7q<|F${-zJGDseg4_}X&7i2Xc%Z1Xc%Z1 zXc%Z1Xc%Z1Xc%Z1Xc%Z1_#b3IhMxB%{JKsDemws#&i^+)=y|KaLEvxqdEOhq%fL&( zi@-O4BfvX*J@41R*MUcYKYhUSUIh#=0lL87-|u<90)7d+07Sqc;2`k!`#kRzAOTJS zp922;UeEhBa22=$Tn5eqUji0@j{*O9kLUdsxBa6hmI_|4s(_haC7;D^8u03Wyq_&!9DYrreO z6<`tgByb4$2yg?U%2nVqz+b@cTLAI=266pw`w06nqb!{Yeo^9hmZe$Sb9Z_@q0_ON zj#Q@n>Y5p;M5qXVG7%Xul3K_(Q!<>1fx<0~7cqoMML@4anD?09GMF>tTeb}2xYJDVMLVwfj^ ziPB_i455uh;cSvm0=A+x8da%hNg%$A!4GLTs?Kr;Q6l4bX1gfxal@+a*x17lqsnbsgvS@#$_@FZ2&4%{0>5i*-o}8q!&Y?L zH6nzZO3VxE*=>fZ7#vsAWR8kSK8T}$2m3!V*5H1jN3<^p%x=MPPw7O{G!|(RsQO$N zIu8P+^$UU=p?lA)OUVyrC=s|; z%RJ~jdpuf0hLqV#K1P<*f(%HP&SaUq3Tt9IiZIesC8jpLq}DnN^%fQ=mvCqoLw4IR zLf~$I7v07GE^_T!df@PGw~HbjarmV6;$ONN=!5GkJDKT*~x}<}$t1!`6 zfo7}~1}ln|Lq!ARy5SP6iH+t#i&X~r{A2$Wb}Kbq!=soyLXr6MM5C*corxJ`YB>ywLE<$T21 zs9o#DE}4q0Ze2?XmxbtaZrmknbIs1?g@kkEE`>OVYKZ?AZ8Zd6l zU_@wXVcQh3u^YSygJWp<vPkt(sVP>3OX8K>IN z+(j59k(s$0WsvGon#W<%Le#`Dwm66`w^Xv1h(83K`@h6D=Wd_tef5%}#*{ ziz%tIzH7Yw$fkZhPoLO0M5(TTMnop2r z?66U$aB~yAoXT0JYnKo^)MJG`W$3Kg=?j`DZxnk~yu3`YNMSZYymYa}H6mI`kS56X zSfmtTQ`$GQBZb^RZsC3B7#`5>8jWC+8%L{3%wzF9ezOEM-z}B{np9Y>$U0fuCr-Ft z1QKne!(uLWZP)tbb45$vkwD{t!V)W8)jLky)@ip9$J9bEQgjGrIYCl`=UlF~Yn=;r zY=zy@iF{^5h0=7na=NuE;&UJYSUE5Ij-UGJHmv@kC%-M=KOnilae~ zI%99lKUUS+B}mI^y`4AfRky7yMRP4~Zrklv)h18et_acu@7Eg+`||hJP4m`YJGajD zb`9;N%a*Ez%@I-aj2FC?N9X_Nac&NArl#|M@qGU$IQPE@d=vN@@EmX$@PYe)y}%!F z{=WvC2JQuZf%Cs%9pD|D``-et0qX#10T+P?I0+mB?goAi4d7=$3=9DUoCh8Reg`e! zN5J=htH1`Jfz!ZV;BEN)Ch$|>HDCf<1cm_hCjgpH!$8A8!$8A8!$8A8!@z%o0aAE& z5isxQ_-L}cI6dA>rR43S(}i0t@rbyE$jFM|x^KT6PmIb=I-RT(4Y4bQ$YIuT?4z@j zkKa~=!C!lx^Klfb3BGPe+P2&BLcy>q@(|#9|gv zAa>FE3Zc+%x&ApJ)uWzO;`NRvHGLk!;)rzp)i70BkU%rGGBF>WaM%o;L^;Q*eWe}~ zV?N(qPFGcOmRy|UF)R&mOTHYF;AIP1@1Pae44hNZom59Lap{t+PqL5w;&|S$=5RDA z2LGl?S6eHbL({QSQX@QgN^^)YotjqlIC$Wm6%66*i6bwLv=VhQ9OfsfeQr?R->y<| zzqdabb8-x6F9!~?D&zzw7_v`XAUB=BqamvS(bEwZpG+C_9IKp`?ApV6;VM72N283_)~JXO{bCviNiGLi&v9OUVu`{&Y?Gtgt8NV`f? z)q=ucg;<;e!IKTy!Z+~jNF!Axtt?>v=!m2#1oBuy&9LeTLLW~7#w3Pdq$aUiXMu#o zTj;7fO{A+-SwcWSH>Ss3UHEO?wSCYK>p>)`*Q`3hH+IVxCm=bK-Ukh#GIi290umq$ z#CWzv5`yjP#6Gcd>NSK4(o}4ZIUuMPrEzPI!gS82kFt3to@F$F&o0mOj7H?RDNQD7 zKV7`r3kdcdB1BD6E*`Pi { + +}) \ No newline at end of file diff --git a/modules/client/src/controllers/AbstractController.ts b/modules/client/src/controllers/AbstractController.ts new file mode 100644 index 0000000000..69596fef34 --- /dev/null +++ b/modules/client/src/controllers/AbstractController.ts @@ -0,0 +1,35 @@ +import Logger from '../lib/Logger' +import { ConnextInternal, IHubAPIClient } from "../Connext"; +import { ConnextState, ConnextStore } from "../state/store"; +import { Validator } from '../validator'; + +export abstract class AbstractController { + name: string + connext: ConnextInternal + logger: Logger + hub: IHubAPIClient + validator: Validator + + constructor(name: string, connext: ConnextInternal) { + this.connext = connext + this.name = name + this.logger = connext.getLogger(this.name) + this.hub = connext.hub + this.validator = connext.validator + } + + get store(): ConnextStore { + return this.connext.store + } + + getState(): ConnextState { + return this.connext.store.getState() + } + + async start(): Promise { } + async stop(): Promise { } + + protected logToApi(key: string, data: any) { + this.logger.logToApi(key, data) + } +} diff --git a/modules/client/src/controllers/BuyController.test.ts b/modules/client/src/controllers/BuyController.test.ts new file mode 100644 index 0000000000..72a3573747 --- /dev/null +++ b/modules/client/src/controllers/BuyController.test.ts @@ -0,0 +1,188 @@ +import { MockConnextInternal, MockStore } from '../testing/mocks' +import { assert, mkHash } from '../testing/index' +import { PaymentArgs, } from '@src/types' +// @ts-ignore +global.fetch = require('node-fetch-polyfill'); + +describe('BuyController: unit tests', () => { + const user = "0xfb482f8f779fd96a857f1486471524808b97452d" + let connext: MockConnextInternal + const mockStore = new MockStore() + + beforeEach(async () => { + mockStore.setChannel({ + user, + balanceWei: [1, 1], + balanceToken: [10, 10], + }) + connext = new MockConnextInternal({ user, store: mockStore.createStore() }) + }) + + it('should work for user to hub payments', async () => { + await connext.start() + await connext.buyController.buy({ + meta: {}, + payments: [ + { + amount: { amountToken: '1', amountWei: '0' }, + type: 'PT_CHANNEL', + meta: {}, + recipient: '$$HUB$$', + }, + ], + }) + + await new Promise(res => setTimeout(res, 20)) + connext.mockHub.assertReceivedUpdate({ + reason: 'Payment', + args: { + amountToken: '1', + amountWei: '0', + recipient: 'hub', + } as PaymentArgs, + sigUser: true, + sigHub: false, + }) + }) + + it('should work for hub to user payments', async () => { + mockStore.setSyncResultsFromHub([{ + type: "channel", + update: { + reason: "Payment", + args: { + recipient: "user", + amountToken: '1', + amountWei: '1', + } as PaymentArgs, + txCount: 1, + sigHub: mkHash('0x51512'), + }, + }]) + connext = new MockConnextInternal({ user, store: mockStore.createStore() }) + + await connext.start() + + await new Promise(res => setTimeout(res, 20)) + connext.mockHub.assertReceivedUpdate({ + reason: 'Payment', + args: { + amountToken: '1', + amountWei: '1', + recipient: 'user', + } as PaymentArgs, + sigUser: true, + sigHub: true, + }) + }) + + it('should fail if the user sends a thread payment', async () => { + await connext.start() + // single thread payments + await assert.isRejected(connext.buyController.buy({ + meta: {}, + payments: [ + { + amount: { amountToken: '1', amountWei: '0' }, + type: 'PT_THREAD', + meta: {}, + recipient: '$$HUB$$', + }, + ], + }), + /REB-36/ + ) + + // embedded thread payments + await assert.isRejected(connext.buyController.buy({ + meta: {}, + payments: [ + { + amount: { amountToken: '1', amountWei: '0' }, + type: 'PT_CHANNEL', + meta: {}, + recipient: '$$HUB$$', + }, + { + amount: { amountToken: '1', amountWei: '0' }, + type: 'PT_THREAD', + meta: {}, + recipient: '$$HUB$$', + }, + ], + }), + /REB-36/ + ) + }) + + it('should fail if it cannot generate a valid state', async () => { + await connext.start() + // single thread payments + await assert.isRejected(connext.buyController.buy({ + meta: {}, + payments: [ + { + amount: { amountToken: '1', amountWei: '50' }, + type: 'PT_CHANNEL', + meta: {}, + recipient: '$$HUB$$', + }, + ], + }), + /User does not have sufficient Wei balance/ + ) + }) + + it('should fail if the hub returns a thread update to the sync queue', async () => { + mockStore.setSyncResultsFromHub([{ + type: "thread", + update: {} as any + }]) + connext = new MockConnextInternal({ user, store: mockStore.createStore() }) + + await assert.isRejected(connext.start(), /REB-36/) + }) + + it('should fail if the update returned by hub to sync queue is unsigned by hub', async () => { + mockStore.setSyncResultsFromHub([{ + type: "channel", + update: { + reason: "Payment", + args: { + recipient: "hub", + amountToken: '1', + amountWei: '1', + } as PaymentArgs, + txCount: 1, + sigHub: '', + }, + }]) + connext = new MockConnextInternal({ user, store: mockStore.createStore() }) + + await assert.isRejected(connext.start(), /sigHub not detected in update/) + }) + + it('should fail if the update returned by hub to sync queue is unsigned by user and directed to hub', async () => { + mockStore.setSyncResultsFromHub([{ + type: "channel", + update: { + reason: "Payment", + args: { + recipient: "hub", + amountToken: '1', + amountWei: '1', + } as PaymentArgs, + txCount: 1, + sigHub: mkHash('0x90283'), + sigUser: '', + }, + }]) + connext = new MockConnextInternal({ user, store: mockStore.createStore() }) + + await assert.isRejected(connext.start(), /sigUser not detected in update/) + }) + + afterEach(async () => { + await connext.stop() + }) +}) \ No newline at end of file diff --git a/modules/client/src/controllers/BuyController.ts b/modules/client/src/controllers/BuyController.ts new file mode 100644 index 0000000000..c5021fcac1 --- /dev/null +++ b/modules/client/src/controllers/BuyController.ts @@ -0,0 +1,58 @@ +import { PurchaseRequest, PurchasePayment, PaymentArgs, SyncResult } from '../types' +import { AbstractController } from './AbstractController' +import { getChannel } from '../lib/getChannel' + + +export default class BuyController extends AbstractController { + public async buy(purchase: PurchaseRequest): Promise<{ purchaseId: string }> { + // Small hack to inject the hub's address; can be removed eventually + purchase = { + ...purchase, + payments: purchase.payments.map(payment => ({ + ...payment, + recipient: payment.recipient === '$$HUB$$' ? process.env.HUB_ADDRESS! : payment.recipient + })), + } + + // Sign the payments + const signedPayments: PurchasePayment[] = [] + + let curChannelState = getChannel(this.store) + for (const payment of purchase.payments) { + if (payment.type == 'PT_THREAD') + throw new Error('TODO: REB-36 (enable threads)') + + if (payment.type != 'PT_CHANNEL' && payment.type != 'PT_CUSTODIAL') + throw new Error('Invalid payment type: ' + payment.type) + + const args: PaymentArgs = { + recipient: 'hub', + ...payment.amount + } + const newChannelState = await this.connext.signChannelState( + this.validator.generateChannelPayment( + curChannelState, + args, + ) + ) + + signedPayments.push({ + ...payment, + type: payment.type as any, + update: { + reason: 'Payment', + args: args, + sigUser: newChannelState.sigUser, + txCount: newChannelState.txCountGlobal, + }, + }) + + curChannelState = newChannelState + } + + const res = await this.connext.hub.buy(purchase.meta, signedPayments) + this.connext.syncController.handleHubSync(res.sync) + return res + } + +} diff --git a/modules/client/src/controllers/CollateralController.test.ts b/modules/client/src/controllers/CollateralController.test.ts new file mode 100644 index 0000000000..52a528c7dc --- /dev/null +++ b/modules/client/src/controllers/CollateralController.test.ts @@ -0,0 +1,36 @@ +import { MockConnextInternal, } from '../testing/mocks'; +// @ts-ignore +global.fetch = require('node-fetch-polyfill'); + +describe('CollateralController: unit tests', () => { + + let connext: MockConnextInternal + + beforeEach(async () => { + connext = new MockConnextInternal() + await connext.start() + }) + + it('should work', async () => { + await connext.collateralController.requestCollateral() + + await new Promise(res => setTimeout(res, 10)) + + connext.mockHub.assertReceivedUpdate({ + reason: 'ProposePendingDeposit', + args: { + depositWeiHub: '420', + depositTokenHub: '69', + depositTokenUser: '0', + depositWeiUser: '0', + }, + sigUser: true, + sigHub: false, + }) + }) + + afterEach(async () => { + await connext.stop() + }) + +}) diff --git a/modules/client/src/controllers/CollateralController.ts b/modules/client/src/controllers/CollateralController.ts new file mode 100644 index 0000000000..d52afe144d --- /dev/null +++ b/modules/client/src/controllers/CollateralController.ts @@ -0,0 +1,12 @@ +// import { WorkerStore } from '../WorkerState/WorkerState' +import { AbstractController } from './AbstractController' +import getTxCount from '../lib/getTxCount'; + +export default class CollateralController extends AbstractController { + + public requestCollateral = async (): Promise => { + console.log(`requestCollateral`) + const sync = await this.hub.requestCollateral(getTxCount(this.store)) + this.connext.syncController.handleHubSync(sync) + } +} diff --git a/modules/client/src/controllers/DepositController.test.ts b/modules/client/src/controllers/DepositController.test.ts new file mode 100644 index 0000000000..901eaadff2 --- /dev/null +++ b/modules/client/src/controllers/DepositController.test.ts @@ -0,0 +1,74 @@ +import { MockConnextInternal, patch } from '../testing/mocks'; +import { assert, } from '../testing/index'; +// @ts-ignore +global.fetch = require('node-fetch-polyfill'); + +describe('DepositController: unit tests', () => { + let connext: MockConnextInternal + + beforeEach(async () => { + connext = new MockConnextInternal() + await connext.start() + }) + + // TODO: properly mock out token transfer approval + it('should work for wei', async () => { + await connext.depositController.requestUserDeposit({ amountWei: '420', amountToken: '0' }) + await new Promise(res => setTimeout(res, 10)) + + connext.mockHub.assertReceivedUpdate({ + reason: 'ProposePendingDeposit', + args: { + depositWeiUser: '420', + depositTokenUser: '0', + }, + sigUser: true, + sigHub: true, + }) + + connext.mockContract.assertCalled('userAuthorizedUpdate', { + pendingDepositWeiUser: '420', + pendingDepositTokenUser: '0', + }) + + assert.containSubset(connext.store.getState(), { + persistent: { + channel: { + pendingDepositTokenHub: '9', + pendingDepositTokenUser: '0', + pendingDepositWeiHub: '8', + pendingDepositWeiUser: '420', + }, + }, + }) + }) + + it('should fail if the hub returns invalidly signed update', async () => { + connext.validator.generateProposePendingDeposit = (req, signer) => { throw new Error('Invalid signer') } + + await assert.isRejected( + connext.depositController.requestUserDeposit({ + amountWei: '420', + amountToken: '69', + }), + /Invalid signer/ + ) + + }) + + it('should fail if the hub returns an invalid timestamp', async () => { + patch(connext.mockHub, 'requestDeposit', async (old: any, ...args: any[]) => { + const res = await old(...args) + res.updates[0].update.args.timeout = 69 + return res + }) + + await assert.isRejected( + connext.depositController.requestUserDeposit({ + amountWei: '420', + amountToken: '69', + }), + /timestamp/ + ) + }) +}) diff --git a/modules/client/src/controllers/DepositController.ts b/modules/client/src/controllers/DepositController.ts new file mode 100644 index 0000000000..72efc853fb --- /dev/null +++ b/modules/client/src/controllers/DepositController.ts @@ -0,0 +1,155 @@ +import getTxCount from '../lib/getTxCount' +import { Payment, convertDeposit, convertChannelState, ChannelState, UpdateRequestTypes, SyncResult, UpdateRequest, ChannelStateUpdate, convertPayment } from '../types' +import { getLastThreadId } from '../lib/getLastThreadId' +import { AbstractController } from "./AbstractController"; +import { validateTimestamp } from "../lib/timestamp"; +import { toBN } from '../helpers/bn'; +const tokenAbi = require("human-standard-token-abi") + +/* + * Rule: + * - Lock needs to be held any time we're requesting sync from the hub + * - We can send updates to the hub any time we want (because the hub will + * reject them if they are out of sync) + * - In the case of deposit, the `syncController` will expose a method + * called something like "tryEnqueueSyncResultsFromHub", which will add the + * sync results if the lock isn't held, but ignore them otherwise (ie, + * because they will be picked up on the next sync anyway), and this method + * will be used by the DepositController. + */ +export default class DepositController extends AbstractController { + private resolvePendingDepositPromise: any = null + + // TODO: should the deposit params (timeout, payment) be saved for sig recovery or just use params sent by hub? + + public async requestUserDeposit(deposit: Payment) { + const signedRequest = await this.connext.signDepositRequestProposal(deposit) + + const sync = await this.hub.requestDeposit( + signedRequest, + getTxCount(this.store), + getLastThreadId(this.store) + ) + + this.connext.syncController.handleHubSync(sync) + + // There can only be one pending deposit at a time, so it's safe to return + // a promise that will resolve/reject when we eventually hear back from the + // hub. + return new Promise((res, rej) => { + this.resolvePendingDepositPromise = { res, rej } + }) + } + + /** + * Given arguments for a user authorized deposit which we want to send + * to chain, generate a state for that deposit, send that state to chain, + * and return the state once it has been successfully added to the mempool. + */ + public async sendUserAuthorizedDeposit(prev: ChannelState, update: UpdateRequestTypes['ProposePendingDeposit']) { + try { + await this._sendUserAuthorizedDeposit(prev, update) + this.resolvePendingDepositPromise && this.resolvePendingDepositPromise.res() + } catch (e) { + console.warn( + `Error handling userAuthorizedUpdate (this update will be ` + + `countersigned and held until it expires - at which point it ` + + `will be invalidated - or the hub sends us a subsequent ` + + `ConfirmPending.`, e + ) + this.resolvePendingDepositPromise && this.resolvePendingDepositPromise.rej(e) + } finally { + this.resolvePendingDepositPromise = null + } + } + + private async _sendUserAuthorizedDeposit(prev: ChannelState, update: UpdateRequestTypes['ProposePendingDeposit']) { + function DepositError(msg: string) { + return new Error(`${msg} (update: ${JSON.stringify(update)}; prev: ${JSON.stringify(prev)})`) + } + + if (!update.sigHub) { + throw DepositError(`A userAuthorizedUpdate must have a sigHub`) + } + + if (update.sigUser) { + // The `StateUpdateController` maintains the invariant that a + // userAuthorizedUpdate will have a `sigUser` if-and-only-if it has been + // sent to chain, so if the update being provided here has a user sig, + // then either it has already been sent to chain, or there's a bug + // somewhere. + throw DepositError( + `Cannot send a userAuthorizedUpdate which already has a sigUser ` + + `(see comments in source)` + ) + } + + const { args } = update + + // throw a deposit error if the signer is not correct on update + if (!args.sigUser) { + throw DepositError(`Args are unsigned, not submitting a userAuthorizedUpdate to chain.`) + } + + try { + this.connext.validator.assertDepositRequestSigner({ + amountToken: args.depositTokenUser, + amountWei: args.depositWeiUser, + sigUser: args.sigUser + }, prev.user) + } catch (e) { + throw DepositError(e.message) + } + + const state = await this.connext.signChannelState( + this.validator.generateProposePendingDeposit( + prev, + update.args, + ), + ) + state.sigHub = update.sigHub + + const tsErr = validateTimestamp(this.store, update.args.timeout) + if (tsErr) { + throw DepositError(tsErr) + } + + + try { + if (args.depositTokenUser !== '0') { + console.log(`Approving transfer of ${args.depositTokenUser} tokens`) + const token = new this.connext.opts.web3.eth.Contract( + tokenAbi, + this.connext.opts.tokenAddress + ) + let sendArgs: any = { + from: prev.user, + } + const call = token.methods.approve(prev.contractAddress, args.depositTokenUser) + const gasEstimate = await call.estimateGas(sendArgs) + sendArgs.gas = toBN(Math.ceil(this.connext.contract.gasMultiple * gasEstimate)) + await call.send(sendArgs) + } + console.log('Sending user authorized deposit to chain.') + const tx = await this.connext.contract.userAuthorizedUpdate(state) + await tx.awaitEnterMempool() + } catch (e) { + const currentChannel = await this.connext.contract.getChannelDetails(prev.user) + if (update.txCount && currentChannel.txCountGlobal >= update.txCount) { + // Update has already been sent to chain + console.log(`Non-critical error encountered processing userAuthorizedUpdate:`, e) + console.log( + `Update has already been applied to chain ` + + `(${currentChannel.txCountGlobal} >= ${update.txCount}), ` + + `countersigning and returning update.` + ) + return + } + + // logic should be retry transaction UNTIL timeout elapses, then + // submit the invalidation update + throw DepositError('Sending userAuthorizedUpdate to chain: ' + e) + } + } + +} diff --git a/modules/client/src/controllers/ExchangeController.test.ts b/modules/client/src/controllers/ExchangeController.test.ts new file mode 100644 index 0000000000..a63aa4e5ab --- /dev/null +++ b/modules/client/src/controllers/ExchangeController.test.ts @@ -0,0 +1,44 @@ +import { MockStore, MockConnextInternal } from '../testing/mocks'; +import { mkAddress } from '../testing'; +// @ts-ignore +global.fetch = require('node-fetch-polyfill'); + +describe('ExchangeController: unit tests', () => { + const user = mkAddress('0xUUU') + let connext: MockConnextInternal + const mockStore = new MockStore() + + beforeEach(async () => { + connext = new MockConnextInternal() + await connext.start() + }) + + it('should exchange all of users wei balance if total exchanged tokens under booty limit', async () => { + // add channel to the store + mockStore.setChannel({ + user, + balanceWei: [0, 10], + balanceToken: [50, 0], + }) + mockStore.setExchangeRate({ 'USD': '5' }) + connext = new MockConnextInternal({ user, store: mockStore.createStore() }) + await connext.start() + await connext.exchangeController.exchange('10', 'wei') + await new Promise(res => setTimeout(res, 20)) + + connext.mockHub.assertReceivedUpdate({ + reason: 'Exchange', + args: { + weiToSell: '10', + tokensToSell: '0', + seller: "user", + }, + sigUser: true, + sigHub: false, + }) + }) + + afterEach(async () => { + await connext.stop() + }) +}) diff --git a/modules/client/src/controllers/ExchangeController.ts b/modules/client/src/controllers/ExchangeController.ts new file mode 100644 index 0000000000..b7b5eb3564 --- /dev/null +++ b/modules/client/src/controllers/ExchangeController.ts @@ -0,0 +1,84 @@ +import getExchangeRates from '../lib/getExchangeRates' +import * as actions from '../state/actions' +import { AbstractController } from './AbstractController' +import { ConnextStore } from '../state/store' +import { Poller } from '../lib/poller/Poller'; +import { ConnextInternal } from '../Connext'; +import { BEI_AMOUNT, FINNEY_AMOUNT, WEI_AMOUNT } from '../lib/constants' +import getTxCount from '../lib/getTxCount'; +import BigNumber from 'bignumber.js'; + +const ONE_MINUTE = 1000 * 60 + +export function validateExchangeRate(store: ConnextStore, rate: string) { + const rates = getExchangeRates(store.getState()) + if (!rates || !rates.USD) { + return ( + `No exchange rates set in store.` + ) + } + const delta = Math.abs(+rates.USD - +rate) + const allowableDelta = +rates.USD * 0.02 + if (delta > allowableDelta) { + // TODO: send an invalidating state back to the hub (REB-12) + return ( + `Proposed exchange rate '${rate}' exceeds ` + + `difference from current rate '${rates.USD}'` + ) + } +} + +export class ExchangeController extends AbstractController { + static POLLER_INTERVAL_LENGTH = ONE_MINUTE + + private poller: Poller + + constructor(name: string, connext: ConnextInternal) { + super(name, connext) + this.poller = new Poller({ + name: 'ExchangeController', + interval: ExchangeController.POLLER_INTERVAL_LENGTH, + callback: this.pollExchangeRates.bind(this), + timeout: 60 * 1000, + }) + } + + async start() { + await this.poller.start() + } + + async stop() { + this.poller.stop() + } + + private pollExchangeRates = async () => { + try { + const rates = await this.hub.getExchangerRates() + if (rates.USD) { + // These are the values wallet expects + rates.USD = new BigNumber(rates.USD).toFixed(2) + rates.BEI = (new BigNumber(rates.USD)).times(new BigNumber(BEI_AMOUNT)).toFixed(0) + rates.WEI = new BigNumber(WEI_AMOUNT).toFixed(0) + rates.ETH = new BigNumber(1).toFixed(0) + rates.BOOTY = new BigNumber(rates.USD).toFixed(0) + rates.FINNEY = new BigNumber(FINNEY_AMOUNT).toFixed(0) + + this.store.dispatch(actions.setExchangeRate({ + lastUpdated: new Date(), + rates, + })) + } + } catch (e) { + console.error('Error polling for exchange rates:', e) + // TODO: properly log this, once API logger is in + } + } + + public exchange = async (toSell: string, currency: "wei" | "token") => { + const weiToSell = currency === "wei" ? toSell : '0' + const tokensToSell = currency === "token" ? toSell : '0' + const sync = await this.hub.requestExchange(weiToSell, tokensToSell, getTxCount(this.store)) + this.connext.syncController.handleHubSync(sync) + + } +} diff --git a/modules/client/src/controllers/StateUpdateController.test.ts b/modules/client/src/controllers/StateUpdateController.test.ts new file mode 100644 index 0000000000..313cf45f98 --- /dev/null +++ b/modules/client/src/controllers/StateUpdateController.test.ts @@ -0,0 +1,93 @@ +import { MockStore, MockConnextInternal, MockHub } from '../testing/mocks'; +import { mkAddress, assertChannelStateEqual, getThreadState, mkHash, getChannelState } from '../testing'; +import { assert, parameterizedTests } from '../testing' +import { SyncResult, ThreadStateUpdate } from '@src/types'; +// @ts-ignore +global.fetch = require('node-fetch-polyfill'); + +/* +StateUpdateController is used by all the other controllers to handle various types of state updates that are present in the `runtime.syncResultsFromHub` state. To do this, it subscribes to the store, and handles the the updates from the hub in the `handleSyncItem` method. As it is an internal controller with no public API there are no unit tests written. +*/ + +describe('StateUpdateController: unit tests', () => { + const user = mkAddress('0xUUU') + let connext: MockConnextInternal + + parameterizedTests([ + { + name: 'reject invalidation that has not timed out', + timeout: 1000, + blockTimestamp: 500, + shouldFailWith: /Hub proposed an invalidation for an update that has not yet timed out/ + }, + + { + name: 'accept a valid invalidation', + timeout: 1000, + blockTimestamp: 2000, + }, + + ], async tc => { + const mockStore = new MockStore() + mockStore.setChannel({ + pendingWithdrawalTokenUser: '100', + txCountGlobal: 2, + txCountChain: 2, + timeout: tc.timeout, + }) + + mockStore.setLatestValidState({ + txCountGlobal: 1, + txCountChain: 1, + }) + + connext = new MockConnextInternal({ + user, + store: mockStore.createStore(), + }) + + connext.opts.web3.eth.getBlock = async () => { + return { + timestamp: tc.blockTimestamp, + } as any + } + + await connext.start() + + const res = connext.stateUpdateController.handleSyncItem({ + type: 'channel', + update: { + reason: 'Invalidation', + txCount: 3, + args: { + previousValidTxCount: 1, + lastInvalidTxCount: 2, + reason: "CU_INVALID_TIMEOUT", + }, + sigHub: '0xsig-hub', + }, + }) + + if (tc.shouldFailWith) { + await assert.isRejected(res, tc.shouldFailWith) + assert.deepEqual(connext.mockHub.receivedUpdateRequests, []) + } else { + await res + connext.mockHub.assertReceivedUpdate({ + reason: 'Invalidation', + args: { + previousValidTxCount: 1, + lastInvalidTxCount: 2, + reason: 'CU_INVALID_TIMEOUT', + }, + sigUser: true, + sigHub: true, + }) + } + + }) + + afterEach(async () => { + await connext.stop() + }) +}) diff --git a/modules/client/src/controllers/StateUpdateController.ts b/modules/client/src/controllers/StateUpdateController.ts new file mode 100644 index 0000000000..e9143799e1 --- /dev/null +++ b/modules/client/src/controllers/StateUpdateController.ts @@ -0,0 +1,486 @@ +import { ConnextState } from '../state/store' +import { ConnextStore } from '../state/store' +import { SyncResult, convertExchange, UpdateRequest, UnsignedChannelState } from '../types' +import { ChannelState, UpdateRequestTypes } from '../types' +import { AbstractController } from './AbstractController' +import * as actions from '../state/actions' +import { getChannel } from '../lib/getChannel' +import { Unsubscribe } from 'redux' +import { Action } from 'typescript-fsa/lib' +import { validateTimestamp } from '../lib/timestamp'; +import { validateExchangeRate } from './ExchangeController'; +import { assertUnreachable } from '../lib/utils' +import { hasPendingOps } from '../hasPendingOps' + +type StateUpdateHandlers = { + [Type in keyof UpdateRequestTypes]: ( + this: StateUpdateController, + prev: ChannelState, + update: UpdateRequestTypes[Type], + ) => Promise +} + +/** + * Watch a value on the store, calling `onChange` callback each time it + * changes. + * + * If the value changes while `onChange` is running, it will be called again + * after it completes. + */ +export async function watchStore( + store: ConnextStore, + getter: (s: ConnextState) => T, + onChange: (v: T) => Promise, +) { + let inCallback = false + let didChange = false + let lastVal = {} + + async function onStoreChange() { + const val = getter(store.getState()) + if (val === lastVal) + return + + if (inCallback) { + didChange = true + return + } + inCallback = true + + const prevVal = lastVal + lastVal = val + + try { + // If ``onChange`` raises an exception, set ``inCallback`` to false, but + // don't try to retry. This means that we _could_ get into a weird state + // (ie, if ``didChange = true`` and ``onChange(...)`` raises an + // exception, then ``onChange(...)`` *should* be called, but also calling + // it might raise another exception, so we don't). + await onChange(val) + } catch (e) { + + // Some naieve but hopefully useful retry logic. If the onChange callback + // throws an exception *and* the current item doesn't change for + // 30 seconds, hope that it's a temporary failure and try again. This + // should be safe because the callback should be idempotent, and we're + // checking that it isn't already running before we call it. + setTimeout(() => { + console.warn( + 'Exception was previously raised by watchStore callback ' + + 'and no progress has been made in the last 30 seconds. ' + + 'Trying again...' + ) + if (lastVal === val && !inCallback) { + lastVal = {} + onStoreChange() + } + }, 1000 * 30) + + throw e + } finally { + inCallback = false + } + + if (didChange) { + didChange = false + await onStoreChange() + } + } + + await onStoreChange() + return store.subscribe(onStoreChange) +} + +export default class StateUpdateController extends AbstractController { + private unsubscribe: Unsubscribe | null = null + + async start() { + this.unsubscribe = await watchStore( + this.store, + state => { + // channel in dispute status, prevent further updates + // TODO: thoroughly handle client response + const status = state.runtime.channelStatus + if (status === "CS_CHANNEL_DISPUTE") { + console.warn(`Not processing updates, channel is not open. Status: ${status}`) + return null + } else if (status === "CS_THREAD_DISPUTE") { + throw new Error('THIS IS BAD. Channel is set to thread dispute state, before threads are enabled. See REB-36. Disabling client.') + } + // channel is open + const item = state.runtime.syncResultsFromHub[0] + if (item && item.type == 'thread') + throw new Error('REB-36: enable threads!') + + // No sync results from hub; nothing to do + if (!item) + return null + + // Wait until we've flushed all the updates to the hub before + // processing the next item. + const { updatesToSync } = state.persistent.syncControllerState + if (updatesToSync.length > 0) + return null + + // Otherwise, call `syncOneItemFromStore` on the item. + return item + }, + item => this.syncOneItemFromStore(item), + ) + } + + async stop() { + if (this.unsubscribe) + this.unsubscribe() + } + + private _queuedActions: null | Action[] = null + + /** + * Used by state update handlers to queue an action to be run after + * `handleStateUpdates` has completed successfully. + */ + private queueAction(action: Action) { + if (!this._queuedActions) + throw new Error('Can only queue an action while `handleStateUpdates` is running!') + this._queuedActions.push(action) + } + + private flushQueuedActions() { + if (!this._queuedActions) + throw new Error('Can only flush queue while `handleStateUpdates` is running!') + for (const action of this._queuedActions) + this.store.dispatch(action) + this._queuedActions = null + } + + private async syncOneItemFromStore(item: SyncResult | null) { + if (!item) + return + + await this.handleSyncItem(item) + this.store.dispatch(actions.dequeueSyncResultsFromHub(item)) + } + + public async handleSyncItem(item: SyncResult) { + this._queuedActions = [] + + if (item.type === 'thread') { + /* + const update = actionItem.update + const err = this.connext.validator.threadPayment(update.state) + if (err) { + console.error('Invalid thread signatures detected:', err, update) + throw new Error('Invalid thread signatures: ' + err) + } + if (!actionItem.update.id) + throw new Error('uh oh we should never have a thread update without an ID here') + this.store.dispatch(actions.setLastThreadId(actionItem.update.id!)) + this.flushQueuedActions() + continue + */ + throw new Error('REB-36: enable threads!') + } + + const update = item.update + console.log(`Applying update from hub: ${update.reason} txCount=${update.txCount}:`, update) + + const connextState = this.getState() + const prevState: ChannelState = connextState.persistent.channel + const latestValidState: ChannelState = connextState.persistent.latestValidState + + console.log('prevState:', prevState) + + if (update.txCount && update.txCount <= prevState.txCountGlobal) { + console.warn( + `StateUpdateController received update with old ` + + `${update.txCount} < ${prevState.txCountGlobal}. Skipping.` + ) + return + } + + if (update.reason != 'Invalidation' && update.txCount && update.txCount != prevState.txCountGlobal + 1) { + throw new Error( + `Update txCount ${update.txCount} != ${prevState.txCountGlobal} + 1 ` + + `(ie, the update is trying to be applied on top of a state that's ` + + `later than our most recent state)` + ) + } + + if (update.reason === 'EmptyChannel') { + console.log('Channel has exited dispute phase, re-enabling client') + this.store.dispatch(actions.setChannelStatus("CS_OPEN")) + } + + const nextState = await this.connext.validator.generateChannelStateFromRequest( + update.reason === 'Invalidation' ? latestValidState : prevState, + update + ) + + // any sigs included on the updates should be valid + this.assertValidSigs(update, nextState) + + // Note: it's important that the client doesn't do anything in response + // to a double signed state, because it means a restored wallet can be + // sync'd by sending it only the latest double-signed state. This isn't + // necessarily a hard requirement - right now we're sending all states + // on every sync - but it would be nice if we can make that guarantee. + if (update.sigHub && update.sigUser) { + this.store.dispatch(actions.setChannel({ + update: update, + state: { + ...nextState, + sigHub: update.sigHub, + sigUser: update.sigUser, + }, + })) + return + } + + if (update.sigUser && !update.sigHub) { + throw new Error('Update has user sig but not hub sig: ' + JSON.stringify(update)) + } + + // NOTE: the previous state passed into the update handlers is NOT + // the previous passed into the validators to generate the + // `nextState`. + const err = await (this.updateHandlers as any)[update.reason].call(this, prevState, update) + if (err) { + console.warn('Not countersigning state update: ' + err) + return + } + + const signedState: ChannelState = await this.connext.signChannelState(nextState) + if (update.sigHub) + signedState.sigHub = update.sigHub + + // For the moment, send the state to the hub and await receipt before + // saving the state to our local store. We're doing this *for now* because + // it dramatically simplifies the sync logic, but does rely on trusting the + // hub: the hub could reject a payment (ex, denying the user access to + // content they purchased), then later countersign and return the state + // (effectively stealing their money). We've decided that this is an + // acceptable risk *for now* because it will only apply to custodial + // payments (ie, the hub can't block thread payments), and it will be fixed + // in the future by storing the pending payment locally, and alerting the + // user if the hub attempts to countersign and return it later. + await this.connext.syncController.sendUpdateToHub({ + id: update.id, + reason: update.reason, + args: update.args, + state: signedState, + }) + + this.flushQueuedActions() + await this.connext.awaitPersistentStateSaved() + + } + + /** + * Theses handlers will be called for updates being sent from the hub. + * + * They should perform any update-reason-specific validation (for example, + * the `Payment` handler should allow any payment where the recipient is + * 'user' (ie, if the hub is sending us money), but if the recipient is + * the hub (ie, we're paying the hub), it should double check that the state + * has already been signed (otherwise the hub could send us an unsigned + * payment from user-> hub). + */ + updateHandlers: StateUpdateHandlers = { + 'Payment': async (prev, update) => { + // We will receive Payment updates from the hub when: + // 1. The hub has countersigned a payment we've made (ex, `recipient == + // 'hub'` and we have already signed it) + // 2. The hub is sending us a payment (ex, a custodial payment; in this + // case, `recipient == 'user'` and will only have a hub sig) + // 3. We're doing a multi-device sync (but we won't get here, because the + // state will be fully signed) + + // Allow any payment being made to us (ie, where recipient == 'user'), + // but double check that we've signed the update if the hub is the + // recipient (ie, because it was a state we generated in BuyController) + assertSigPresence(update, 'sigHub') + + if (update.args.recipient != 'user') { + assertSigPresence(update, 'sigUser') + } + }, + + 'Exchange': async (prev, update) => { + // We will receive Exchange updates from the hub only when: + // 1. The hub is countersigning and returning an exchange we have sent + // from the `ExchangeController` + // 2. The ExchangeController sends an unsigned update here + if (!update.sigHub) { + const ExchangeError = (msg: string) => { + return new Error(`${msg} (args: ${JSON.stringify(convertExchange("str", update.args))}; prev: ${JSON.stringify(prev)})`) + } + + // validate the exchange rate against store + const err = validateExchangeRate(this.store, update.args.exchangeRate) + if (err) + throw ExchangeError(err) + return + } + assertSigPresence(update) + }, + + 'ProposePendingDeposit': async (prev, update) => { + // We will recieve ProposePendingDeposit updates from the hub when: + // 1. The hub wants to collatoralize + // 2. We have proposed a deposit (see DepositController) + + // 1: The hub is requesting a deposit, ex: it wants to collateralize + if (!update.sigHub) { + // This is a hub authorized deposit (ex, because the hub wants to + // recollatoralize). We don't need to check or do anything other than + // countersign and return to the hub. + console.log('Received a hub authorized deposit; countersigning and returning.') + const CollateralError = (msg: string) => { + return new Error(`${msg} (args: ${JSON.stringify(update.args)}; prev: ${JSON.stringify(prev)})`) + } + + // verification of args: timestamp, no user deposits + const tsErr = validateTimestamp(this.store, update.args.timeout) + if (tsErr) + throw CollateralError(tsErr) + + return + } + + // 2: We have requested a deposit + if (update.sigHub && !update.sigUser) { + // Because we will only save the signed update once it has been sent to + // chain, we can safely assume that the deposit is on chain + // if-and-only-if we have signed it. + // Note that the `sendDepositToChain` method will validate that the + // deposit amount is the amount we expect. + + // user signature on the deposit parameters in enforced within the + // deposit controller, and additionally in the validators if it is + // included, so no need to check here + return await this.connext.depositController.sendUserAuthorizedDeposit(prev, update) + } + + throw new Error( + `Recieved a ProposePendingDeposit from the hub that was not signed ` + + `by the user (update: ${update})` + ) + }, + + 'ProposePendingWithdrawal': async (prev, update) => { + if (!update.sigHub) { + const WithdrawalError = (msg: string) => { + return new Error(`${msg} (args: ${JSON.stringify(update.args)}; prev: ${JSON.stringify(prev)})`) + } + + const tsErr = validateTimestamp(this.store, update.args.timeout) + if (tsErr) + throw WithdrawalError(tsErr) + + const exchangeErr = validateExchangeRate(this.store, update.args.exchangeRate) + if (exchangeErr) + throw WithdrawalError(exchangeErr) + + return + } + }, + + 'Invalidation': async (prev, update) => { + + // NOTE (BSU-72): this will break in two ways if the hub tries to + // invalidate a state without a timeout: + // 1. The txCountGlobal will not necessarily be the most recent (ex, + // because there may have been tips on top of the pending state) + // 2. The `didContractEmitUpdateEvent` will throw an error because it + // has not been tested with `timeout = 0` states. + + if (update.args.lastInvalidTxCount !== prev.txCountGlobal) { + throw new Error( + `Hub proposed invalidation for a state which isn't our latest. ` + + `Invalidation: ${JSON.stringify(update)} ` + + `Latest state: ${JSON.stringify(prev)}` + ) + } + + if (!hasPendingOps(prev)) { + throw new Error( + `Hub proposed invalidation for a double signed state with no ` + + `pending fields. Invalidation: ${JSON.stringify(update)} ` + + `state: ${JSON.stringify(prev)}` + ) + } + + const { syncController } = this.connext + const { didEmit, latestBlock, event } = await syncController.didContractEmitUpdateEvent(prev, update.createdOn) + + switch (didEmit) { + case 'unknown': + throw new Error( + `Hub proposed an invalidation for an update that has not yet ` + + `timed out. Update timeout: ${prev.timeout}; latest block: ` + + `timestamp: ${latestBlock.timestamp} (hash: ${latestBlock.hash})` + ) + + case 'yes': + throw new Error( + `Hub proposed invalidation for a state that has made it to chain. ` + + `State txCount: ${prev.txCountGlobal}, event: ${JSON.stringify(event)}` + ) + + case 'no': + // No event was emitted, the state has timed out; countersign and + // return the invalidation + return + + default: + assertUnreachable(didEmit) + } + + }, + + 'EmptyChannel': async (prev, update) => { + // channel event is checked in the validator + return + }, + + 'ConfirmPending': async (prev, update) => { + // throw new Error('BSU-37: merge the PR!') + // channel event is checked in the validator + return + }, + 'OpenThread': async (prev, update) => { + throw new Error('REB-36: enable threads!') + }, + 'CloseThread': async (prev, update) => { + throw new Error('REB-36: enable threads!') + }, + } + + private assertValidSigs(update: UpdateRequest, proposed: UnsignedChannelState) { + // if sig is on the update, it recovers it + // if the sig is not on the update, it does nothing + if (update.sigHub) { + this.validator.assertChannelSigner({ sigHub: update.sigHub, ...proposed }, "hub") + } + + if (update.sigUser) { + this.validator.assertChannelSigner({ sigUser: update.sigUser, ...proposed }, "user") + } + } + +} + + +/** + * This function takes sigs and asserts that they exist and that they are + * signed by the correct person. + * @param x: string of sigs to assert + */ +function assertSigPresence(update: UpdateRequest, x?: "sigHub" | "sigUser") { + if (x && !update[x]) + throw new Error(`${x} not detected in update: ${JSON.stringify(update)}`) + if (!x && !update["sigHub"] && !update["sigUser"]) { + throw new Error(`Update does not have both signatures ${JSON.stringify(update)}`) + } +} diff --git a/modules/client/src/controllers/SyncController.test.ts b/modules/client/src/controllers/SyncController.test.ts new file mode 100644 index 0000000000..5ef6731990 --- /dev/null +++ b/modules/client/src/controllers/SyncController.test.ts @@ -0,0 +1,457 @@ +import { SyncResult, convertChannelState, InvalidationArgs, UpdateRequest, convertVerboseEvent, unsignedChannel } from '../types' +import { mergeSyncResults, filterPendingSyncResults } from './SyncController' +import { assert, getChannelState, mkAddress, mkHash, parameterizedTests, updateObj, getChannelStateUpdate } from '../testing' +import { MockConnextInternal, MockStore, MockHub, MockWeb3, patch } from '../testing/mocks'; +import { StateGenerator } from '../StateGenerator'; +// @ts-ignore +global.fetch = require('node-fetch-polyfill'); + +describe('mergeSyncResults', () => { + function mkResult(txCount: number | null, sigs: 'hub' | 'user' | 'both' = 'both'): SyncResult { + return { + type: 'channel', + update: { + reason: 'Payment', + args: { expectedPos: txCount } as any, + txCount, + sigHub: sigs == 'hub' || sigs == 'both' ? 'sig-hub' : undefined, + sigUser: sigs == 'user' || sigs == 'both' ? 'sig-user' : undefined, + }, + } + } + + const tests = [ + ['empty', [], []], + ['only old', [], [mkResult(1)]], + ['only new', [mkResult(1)], []], + ['both old', [mkResult(1)], [mkResult(2)]], + ['both new', [mkResult(2)], [mkResult(1)]], + ['both new', [mkResult(1)], [mkResult(1)]], + ['mixed', [mkResult(1), mkResult(2)], [mkResult(3), mkResult(2)]], + ] + + tests.forEach(([name, xs, ys]) => { + it(name as any, () => { + const actual = mergeSyncResults(xs as any, ys as any) + const expectedCount = Math.max(0, ...[...xs as any, ...ys as any].map(x => x.update.txCount)) + assert.equal(actual.length, expectedCount) + + for (let i = 0; i < actual.length; i += 1) { + assert.containSubset(actual[i], { + update: { + args: { + expectedPos: i + 1, + }, + txCount: i + 1, + sigHub: 'sig-hub', + sigUser: 'sig-user', + }, + }, ( + `Mismatch at item ${i + 1}: should have sigHub, sigUser, and expectedPos = ${i + 1}:\n` + + actual.map((x, idx) => `${idx + 1}: ${JSON.stringify(x)}`).join('\n') + )) + } + }) + }) + + it('should not merge sigs', () => { + const actual = mergeSyncResults([mkResult(1, 'user')], [mkResult(2), mkResult(1, 'hub')]) + assert.containSubset(actual[0], { + update: { + txCount: 1, + sigUser: 'sig-user', + sigHub: undefined, + }, + }) + assert.containSubset(actual[1], { + update: { + txCount: 1, + sigUser: undefined, + sigHub: 'sig-hub', + }, + }) + assert.containSubset(actual[2], { + update: { + txCount: 2, + sigUser: 'sig-user', + sigHub: 'sig-hub', + }, + }) + }) + + it('should handle null states', () => { + const actual = mergeSyncResults([mkResult(null), mkResult(1)], [mkResult(null), mkResult(2)]) + assert.deepEqual(actual.map(t => (t.update as any).txCount), [1, 2, null]) + }) + +}) + +describe('filterPendingSyncResults', () => { + function mkFromHub(opts: any) { + return { + type: 'channel', + update: { + reason: 'Payment', + ...opts, + }, + } + } + + parameterizedTests([ + { + name: 'toSync contains invalidation', + + fromHub: [mkFromHub({ txCount: 4 }), mkFromHub({ txCount: 5 })], + + toHub: [ + { + reason: 'Invalidation', + args: { + previousValidTxCount: 4, + lastInvalidTxCount: 5, + }, + sigUser: true, + txCount: 6, + }, + ], + + expected: [{ txCount: 4 }], + }, + + { + name: 'results contain state with more sigs', + + fromHub: [ + mkFromHub({ + txCount: 5, + sigHub: true, + sigUser: true, + }) + ], + + toHub: [ + { + txCount: 5, + sigUser: true, + }, + ], + + expected: [ + { + txCount: 5, + sigHub: true, + sigUser: true, + }, + ], + }, + + { + name: 'results are fully signed signed', + + fromHub: [mkFromHub({ txCount: 5, sigHub: true, sigUser: true })], + + toHub: [ + { + txCount: 5, + sigHub: true, + sigUser: true, + }, + ], + + expected: [ + { + txCount: 5, + sigHub: true, + sigUser: true, + }, + ], + }, + + { + name: 'results are less signed', + + fromHub: [mkFromHub({ txCount: 5, sigHub: true })], + + toHub: [ + { + txCount: 5, + sigHub: true, + sigUser: true, + }, + ], + + expected: [], + }, + + { + name: 'unsigned state being synced', + + fromHub: [mkFromHub({ id: -69 })], + + toHub: [ + { + id: -69, + sigUser: true, + }, + ], + + expected: [], + }, + + { + name: 'unsigned state is new', + + fromHub: [mkFromHub({ id: -69 })], + + toHub: [], + + expected: [ + { id: -69 }, + ], + }, + ], tc => { + const actual = filterPendingSyncResults(tc.fromHub as any, tc.toHub as any) + assert.equal( + actual.length, tc.expected.length, + `Actual != expected;\n` + + `Actual: ${JSON.stringify(actual)}\n` + + `Expected: ${JSON.stringify(tc.expected)}` + ) + + for (let i = 0; i < actual.length; i += 1) + assert.containSubset(actual[i].update, tc.expected[i]) + }) +}) + +describe('SyncController.findBlockNearestTimeout', () => { + const connext = new MockConnextInternal() + + let latestBlockNumber: number | null = null + connext.opts.web3.eth.getBlock = ((num: any) => { + if (num == 'latest') + num = latestBlockNumber + return Promise.resolve({ + timestamp: num, + number: num, + }) + }) as any + + // To simplify testing, assume the timestamp == block.number + parameterizedTests([ + { + name: 'current block is sufficiently close', + latestBlockNumber: 500, + targetTimestamp: 490, + expectedBlockNumber: 500, + }, + + { + name: 'current block is before the timeout', + latestBlockNumber: 400, + targetTimestamp: 500, + expectedBlockNumber: 400, + }, + + { + name: 'current block is far in the future', + latestBlockNumber: 20000, + targetTimestamp: 500, + expectedBlockNumber: 510, + }, + ], async input => { + latestBlockNumber = input.latestBlockNumber + const block = await connext.syncController.findBlockNearestTimeout(input.targetTimestamp, 15) + assert.equal(block.number, input.expectedBlockNumber) + }) + +}) + +describe("SyncController: invalidation handling", () => { + const user = mkAddress('0xUUU') + let connext: MockConnextInternal + const prevStateTimeout = 1000 + + const lastValid = getChannelState("empty", { + balanceToken: [10, 10], + balanceWei: [10, 10], + timeout: Math.floor(Date.now() / 1000) + 69, + txCount: [2, 1], + sigHub: mkHash('0xeh197'), + sigUser: mkHash('0xeh197'), + }) + + const prev = getChannelState("empty", { + ...lastValid, + pendingDepositToken: [5, 5], + pendingDepositWei: [5, 5], + timeout: prevStateTimeout, + txCount: [4, 1] + }) + + // This is used for both block number and block timestamp. Tests can mutate + // it to change the value returned by web3 + let curBlockTimestamp: number + + beforeEach(async () => { + const mockStore = new MockStore() + mockStore.setSyncControllerState([]) + mockStore.setChannel(prev) + mockStore.setLatestValidState(lastValid) + mockStore.setChannelUpdate({ + reason: 'ProposePendingDeposit', + txCount: 4, + args: {} as any, + sigHub: '0xsig-hub', + }) + + // update web3 functions to return mocked values + const mocked = new MockWeb3() + + connext = new MockConnextInternal({ + user, + store: mockStore.createStore(), + }) + + // stub out block times + // TODO: fix mock web3 to handle provider better + connext.opts.web3.eth.getBlockNumber = async () => { + return curBlockTimestamp + } + connext.opts.web3.eth.getBlock = async () => { + return { + number: curBlockTimestamp, + timestamp: curBlockTimestamp, + } as any + } + }) + + afterEach(() => { + return connext.stop() + }) + + parameterizedTests([ + { + name: 'invalidation should work', + invalidates: true, + }, + + { + name: 'should not invalidate until the timeout has expired', + curBlockTimestamp: prevStateTimeout - 100, + invalidates: false, + }, + + + { + name: 'should not invalidate if the event has been broadcast to chain', + eventTxCounts: [prev.txCountGlobal, prev.txCountChain], + invalidates: false, + }, + + { + name: 'should invalidate if chain event does not match', + eventTxCounts: [prev.txCountGlobal - 1, prev.txCountChain - 1], + invalidates: true, + }, + + ], async _test => { + const test = { + curBlockTimestamp: prevStateTimeout + 100, + eventTxCounts: null, + ..._test, + } + + curBlockTimestamp = test.curBlockTimestamp + connext.getContractEvents = (eventName, fromBlock) => { + return !test.eventTxCounts ? [] : [ + { + returnValues: { + txCount: test.eventTxCounts, + }, + }, + ] as any + } + + await connext.start() + await new Promise(res => setTimeout(res, 20)) + + if (test.invalidates) { + connext.mockHub.assertReceivedUpdate({ + reason: "Invalidation", + args: { + previousValidTxCount: lastValid.txCountGlobal, + lastInvalidTxCount: prev.txCountGlobal, + reason: "CU_INVALID_TIMEOUT", + } as InvalidationArgs, + sigUser: true, + sigHub: false, + }) + } else { + assert.deepEqual(connext.mockHub.receivedUpdateRequests, []) + } + }) + + afterEach(async () => { + await connext.stop() + }) +}) + +// TODO: changes were made, merged into WIP PR 12/13 +// these tests must be revisited in addition to other found bugs. +describe.skip('SyncController: unit tests (ConfirmPending)', () => { + const user = mkAddress('0xUUU') + let connext: MockConnextInternal + const mockStore = new MockStore() + + const initialChannel = getChannelState("empty", { + pendingDepositWei: [10, 10], + pendingDepositToken: [10, 10], + timeout: Math.floor(Date.now() / 1000) + 69, + txCount: [1, 1], + }) + + beforeEach(async () => { + connext = new MockConnextInternal({ user }) + // NOTE: this validator depends on web3. have it just return + // the generated state + connext.validator.generateConfirmPending = async (prev, args) => { + return new StateGenerator().confirmPending( + convertChannelState("bn", prev), + ) + } + mockStore.setChannel(initialChannel) + }) + + it('should work when hub returns a confirm pending from sync', async () => { + connext.store = mockStore.createStore() + + connext.hub.sync = (txCountGlobal: number, lastThreadUpdateId: number) => { + return [{ + type: "channel", + update: { + reason: "ConfirmPending", + sigHub: mkHash('0x9733'), + txCount: 2, + args: { transactionHash: mkHash('0x444') } + }, + }] as any + } + + await connext.start() + + // await connext.syncController.sync() + + await new Promise(res => setTimeout(res, 30)) + + connext.mockHub.assertReceivedUpdate({ + reason: 'ConfirmPending', + args: { transactionHash: mkHash('0x444') }, + sigUser: true, + sigHub: true, + }) + }).timeout(15000) + + afterEach(async () => { + await connext.stop() + }) +}) \ No newline at end of file diff --git a/modules/client/src/controllers/SyncController.ts b/modules/client/src/controllers/SyncController.ts new file mode 100644 index 0000000000..bf5873767c --- /dev/null +++ b/modules/client/src/controllers/SyncController.ts @@ -0,0 +1,575 @@ +import { UpdateRequest, ChannelState, convertChannelState, InvalidationArgs, Sync } from '../types' +import { assertUnreachable } from '../lib/utils' +import { Block } from 'web3/eth/types' +import { ChannelStateUpdate, SyncResult, InvalidationReason } from '../types' +import { Poller } from '../lib/poller/Poller' +import { ConnextInternal } from '../Connext' +import { SyncControllerState, CHANNEL_ZERO_STATE } from '../state/store' +import getTxCount from '../lib/getTxCount' +import { getLastThreadId } from '../lib/getLastThreadId' +import { AbstractController } from './AbstractController' +import * as actions from '../state/actions' +import { maybe, Lock } from '../lib/utils' +import Semaphore = require('semaphore') +import { getChannel } from '../lib/getChannel'; +import { EventLog } from 'web3/types' +import { hasPendingOps } from '../hasPendingOps' + +function channelUpdateToUpdateRequest(up: ChannelStateUpdate): UpdateRequest { + return { + id: up.id, + reason: up.reason, + args: up.args, + txCount: up.state.txCountGlobal, + sigHub: up.state.sigHub, + sigUser: up.state.sigUser, + } +} + +export function mergeSyncResults(xs: SyncResult[], ys: SyncResult[]): SyncResult[] { + let sorted = [...xs, ...ys] + sorted.sort((a, b) => { + // When threads are enabled, we'll need to make sure they are being + // sorted correctly with respect to channel updates. See comments in + // the hub's algorithm which sorts sync results. + if (a.type == 'thread' || b.type == 'thread') + throw new Error('TODO: REB-36 (enable threads)') + + // Always sort the current single unsigned state from the hub last + if (!a.update.txCount) + return 1 + + if (!b.update.txCount) + return -1 + + return a.update.txCount - b.update.txCount + }) + + // Filter sorted to ensure there is just one update with a null txCount + let hasNull = false + sorted = sorted.filter(s => { + if (s.type == 'thread') + throw new Error('TODO: REB-36 (enable threads)') + + if (!s.update.txCount) { + if (hasNull) + return false + hasNull = true + return true + } + + return true + }) + + // Dedupe updates by iterating over the sorted updates and ignoring any + // duplicate txCounts with identical signatures. + const deduped = sorted.slice(0, 1) + for (let next of sorted.slice(1)) { + const cur = deduped[deduped.length - 1] + + if (next.type == 'thread' || cur.type == 'thread') + throw new Error('TODO: REB-36 (enable threads)') + + if (next.update.txCount && next.update.txCount < cur.update.txCount!) { + throw new Error( + `next update txCount should never be < cur: ` + + `${JSON.stringify(next.update)} >= ${JSON.stringify(cur.update)}` + ) + } + + if (!next.update.txCount || next.update.txCount > cur.update.txCount!) { + deduped.push(next) + continue + } + + // The current and next updates both have the same txCount. Double check + // that they both match (they *should* always match, because if they + // don't it means that the hub has sent us two different updates with the + // same txCount, and that is Very Bad. But better safe than sorry.) + const nextSigs = next.update + const curSigs = cur.update + const nextAndCurMatch = ( + next.update.reason == cur.update.reason && + ((nextSigs.sigHub && curSigs.sigHub) ? nextSigs.sigHub == curSigs.sigHub : true) && + ((nextSigs.sigUser && curSigs.sigUser) ? nextSigs.sigUser == curSigs.sigUser : true) + ) + if (!nextAndCurMatch) { + throw new Error( + `Got two updates from the hub with the same txCount but different ` + + `reasons or signatures: ${JSON.stringify(next.update)} != ${JSON.stringify(cur.update)}` + ) + } + + // If the two updates have different sigs (ex, the next update is the + // countersigned version of the prev), then keep both + if (nextSigs.sigHub != cur.update.sigHub || nextSigs.sigUser != cur.update.sigUser) { + deduped.push(next) + continue + } + + // Otherwise the updates are identical; ignore the "next" update. + } + + return deduped +} + + +export function filterPendingSyncResults(fromHub: SyncResult[], toHub: UpdateRequest[]) { + // De-dupe incoming updates and remove any that are already in queue to be + // sent to the hub. This is done by removing any incoming updates which + // have a corresponding (by txCount, or id, in the case of unsigned + // updates) update and fewer signatures than the update in queue to send to + // the hub. Additionally, if there is an invalidation in the queue of updates + // to be sent, the corresponding incoming update will be ignored. + const updateKey = (u: UpdateRequest) => u.id && u.id < 0 ? `unsigned:${u.id}` : `tx:${u.txCount}` + + const existing: { [key: string]: { sigHub: boolean, sigUser: boolean } } = {} + toHub.forEach(u => { + existing[updateKey(u)] = { + sigHub: !!u.sigHub, + sigUser: !!u.sigUser, + } + + if (u.reason == 'Invalidation') { + const args: InvalidationArgs = u.args as InvalidationArgs + for (let i = args.previousValidTxCount + 1; i <= args.lastInvalidTxCount; i += 1) { + existing[`tx:${i}`] = { + sigHub: true, + sigUser: true, + } + } + } + }) + + return fromHub.filter(u => { + if (u.type != 'channel') + throw new Error('TODO: REB-36 (enable threads)') + + const cur = existing[updateKey(u.update)] + if (!cur) + return true + + if (cur.sigHub && !u.update.sigHub) + return false + + if (cur.sigUser && !u.update.sigUser) + return false + + return true + }) +} + + +export default class SyncController extends AbstractController { + static POLLER_INTERVAL_LENGTH = 2 * 1000 + + private poller: Poller + + private flushErrorCount = 0 + + constructor(name: string, connext: ConnextInternal) { + super(name, connext) + this.poller = new Poller({ + name: 'SyncController', + interval: SyncController.POLLER_INTERVAL_LENGTH, + callback: this.sync.bind(this), + timeout: 5 * 60 * 1000, + }) + + } + + async start() { + await this.poller.start() + } + + async stop() { + this.poller.stop() + } + + async sync() { + try { + const state = this.store.getState() + const hubSync = await this.hub.sync( + state.persistent.channel.txCountGlobal, + getLastThreadId(this.store), + ) + if (!hubSync) { + console.log('No updates found from the hub to sync') + return + } + this.handleHubSync(hubSync) + } catch (e) { + console.error('Sync error:', e) + this.logToApi('sync', { message: '' + e }) + } + + try { + await this.flushPendingUpdatesToHub() + } catch (e) { + console.error('Flush error:', e) + this.logToApi('flush', { message: '' + e }) + } + + try { + await this.checkCurrentStateTimeoutAndInvalidate() + } catch (e) { + this.logToApi('invalidation-check', { message: '' + e }) + console.error('Error checking whether current state should be invalidated:', e) + } + + } + + public getSyncState(): SyncControllerState { + return this.store.getState().persistent.syncControllerState + } + + private async checkCurrentStateTimeoutAndInvalidate() { + // If latest state has a timeout, check to see if event is mined + const state = this.getState() + + const { channel, channelUpdate } = this.getState().persistent + if (!hasPendingOps(channel)) + return + + // Wait until all the hub's sync results have been handled before checking + // if we need to invalidate (the current state might be invalid, but the + // pending updates from the hub might resolve that; ex, they might contain + // an ConfirmPending). + if (state.runtime.syncResultsFromHub.length > 0) + return + + const { didEmit, latestBlock } = await this.didContractEmitUpdateEvent(channel, channelUpdate.createdOn) + switch (didEmit) { + case 'unknown': + // The timeout hasn't expired yet; do nothing. + return + + case 'yes': + // For now, just sit tight and wait for Chainsaw to find the event. In + // the future, the client could send a `ConfirmPending` here. + return + + case 'no': + const msg = ( + `State has timed out (timestamp: ${channel.timeout} < latest block ` + + `${latestBlock.timestamp} (${latestBlock.number}/${latestBlock.hash}) and no ` + + `DidUpdateChannel events have been seen since block ${latestBlock.number - 2000}` + ) + await this.sendInvalidation(channelUpdate, 'CU_INVALID_TIMEOUT', msg) + return + + default: + assertUnreachable(didEmit) + } + } + + /** + * Checks to see whether a `DidUpdateChannel` event with `txCountGlobal` + * matching `channel.txCountGlobal` has been emitted. + * + * Returns 'yes' if it has, 'no' if it has not, and 'unknown' if the + * channel's timeout has not yet expired. + */ + public async didContractEmitUpdateEvent(channel: ChannelState, updateTimestamp?: Date): Promise<{ + didEmit: 'yes' | 'no' | 'unknown' + latestBlock: Block + event?: EventLog + }> { + let timeout = channel.timeout + if (!channel.timeout) { + if (!updateTimestamp) { + // Note: this isn't a hard or inherent limitation... but do it here for + // now to make sure we don't accidentally do Bad Things for states + // with pending operations where the timeout = 0. + throw new Error( + 'Cannot check whether the contract has emitted an event ' + + 'for a state without a timeout. State: ' + JSON.stringify(channel) + ) + } + + // If the state doesn't have a timeout, use the update's timestamp + 5 minutes + // as an approximate timeout window. + timeout = +(new Date(updateTimestamp)) / 1000 + 60 * 5 + } + + let block = await this.findBlockNearestTimeout(timeout) + if (block.timestamp < timeout) + return { didEmit: 'unknown', latestBlock: block } + + const evts = await this.connext.getContractEvents( + 'DidUpdateChannel', + Math.max(block.number - 4000, 0), // 4000 blocks = ~16 hours + ) + const event = evts.find(e => e.returnValues.txCount[0] == channel.txCountGlobal) + if (event) + return { didEmit: 'yes', latestBlock: block, event } + + return { didEmit: 'no', latestBlock: block } + } + + public async sendUpdateToHub(update: ChannelStateUpdate) { + const state = this.getSyncState() + this.store.dispatch(actions.setSyncControllerState({ + ...state, + updatesToSync: [ + ...state.updatesToSync, + channelUpdateToUpdateRequest(update), + ], + })) + this.flushPendingUpdatesToHub() + } + + /** + * If the current latest block has a `timestamp < timeout`, return the current + * latest block. Otherwise find a block with a + * `timestamp > timeout && timestamp < timeout + 60 minutes` (ie, a block + * with a timestamp greater than the timeout, but no more than 60 minutes + * greater). + */ + async findBlockNearestTimeout(timeout: number, delta = 60 * 60): Promise { + let block = await this.connext.opts.web3.eth.getBlock('latest') + if (block.timestamp < timeout + delta) + return block + + // Do a sort of binary search for a valid target block + // Start with a step of 10k blocks (~2 days) + // Precondition: + // block.timestamp >= timeout + delta + // Invariants: + // 1. block.number + step < latestBlock.number + // 2. if step < 0: block.timestamp >= timeout + delta + // 3. if step > 0: block.timestamp < timeout + delta + let step = -1 * Math.min(block.number, 10000) + while (true) { + if (Math.abs(step) <= 2) { + // This should never happen, and is a sign that we'll get into an + // otherwise infinite loop. Indicative of a bug in the code. + throw new Error( + `Step too small trying to find block (this should never happen): ` + + `target timeout: ${timeout}; block: ${JSON.stringify(block)}` + ) + } + + block = await this.connext.opts.web3.eth.getBlock(block.number + step) + if (block.timestamp > timeout && block.timestamp < timeout + delta) { + break + } + + if (block.timestamp < timeout) { + // If the current block's timestamp is before the timeout, step + // forward half a step. + step = Math.ceil(Math.abs(step) / 2) + } else { + // If the current block's timestamp is after the timeout, step + // backwards a full step. Note: we can't step backwards half a step + // because we don't know how far back we're going to need to look, + // so guarantee progress only in the "step forward" stage. + step = Math.abs(step) * -1 + } + + } + + return block + } + + /** + * Sends all pending updates (that is, those which have been put onto the + * store, but not yet sync'd) to the hub. + */ + private flushLock = Semaphore(1) + private async flushPendingUpdatesToHub() { + // Lock around `flushPendingUpdatesToHub` to make sure it doesn't get + // called by both the poller something else at the same time. + return new Promise(res => { + this.flushLock.take(async () => { + try { + await this._flushPendingUpdatesToHub() + } catch (e) { + console.error('Error flushing updates:', e) + } finally { + this.flushLock.leave() + res() + } + }) + }) + } + + private async _flushPendingUpdatesToHub() { + const state = this.getSyncState() + if (!state.updatesToSync.length) + return + + console.log(`Sending updates to hub: ${state.updatesToSync.map(u => u && u.reason)}`, state.updatesToSync) + const [res, err] = await maybe(this.hub.updateHub( + state.updatesToSync, + getLastThreadId(this.store), + )) + + let shouldRemoveUpdates = true + + if (err || res.error) { + const error = err || res.error + this.flushErrorCount += 1 + const triesRemaining = Math.max(0, 4 - this.flushErrorCount) + console.error( + `Error sending updates to hub (will flush and reset ` + + `${triesRemaining ? `after ${triesRemaining} attempts` : `now`}): ` + + `${error}` + ) + + if (triesRemaining <= 0) { + console.error( + 'Too many failed attempts to send updates to hub; flushing all of ' + + 'our updates. Updates being flushed:', + state.updatesToSync, + ) + } else { + shouldRemoveUpdates = false + } + + // If there's a bug somewhere, it can cause a loop here where the hub + // sends something bad, wallet does something bad, then immidately sends + // back to the hub... so sleep a bit to make sure we don't clobber the + // poor hub. + console.log('Sleeping for a bit before trying again...') + await new Promise(res => setTimeout(res, 6.9 * 1000)) + } else { + this.flushErrorCount = 0 + } + + // First add any new items into the sync queue... + this.enqueueSyncResultsFromHub(res.updates.updates) + + // ... then flush any pending items. This order is important to make sure + // that the merge methods work correctly. + if (shouldRemoveUpdates) { + const newState = this.getSyncState() + this.store.dispatch(actions.setSyncControllerState({ + ...newState, + updatesToSync: newState.updatesToSync.slice(state.updatesToSync.length), + })) + } + } + + /** + * Responsible for handling sync responses from the hub, specifically + * the channel status. + */ + public handleHubSync(sync: Sync) { + if (this.store.getState().runtime.channelStatus !== sync.status) { + this.store.dispatch(actions.setChannelStatus(sync.status)) + } + + // signing disabled in state update controller based on channel sync status + // unconditionally enqueue results + this.enqueueSyncResultsFromHub(sync.updates) + + // descriptive status error handling + switch (sync.status) { + case "CS_OPEN": + break + case "CS_CHANNEL_DISPUTE": + break + case "CS_THREAD_DISPUTE": + throw new Error('THIS IS BAD. Channel is set to thread dispute state, before threads are enabled. See See REB-36. Disabling client.') + default: + assertUnreachable(sync.status) + } + } + + /** + * Enqueues updates from the hub, to be handled by `StateUpdateController`. + */ + private enqueueSyncResultsFromHub(updates: SyncResult[]) { + if (updates.length === undefined) + throw new Error(`This should never happen, this was called incorrectly. An array of SyncResults should always have a defined length.`) + + if (updates.length == 0) + return + + const oldSyncResults = this.getState().runtime.syncResultsFromHub + const merged = mergeSyncResults(oldSyncResults, updates) + const filtered = filterPendingSyncResults(merged, this.getSyncState().updatesToSync) + + console.info(`updates from hub: ${updates.length}; old len: ${oldSyncResults.length}; merged: ${filtered.length}:`, filtered) + this.store.dispatch(actions.setSortedSyncResultsFromHub(filtered)) + } + + /** + * Sends an invalidation to the hub. + * + * Note: this assumes that the caller has guaranteed that the state can + * safely be invalidated. Currently this is true because `sendInvalidation` + * is only called from one place - checkCurrentStateTimeoutAndInvalidate - + * which performs the appropriate checks. + * + * If this gets called from other places, care will need to be taken to + * ensure they have done the appropriate validation too. + */ + private async sendInvalidation( + updateToInvalidate: UpdateRequest, + reason: InvalidationReason, + message: string, + ) { + console.log( + `Sending invalidation of txCount=${updateToInvalidate.txCount} ` + + `because: ${reason} (${message})` + ) + console.log(`Update being invalidated:`, updateToInvalidate) + + if (!updateToInvalidate.txCount || !updateToInvalidate.sigHub) { + console.error( + `Oops, it doesn't make sense to invalidate an unsigned update, ` + + `and requested invalidation is an unsigned update without a txCount/sigHub: `, + updateToInvalidate, + ) + return + } + + // at the moment, you cannot invalidate states that have pending + // operations and have been built on top of + const channel = getChannel(this.store) + if ( + // If the very first propose pending is invalidated, then the + // channel.txCountGlobal will be 0 + !(channel.txCountGlobal == 0 && updateToInvalidate.txCount == 1) && + updateToInvalidate.txCount < channel.txCountGlobal && + updateToInvalidate.reason.startsWith("ProposePending") + ) { + throw new Error( + `Cannot invalidate 'ProposePending*' type updates that have been built ` + + `on (channel: ${JSON.stringify(channel)}; updateToInvalidate: ` + + `${JSON.stringify(updateToInvalidate)})` + ) + } + + // If we've already signed the update that's being invalidated, make sure + // the corresponding state being invalidated (which is, for the moment, + // always going to be our current state, as guaranteed by the check above) + // has pending operations. + if (updateToInvalidate.sigUser && !hasPendingOps(channel)) { + throw new Error( + `Refusing to invalidate an update with no pending operations we have already signed: ` + + `${JSON.stringify(updateToInvalidate)}` + ) + } + + const latestValidState = this.getState().persistent.latestValidState + const args: InvalidationArgs = { + previousValidTxCount: latestValidState.txCountGlobal, + lastInvalidTxCount: updateToInvalidate.txCount, + reason, + message, + } + + const invalidationState = await this.connext.signChannelState( + this.validator.generateInvalidation(latestValidState, args) + ) + + await this.sendUpdateToHub({ + reason: 'Invalidation', + state: invalidationState, + args, + }) + } +} diff --git a/modules/client/src/controllers/ThreadsController.ts.todo b/modules/client/src/controllers/ThreadsController.ts.todo new file mode 100644 index 0000000000..33b2864d02 --- /dev/null +++ b/modules/client/src/controllers/ThreadsController.ts.todo @@ -0,0 +1,81 @@ +import { ConnextState } from '../state/store' +import { Store } from 'redux' +import { Address, convertChannelState, convertThreadState, Payment, UpdateRequest } from '../types' +import { StateGenerator } from '../StateGenerator' +import { Utils } from '../Utils' +import getAddress from '../lib/getAddress' +import { IHubAPIClient } from '../Connext' + +export default class ThreadsController { + private store: Store + + private client: IHubAPIClient + + private utils: Utils + + private web3: any + + constructor(store: Store, client: IHubAPIClient, utils: Utils, web3: any) { + this.store = store + this.client = client + this.utils = utils + this.web3 = web3 + } + + async openThread(receiver: Address, balance: Payment): Promise { + const state = this.store.getState() + const userAddress = getAddress(this.store) + const chan = convertChannelState('bn', state.persistent.channel) + const threadState = { + contractAddress: chan.contractAddress, + sender: getAddress(this.store), + receiver, + threadId: state.persistent.lastThreadId + 1, + balanceWeiReceiver: '0', + balanceTokenReceiver: '0', + balanceWeiSender: balance.amountWei, + balanceTokenSender: balance.amountToken, + txCount: 0, + } + const gen = new StateGenerator() + const channelUpdate = gen.openThread(chan, state.persistent.initialThreadStates, convertThreadState('bn-unsigned', threadState)) + const channelStateHash = this.utils.createChannelStateHash(channelUpdate) + const channelSig = await this.web3.eth.personal.sign(channelStateHash, userAddress) + const threadStateHash = this.utils.createThreadStateHash(threadState) + const threadSig = await this.web3.eth.personal.sign(threadStateHash, userAddress) + + await this.client.updateHub([ + { + reason: 'OpenThread', + args: { + ...threadState, + sigA: threadSig + }, + txCount: channelUpdate.txCountGlobal, + sigUser: channelSig, + } + ] as UpdateRequest[], state.persistent.lastThreadId) + } + + async closeThread(receiver: Address): Promise { + const state = this.store.getState() + const thread = state.persistent.threads.find((t) => (t.receiver === receiver)) + + if (!thread) { + throw new Error('No thread with that receiver found.') + } + const chan = convertChannelState('bn', state.persistent.channel) + const gen = new StateGenerator() + const channelUpdate = gen.closeThread(chan, state.persistent.initialThreadStates, convertThreadState('bn-unsigned', thread)) + const sig = await this.utils.createChannelStateHash(channelUpdate) + + await this.client.updateHub([ + { + reason: 'CloseThread', + args: thread, + txCount: chan.txCountGlobal, + sigUser: sig, + } + ] as UpdateRequest[], thread.threadId) + } +} diff --git a/modules/client/src/controllers/WithdrawalController.test.ts b/modules/client/src/controllers/WithdrawalController.test.ts new file mode 100644 index 0000000000..2cafdf9b62 --- /dev/null +++ b/modules/client/src/controllers/WithdrawalController.test.ts @@ -0,0 +1,62 @@ +import { MockStore, MockConnextInternal } from '../testing/mocks'; +import { mkAddress } from '../testing'; +import { WithdrawalParameters } from '../types'; +// @ts-ignore +global.fetch = require('node-fetch-polyfill'); + +describe('WithdrawalController: unit tests', () => { + const user = mkAddress('0xUUU') + let connext: MockConnextInternal + const mockStore = new MockStore() + + beforeEach(async () => { + connext = new MockConnextInternal() + }) + + it('should withdraw all of users tokens', async () => { + // add channel with initial booty balance to exchange and withdraw + mockStore.setChannel({ + user, + balanceWei: [10, 0], + balanceToken: [0, 50], + }) + mockStore.setExchangeRate({ 'USD': '5' }) + connext = new MockConnextInternal({ user, store: mockStore.createStore() }) + + await connext.start() + + const params: WithdrawalParameters = { + exchangeRate: '5', + recipient: mkAddress('0xRRR'), + tokensToSell: '50', + withdrawalWeiUser: '5', + weiToSell: '0', + } + + // wait to allow controller to set exchange rates + await new Promise(res => setTimeout(res, 20)) + + await connext.withdrawalController.requestUserWithdrawal(params) + await new Promise(res => setTimeout(res, 20)) + + connext.mockHub.assertReceivedUpdate({ + reason: 'ProposePendingWithdrawal', + args: { + exchangeRate: '5', + recipient: mkAddress('0xRRR'), + tokensToSell: '50', + weiToSell: '0', + targetWeiUser: '0', + targetWeiHub: '0', + targetTokenHub: '0', + additionalWeiHubToUser: '0', + }, + sigUser: true, + sigHub: false, + }) + }) + + afterEach(async () => { + await connext.stop() + }) +}) diff --git a/modules/client/src/controllers/WithdrawalController.ts b/modules/client/src/controllers/WithdrawalController.ts new file mode 100644 index 0000000000..cc76bdaecb --- /dev/null +++ b/modules/client/src/controllers/WithdrawalController.ts @@ -0,0 +1,26 @@ +import { WithdrawalParameters, convertChannelState, convertWithdrawalParameters, convertWithdrawal } from '../types' +import { AbstractController } from './AbstractController' +import { getChannel } from '../lib/getChannel' +import { validateExchangeRate, } from './ExchangeController'; +import { validateTimestamp } from '../lib/timestamp'; +import getTxCount from '../lib/getTxCount'; + +/* NOTE: the withdrawal parameters have optional withdrawal tokens and wei to + * sell values for completeness. In the BOOTY case, there is no need for the + * weiToSell or the withdrawalTokenUser, to be non zero. + * + * There is nothing in the validators or client package to prevent this, and + * this logic should be restricted at the wallet level. + * + * */ + +export default class WithdrawalController extends AbstractController { + public requestUserWithdrawal = async (withdrawalStr: WithdrawalParameters): Promise => { + const err = this.validator.withdrawalParams(convertWithdrawalParameters("bn", withdrawalStr)) + if (err) { + throw new Error(`Invalid withdrawal parameters detected: ${JSON.stringify(withdrawalStr, null, 2)}`) + } + const sync = await this.hub.requestWithdrawal(withdrawalStr, getTxCount(this.store)) + this.connext.syncController.handleHubSync(sync) + } +} diff --git a/modules/client/src/hasPendingOps.test.ts b/modules/client/src/hasPendingOps.test.ts new file mode 100644 index 0000000000..d0ec494c12 --- /dev/null +++ b/modules/client/src/hasPendingOps.test.ts @@ -0,0 +1,20 @@ +import { hasPendingOps } from './hasPendingOps' +import { assert } from './testing' + +describe('hasPendingOps', () => { + const hasPendingOpsTests = [ + [{ balanceWeiHub: '0', pendingDepositTokenHub: '0' }, false], + [{ balanceWeiHub: '1', pendingDepositTokenHub: '0' }, false], + [{ balanceWeiHub: '0', pendingDepositTokenHub: '1' }, true], + [{ balanceWeiHub: '1', pendingDepositTokenHub: '1' }, true], + ] + + hasPendingOpsTests.forEach((t: any) => { + const input = t[0] + const expected = t[1] + it(`hasPendingOps(${JSON.stringify(input)}) => ${expected}`, () => { + assert.equal(hasPendingOps(input), expected) + }) + }) +}) + diff --git a/modules/client/src/hasPendingOps.ts b/modules/client/src/hasPendingOps.ts new file mode 100644 index 0000000000..e84bec9d04 --- /dev/null +++ b/modules/client/src/hasPendingOps.ts @@ -0,0 +1,12 @@ +import { ChannelState, convertChannelState } from './types' + +export function hasPendingOps(stateAny: ChannelState) { + const state = convertChannelState('str', stateAny) + for (let field in state) { + if (!field.startsWith('pending')) + continue + if ((state as any)[field] != '0') + return true + } + return false +} diff --git a/modules/client/src/helpers/bn.ts b/modules/client/src/helpers/bn.ts new file mode 100644 index 0000000000..64f350f3bd --- /dev/null +++ b/modules/client/src/helpers/bn.ts @@ -0,0 +1,37 @@ +import BN = require('bn.js') + +export function toBN(n: string | number): BN { + return new BN(n) +} + +export function maxBN(a: BN, b: BN): BN { + return a.gte(b) ? a : b +} + +export function minBN(a: BN, ...bs: BN[]): BN { + for (let b of bs) + a = a.lte(b) ? a : b + return a +} + +// this function uses string manipulation +// to move the decimal point of the non-integer number provided +// multiplier is of base10 +// this is added because of exchange rate issues with BN lib +export function mul(num: string, multiplier: number) { + const n = Math.log10(multiplier) + const decimalPos = num.indexOf('.') === -1 ? num.length : num.indexOf('.') + const prefix = num.slice(0, decimalPos) + let istr = prefix + for (let i = 0; i < n; i++) { + if (num.charAt(i + prefix.length + 1)) { + istr += num.charAt(i + prefix.length + 1) + } else { + istr += '0' + } + } + const newPos = decimalPos + 1 + n + const suffix = num.substr(newPos) ? '.' + num.substr(newPos) : '' + istr += suffix + return istr +} diff --git a/modules/client/src/helpers/merkleTree.ts b/modules/client/src/helpers/merkleTree.ts new file mode 100644 index 0000000000..67dda6ee19 --- /dev/null +++ b/modules/client/src/helpers/merkleTree.ts @@ -0,0 +1,101 @@ +const util: any = require('ethereumjs-util') +import { MerkleUtils } from './merkleUtils' + +function combinedHash(first: any, second: any) { + if (!second) { + return first + } + if (!first) { + return second + } + // @ts-ignore + let sorted = Buffer.concat([first, second].sort(Buffer.compare)) + + // @ts-ignore + return (util as any).keccak256(sorted) +} + +function deduplicate(buffers: any[]) { + return buffers.filter((buffer, i) => { + return buffers.findIndex(e => e.equals(buffer)) === i + }) +} + +function getPair(index: number, layer: any) { + let pairIndex = index % 2 ? index - 1 : index + 1 + if (pairIndex < layer.length) { + return layer[pairIndex] + } else { + return null + } +} + +function getLayers(elements: any) { + if (elements.length === 0) { + return [[Buffer.from('')]] + } + let layers = [] + layers.push(elements) + while (layers[layers.length - 1].length > 1) { + layers.push(getNextLayer(layers[layers.length - 1])) + } + return layers +} + +function getNextLayer(elements: any[]) { + return elements.reduce((layer, element, index, arr) => { + if (index % 2 === 0) { + layer.push(combinedHash(element, arr[index + 1])) + } + return layer + }, []) +} + +export default class MerkleTree { + elements: any + root: any + layers: any + + constructor(_elements: any) { + if (!_elements.every(MerkleUtils.isHash)) { + throw new Error('elements must be 32 byte buffers') + } + const e = { elements: deduplicate(_elements) } + Object.assign(this, e) + this.elements.sort(Buffer.compare) + + const l = { layers: getLayers(this.elements) } + Object.assign(this, l) + } + + getRoot() { + if (!this.root) { + let r = { root: this.layers[this.layers.length - 1][0] } + Object.assign(this, r) + } + return this.root + } + + verify(proof: any, element: any) { + return this.root.equals( + proof.reduce((hash: any, pair: any) => combinedHash(hash, pair), element), + ) + } + + proof(element: any) { + let index = this.elements.findIndex((e: any) => e.equals(element)) + + if (index === -1) { + throw new Error('element not found in merkle tree') + } + + return this.layers.reduce((proof: any, layer: any) => { + let pair = getPair(index, layer) + if (pair) { + proof.push(pair) + } + index = Math.floor(index / 2) + return proof + }, []) + } +} diff --git a/modules/client/src/helpers/merkleUtils.ts b/modules/client/src/helpers/merkleUtils.ts new file mode 100644 index 0000000000..74418d2805 --- /dev/null +++ b/modules/client/src/helpers/merkleUtils.ts @@ -0,0 +1,65 @@ +const Web3 = require('web3') + +export class MerkleUtils { + static getBytes = (input: any): string => { + if (Buffer.isBuffer(input)) input = '0x' + input.toString('hex') + if (66 - input.length <= 0) return Web3.utils.toHex(input) + return MerkleUtils.padBytes32(Web3.utils.toHex(input)) + } + + static marshallState = (inputs: any[]): any => { + var m = MerkleUtils.getBytes(inputs[0]) + + for (var i = 1; i < inputs.length; i++) { + let x = MerkleUtils.getBytes(inputs[i]) + m += x.substr(2, x.length) + } + return m + } + + static getCTFaddress = (_r: any): string => { + return Web3.utils.sha3(_r, { encoding: 'hex' }) + } + + static getCTFstate = (_contract: any, _signers: any, _args: any): any => { + _args.unshift(_contract) + var _m = MerkleUtils.marshallState(_args) + _signers.push(_contract.length) + _signers.push(_m) + var _r = MerkleUtils.marshallState(_signers) + return _r + } + + static padBytes32 = (data: any): string => { + // TODO: check input is hex / move to TS + let l = 66 - data.length + + let x = data.substr(2, data.length) + + for (var i = 0; i < l; i++) { + x = 0 + x + } + return '0x' + x + } + + static rightPadBytes32 = (data: any): string => { + let l = 66 - data.length + + for (var i = 0; i < l; i++) { + data += 0 + } + return data + } + + static hexToBuffer = (hexString: string): Buffer => { + return new Buffer(hexString.substr(2, hexString.length), 'hex') + } + + static bufferToHex = (buffer: Buffer): string => { + return '0x' + buffer.toString('hex') + } + + static isHash = (buffer: Buffer): Boolean => { + return buffer.length === 32 && Buffer.isBuffer(buffer) + } +} diff --git a/modules/client/src/helpers/naming.ts b/modules/client/src/helpers/naming.ts new file mode 100644 index 0000000000..b3f70bb6c5 --- /dev/null +++ b/modules/client/src/helpers/naming.ts @@ -0,0 +1,3 @@ +export function capitalize(str: string): string { + return str.substring(0, 1).toUpperCase() + str.substring(1) +} \ No newline at end of file diff --git a/modules/client/src/helpers/networking.ts b/modules/client/src/helpers/networking.ts new file mode 100644 index 0000000000..2ae93f8e1e --- /dev/null +++ b/modules/client/src/helpers/networking.ts @@ -0,0 +1,64 @@ +export const GET = 'GET' +export const POST = 'POST' + +export class Networking { + baseUrl: string + + constructor(baseUrl: string) { + this.baseUrl = baseUrl + } + + get = (url: string) => { + return this.request(url, GET) + } + + post = (url: string, body: any) => { + return this.request(url, POST, body) + } + + request = async (url: string, method: any, body?: any) => { + // TO DO: better type + const opts = { + method, + } as any + + let res + if (method === POST) { + opts.body = JSON.stringify(body) + opts.headers = { + 'Content-Type': 'application/json', + } + } + opts.mode = 'cors' + opts.credentials = 'include' + res = await fetch(`${this.baseUrl}/${url}`, opts) + + if (res.status < 200 || res.status > 299) { + throw errorResponse( + res.status, + res.body, + `Received non-200 response: ${res.status}`, + ) + } + + if (res.status === 204) { + return { + data: null, + } + } + + const data = await res.json() + + return { + data, + } + } +} + +export const errorResponse = (status: number, body: any, message: string) => { + return { + status, + body, + message, + } +} diff --git a/modules/client/src/lib/Logger.ts b/modules/client/src/lib/Logger.ts new file mode 100644 index 0000000000..dda81516b6 --- /dev/null +++ b/modules/client/src/lib/Logger.ts @@ -0,0 +1,4 @@ +export default interface Logger { + source: string + logToApi(key: string, data: any): Promise +} diff --git a/modules/client/src/lib/constants.ts b/modules/client/src/lib/constants.ts new file mode 100644 index 0000000000..1d22d0cf85 --- /dev/null +++ b/modules/client/src/lib/constants.ts @@ -0,0 +1,44 @@ +import BN = require('bn.js') +import toFinney from './web3/toFinney' +import { CurrencyType } from '../state/ConnextState/CurrencyTypes' + +// !!! WARNING !!! +// There is a duplicate of this file in vynos/vynos/lib/constants.ts +// Some fields are used in one, some of the fields are used in the other +// This needs to be cleaned up! Please clean this up! +// !!! WARNING !!! + +// TODO string these +export const GWEI = new BN('1000000000') +export const FINNEY = toFinney(1) +//export const FIVE_FINNEY = toFinney(5) +//export const TEN_FINNEY = toFinney(10) +//export const FIFTEEN_FINNEY = toFinney(15) +//export const TWENTY_FINNEY = toFinney(20) +export const ETHER = toFinney(1000) +export const OPEN_CHANNEL_GAS = new BN('600000') +export const CLOSE_CHANNEL_GAS = new BN('750000') +export const DEPOSIT_GAS = new BN('300000') +export const RESERVE_GAS_PRICE = new BN('25') +export const OPEN_CHANNEL_COST = GWEI.mul(RESERVE_GAS_PRICE).mul(OPEN_CHANNEL_GAS) +const actionsBeforeRefill = new BN(2) +export const RESERVE_BALANCE = actionsBeforeRefill.mul(OPEN_CHANNEL_COST) +//export const INITIAL_DEPOSIT_WEI = TEN_FINNEY +export const INITIAL_DEPOSIT_BEI = (new BN('69')).mul(new BN('100000000000000000')) +export const ZERO = new BN(0) +export const WEI_PER_ETH = new BN('1000000000000000000') +export const WEI_AMOUNT = '1000000000000000000' +export const BEI_AMOUNT = '1000000000000000000' +export const FINNEY_AMOUNT = '1000' +// TODO string these +export const BOOTY = { + amount: '1000000000000000000', + type: CurrencyType.BEI, +} + +//export const SIXTY_NINE_BOOTY = { +// amount: '69000000000000000000', +// type: CurrencyType.BEI, +//} + +export const EMPTY_ROOT_HASH = '0x0000000000000000000000000000000000000000000000000000000000000000' diff --git a/modules/client/src/lib/currency/.CurrencyConvertable.ts.swo b/modules/client/src/lib/currency/.CurrencyConvertable.ts.swo new file mode 100644 index 0000000000000000000000000000000000000000..6d3afd5f056aba3414d3c4299cdf074684fe4411 GIT binary patch literal 12288 zcmeI2&2QX97{;d*C=^0b3!M0v0P1?BV7DNlDq@kkVJjBIHqEA#P_@YGog@bDco~nA zR7pW9CysE06GCu6;)*zd;2-Frhy&t)IPfo&9uPb;<6c5Odi?j(_wPANF2731dK|GeCREFg$K0wPCvFr6ezar1w=zSo zKlb?>hzhwU*f>tSz#k={WL^-3j7u*Offt1XFGz)8JUHjgWnboEj)bH?h?pyr9dBkE zEdfiQB#_GGCyq>!r=C1~NL_sF(P?^M|H-o5ezOED0ZYIVummgtOTZGa1T2C7Jpno1 zMc&3JZ^=g5$v$^o^_iX84@bK` zZb$O?|9}4X|HXZTd=9+_N$3RRKtJC@$cNAe&{@cXTx3XS9#5A|=HwgFOeJPYrBryS@lEcg z>A=tPO3VKI(cX&_Tk7aqipRRWiQN#}>nzpmPW=UXh^={b7N4IuHN@7uPWGRh*bA{W zFJ*LdE3Cd`vem0LwAs%2rp^|=E_rBFN8Y3=m5A5%GoT|xESyGO{Y;&uJeD+Ja=M{% zRfpO()7r~y6m`widVLY2$Q>3`p+@~wJ#?zI2F=av6V)L}$4oe?l+9!h7Zs?DMH)zC zrrOHQrZz_2bC$MyTg3xqE0H4PD_dMYUS{g)%wh|wn|7*&KPweu5la?GHoSol!ZyY+ zs=|;h`)MR;5cx?`6ozUgU>lP0VcDkYLO9aFoAos4dAf|}A8oj { + it('should return formatted currency', () => { + const c = new Currency(CurrencyType.USD, 105.70) + + assert.equal(c.format({ + decimals: 2, + withSymbol: true, + showTrailingZeros: true, + }), '$105.70') + + assert.equal(c.format({ + decimals: 0, + withSymbol: true, + showTrailingZeros: false, + }), '$106') + + assert.equal(c.format({}), '$105.70') + }) + + it('Currency.equals should determine if ICurrencies are equal', () => { + const convertable = new CurrencyConvertable(CurrencyType.BOOTY, 69, (() => {}) as any) + const currency = Currency.BOOTY(69) + const iCurrency = {type: CurrencyType.BOOTY, amount: '69'} + + expect( + Currency.equals(convertable, currency) && + Currency.equals(convertable, iCurrency) && + Currency.equals(currency, iCurrency) + ).eq(true) + }) + + it('Currency.equals should determine if ICurrencies are not equal', () => { + const convertable = new CurrencyConvertable(CurrencyType.BOOTY, 69, (() => {}) as any) + const currency = Currency.BOOTY(420) + const iCurrency = {type: CurrencyType.BOOTY, amount: '0'} + + expect( + Currency.equals(convertable, currency) || + Currency.equals(convertable, iCurrency) || + Currency.equals(currency, iCurrency) + ).eq(false) + }) + + it('Currency.floor should take the floor of a currency', () => { + expect( + Currency.BOOTY(69.69) + .floor() + .amount + ).eq('69') + }) +}) diff --git a/modules/client/src/lib/currency/Currency.ts b/modules/client/src/lib/currency/Currency.ts new file mode 100644 index 0000000000..08cd391f4b --- /dev/null +++ b/modules/client/src/lib/currency/Currency.ts @@ -0,0 +1,197 @@ +import { CurrencyType } from '../../state/ConnextState/CurrencyTypes' +import { BigNumber } from 'bignumber.js' +import BN = require('bn.js') + +export interface CurrencyFormatOptions { + decimals?: number + withSymbol?: boolean + showTrailingZeros?: boolean + takeFloor?: boolean +} + +export interface ICurrency { + type: ThisType, + amount: string +} + +export type CmpType = 'lt' | 'lte' | 'gt' | 'gte' | 'eq' + +export default class Currency implements ICurrency { + static typeToSymbol: { [key: string]: string } = { + [CurrencyType.USD]: '$', + [CurrencyType.ETH]: 'ETH', + [CurrencyType.WEI]: 'WEI', + [CurrencyType.FINNEY]: 'FIN', + [CurrencyType.BOOTY]: 'BOO', + [CurrencyType.BEI]: 'BEI' + } + + static ETH = (amount: BN | BigNumber | string | number) => new Currency(CurrencyType.ETH, amount) + static USD = (amount: BN | BigNumber | string | number) => new Currency(CurrencyType.USD, amount) + static WEI = (amount: BN | BigNumber | string | number) => new Currency(CurrencyType.WEI, amount) + static FIN = (amount: BN | BigNumber | string | number) => new Currency(CurrencyType.FINNEY, amount) + // static SPANK = (amount: BN|BigNumber|string|number): Currency => new Currency(CurrencyType.SPANK, amount) + static BOOTY = (amount: BN | BigNumber | string | number) => new Currency(CurrencyType.BOOTY, amount) + static BEI = (amount: BN | BigNumber | string | number) => new Currency(CurrencyType.BEI, amount) + + static equals = (c1: ICurrency, c2: ICurrency) => { + return c1.amount === c2.amount && c1.type == c2.type + } + + + private _type: ThisType + private _amount: BigNumber + + static _defaultOptions = { + [CurrencyType.USD]: { + decimals: 2, + withSymbol: true, + showTrailingZeros: true + } as CurrencyFormatOptions, + [CurrencyType.ETH]: { + decimals: 3, + withSymbol: true, + showTrailingZeros: true + } as CurrencyFormatOptions, + [CurrencyType.WEI]: { + decimals: 0, + withSymbol: true, + showTrailingZeros: false + } as CurrencyFormatOptions, + [CurrencyType.FINNEY]: { + decimals: 0, + withSymbol: true, + showTrailingZeros: false + } as CurrencyFormatOptions, + [CurrencyType.BOOTY]: { + decimals: 2, + withSymbol: false, + showTrailingZeros: false + } as CurrencyFormatOptions, + [CurrencyType.BEI]: { + decimals: 0, + withSymbol: true, + showTrailingZeros: false + } as CurrencyFormatOptions + } + + constructor (currency: ICurrency); + constructor (type: ThisType, amount: BN | BigNumber | string | number); + + constructor (...args: any[]) { + let [_type, _amount] = ( + args.length == 1 ? [args[0].type, args[0].amount] : args + ) + + this._type = _type + + const _amountAny = _amount as any + + try { + if (_amountAny instanceof BigNumber) { + this._amount = _amountAny + } else if (_amountAny.c && _amountAny.e && _amountAny.s) { + const b = new BigNumber('0') as any + b.c = _amountAny.c + b.e = _amountAny.e + b.s = _amountAny.s + this._amount = b + } else if (BN.isBN(_amountAny)) { + this._amount = new BigNumber(_amount.toString(10)) + } else if (typeof _amount === 'string' || typeof _amount === 'number') { + this._amount = new BigNumber(_amount) + } else { + throw new Error('incorrect type') + } + } catch (e) { + throw new Error(`Invalid amount: ${_amount} amount must be BigNumber, string, number or BN (original error: ${e})`) + } + } + + get type (): ThisType { + return this._type + } + + get symbol (): string { + return Currency.typeToSymbol[this._type] as string + } + + get currency (): ICurrency { + return { + amount: this.amount, + type: this.type + } + } + + get amount (): string { + return this._amount.toString(10) + } + + get amountBigNumber (): BigNumber { + return this._amount + } + + get amountBN (): BN { + return new BN(this._amount.decimalPlaces(0).toString(10)) + } + + public toFixed(): string { + return this.amount.replace(/\..*$/, '') + } + + public getDecimalString = (decimals?: number) => this.format({ + decimals, + showTrailingZeros: true, + withSymbol: false + }) + + public format = (_options?: CurrencyFormatOptions): string => { + const options: CurrencyFormatOptions = { + ...Currency._defaultOptions[this._type] as any, + ..._options || {} + } + + const symbol = options.withSymbol ? `${this.symbol}` : `` + + let amountBigNum = this._amount + if (options.takeFloor) { + amountBigNum = new BigNumber(amountBigNum.dividedToIntegerBy(1) ) + } + + let amount = options.decimals === undefined + ? amountBigNum.toString(10) + : amountBigNum.toNumber().toFixed(options.decimals) + + if (!options.showTrailingZeros) { + amount = parseFloat(amount).toString() + } + + return `${symbol}${amount}` + } + + public floor = (): Currency => { + return new Currency( + this.type, + this.amountBigNumber.dividedToIntegerBy(1) + ) + } + + public toString (): string { + return this.format() + } + + public compare (cmp: CmpType, b: Currency | string, bType?: CurrencyType): boolean { + if (typeof b == 'string') + b = new Currency(bType || this._type, b) as Currency + + if (this.type != b.type) { + throw new Error( + `Cannot compare incompatible currency types ${this.type} and ${b.type} ` + + `(amounts: ${this.amount}, ${b.amount})` + ) + } + + return this.amountBigNumber[cmp](b.amountBigNumber) + } + +} diff --git a/modules/client/src/lib/currency/CurrencyConvertable.test.ts b/modules/client/src/lib/currency/CurrencyConvertable.test.ts new file mode 100644 index 0000000000..c6cf4c8774 --- /dev/null +++ b/modules/client/src/lib/currency/CurrencyConvertable.test.ts @@ -0,0 +1,104 @@ +import { expect } from 'chai' +import * as redux from 'redux' +import CurrencyConvertable from './CurrencyConvertable' +import BN = require('bn.js') +import Currency from './Currency'; +import { default as generateExchangeRates } from '../../testing/generateExchangeRates' +import { default as getExchangeRates } from '../getExchangeRates' +import { ConnextState } from '../../state/store' +import { reducers } from '../../state/reducers' +import { CurrencyType } from '../../state/ConnextState/CurrencyTypes' +import { BigNumber } from 'bignumber.js' +import { MockConnextInternal, MockStore } from '../../testing/mocks'; +import toFinney from '../web3/toFinney'; + +describe('CurrencyConvertable', () => { + + const mockStore = new MockStore() + mockStore.setExchangeRate(generateExchangeRates('420')) + + const store = mockStore.createStore() + + it('should convert to a new currency with the CurrencyConvertable.to method', () => { + const eth = new CurrencyConvertable(CurrencyType.ETH, '100', () => getExchangeRates(store.getState())) + const usd = eth.to(CurrencyType.USD) + + expect(usd.amount).to.equal('42000') + expect(usd.type).to.equal(CurrencyType.USD) + }) + + it('should not change amount if converting to the same currency', () => { + const eth = new CurrencyConvertable(CurrencyType.ETH, '100', () => getExchangeRates(store.getState())) + const eth2 = eth.toETH() + + expect(eth.amount).equals(eth2.amount) + expect(eth.type).equals(eth2.type) + }) + + describe('currency convertable should not lose precision when converting numbers with under 64 significant digits', () => { + const testCases = [ + 'normal number with 64 significant digits', + '64 significant digit number with mostly 0s', + '64 digits of all 9s', + '64 digits all 5s', + '64 digits mostly 0s with a 5 at end', + 'another normal number', + 'purely decimal number', + 'number with a ton of leading 0s', + ] + const bigStrings = [ + '69696969696969696969696969696969696969696969.6969696969696966969', + '10000000000000000000000000000000000000000000.0000000000000000001', + '99999999999999999999999999999999999999999999.9999999999999999999', + '55555555555555555555555555555555555555555555.5555555555555555555', + '50000000000000000000000000000000000000000000.0000000000000000005', + '42042042042042042042042042042042042042042042.0420420420420420069', + '0.69696969699696252075295295349234952495023592540952590235925999', + '69696969696969696969696942069694295423952969696969696996962520700000000000000000000000000000000000000000000000000000', + '0.00000000000000000000000000000000000000000000000000000696969696969696969696969420696942954239529696969696969969625207', + ] + const bigNums = bigStrings.map(bigString => new BigNumber(bigString)) + const bnTomfoolery = bigStrings.map(bigString => new BN(bigString)) + + type TestCase = BigNumber | string | BN + + + function testIt(tc: TestCase) { + const eth = new CurrencyConvertable(CurrencyType.ETH, tc, () => getExchangeRates(store.getState())) + const eth2 = eth//.toBEI().toETH().toETH().toBEI().toWEI().toFIN().toBOOTY().toFIN().toUSD().toETH().toBEI().toWEI().toBOOTY().toETH().toFIN().toUSD().toWEI().toETH().toBOOTY().toETH().toFIN().toBEI().toBOOTY().toBEI().toETH() + + expect(Currency.equals(eth2, eth)).equals(true) + + expect(eth.type).equals(eth2.type) + + expect(eth.amount).equals(eth2.amount) + expect(eth.amountBN.eq(eth2.amountBN)).equals(true) + + expect(eth.amountBigNumber.minus(eth2.amountBigNumber).eq(0)) + expect(eth.amountBigNumber.eq(eth2.amountBigNumber)).equals(true) + + if (tc instanceof BN) { + expect(tc.eq(eth2.amountBN)).equals(true) + } + + if (tc instanceof BigNumber) { + expect(tc.eq(eth2.amountBigNumber)).equals(true) + expect(tc.eq(eth2.amount)).equals(true) + } + + BigNumber.config({ DECIMAL_PLACES: 200 }) + + if (typeof tc === 'string') { + expect(eth2.amount).equals(tc) + } + } + + for (let i = 0; i < testCases.length; i++) { + const numberType = testCases[i] + + it('should not lose precision with string amounts: ' + numberType, () => testIt(bigStrings[i])) + it('should not lose precision with BN amounts: ' + numberType, () => testIt(bnTomfoolery[i])) + it('should not lose precision with BigNumber amounts: ' + numberType, () => testIt(bigNums[i])) + } + }) +}) diff --git a/modules/client/src/lib/currency/CurrencyConvertable.ts b/modules/client/src/lib/currency/CurrencyConvertable.ts new file mode 100644 index 0000000000..47631b3a2e --- /dev/null +++ b/modules/client/src/lib/currency/CurrencyConvertable.ts @@ -0,0 +1,89 @@ +import { ConnextStore } from '../../state/store' +import Currency from './Currency' +import {Store} from 'redux' +import BN = require('bn.js') +import { BEI_AMOUNT, WEI_AMOUNT } from '../constants' +import { BigNumber } from 'bignumber.js' +import { CurrencyType } from '../../state/ConnextState/CurrencyTypes' +import { ExchangeRates } from '../../state/ConnextState/ExchangeRates' + +export default class CurrencyConvertable extends Currency { + protected exchangeRates: () => ExchangeRates + + constructor(type: CurrencyType, amount: BN|BigNumber|string|number, exchangeRates: () => ExchangeRates) { + super(type, amount) + this.exchangeRates = () => { + const rates = exchangeRates() + if (!rates) { + return { } + } + return rates + } + } + + public to = (toType: CurrencyType): CurrencyConvertable => this._convert(toType) + public toUSD = (): CurrencyConvertable => this._convert(CurrencyType.USD) + public toETH = (): CurrencyConvertable => this._convert(CurrencyType.ETH) + public toWEI = (): CurrencyConvertable => this._convert(CurrencyType.WEI) + public toFIN = (): CurrencyConvertable => this._convert(CurrencyType.FINNEY) + // public toSPANK = (): CurrencyConvertable => this._convert(CurrencyType.SPANK) + public toBOOTY = (): CurrencyConvertable => this._convert(CurrencyType.BOOTY) + public toBEI = (): CurrencyConvertable => this._convert(CurrencyType.BEI) + + public getExchangeRate = (currency: 'USD'): string => { + const rate = this.exchangeRates().USD + if (!rate) + throw new Error('No exchange rate for USD! Have: ' + JSON.stringify(this.exchangeRates())) + return rate.toString() + } + + private _convert = (toType: CurrencyType): CurrencyConvertable => { + if (this.type === toType) { + return this + } + + if (!this.amountBigNumber.gt(new BigNumber(0))) { + return new CurrencyConvertable( + toType, + this.amountBigNumber, + this.exchangeRates + ) + } + + if (this.type === CurrencyType.BEI && toType === CurrencyType.BOOTY) { + const amountInBootyBigNumber = this.amountBigNumber.div(new BigNumber(BEI_AMOUNT)) + return new CurrencyConvertable( + toType, + amountInBootyBigNumber, + this.exchangeRates, + ) + } + + if (this.type === CurrencyType.BOOTY && toType === CurrencyType.BEI) { + const amountInBeiBigNumber = this.amountBigNumber.times(new BigNumber(BEI_AMOUNT)) + return new CurrencyConvertable( + toType, + amountInBeiBigNumber, + this.exchangeRates, + ) + } + + const rates: any = this.exchangeRates() + let amountInToType = new BigNumber(0) + + if (rates[this.type] != null && rates[toType] != null) { + const typeToETH = new BigNumber(rates[this.type]) + const toTypeToETH = new BigNumber(rates[toType]) + const amountInETH = this.amountBigNumber.div(typeToETH) + + amountInToType = amountInETH.times(toTypeToETH) + } + + return new CurrencyConvertable( + toType, + amountInToType, + this.exchangeRates + ) + } +} + diff --git a/modules/client/src/lib/currency/bootyToBEI.test.ts b/modules/client/src/lib/currency/bootyToBEI.test.ts new file mode 100644 index 0000000000..0673444b75 --- /dev/null +++ b/modules/client/src/lib/currency/bootyToBEI.test.ts @@ -0,0 +1,36 @@ +import {expect} from 'chai' +import Currency from '../currency/Currency'; +import bootyToBEI from './bootyToBEI'; +import { BigNumber } from 'bignumber.js' +import BN = require('bn.js') + +describe('bootyToBEI', () => { + it('should convert ICurrency, string, number, BN, BigNumber to a Bei Currency', () => { + + const cases = [ + Currency.BOOTY(69), + 69, + '69', + new BigNumber(69), + new BN(69) + ] + + cases.forEach(bootyAmount => + expect( + Currency.equals( + bootyToBEI(bootyAmount), + Currency.BEI('69000000000000000000') + ) + ).eq(true) + ) + }) + + it('should work with decimals', () => { + expect( + Currency.equals( + bootyToBEI(69.69) , + Currency.BEI('69690000000000000000') + ) + ).eq(true) + }) +}) diff --git a/modules/client/src/lib/currency/bootyToBEI.ts b/modules/client/src/lib/currency/bootyToBEI.ts new file mode 100644 index 0000000000..2dbb78931b --- /dev/null +++ b/modules/client/src/lib/currency/bootyToBEI.ts @@ -0,0 +1,25 @@ +import { CurrencyType } from '../../state/ConnextState/CurrencyTypes' +import Currency, { ICurrency } from "./Currency"; +import BN = require('bn.js') +import { BOOTY } from "../constants"; +import { BigNumber } from 'bignumber.js' + +export default function bootyToBEI( + bootyAmount: ICurrency|number|string|BN|BigNumber +): Currency { + if ( + bootyAmount instanceof BigNumber || + typeof bootyAmount === 'number' || + typeof bootyAmount === 'string' + ) { + return _bootyToBEI(bootyAmount) + } + if (bootyAmount instanceof BN) { + return _bootyToBEI(bootyAmount.toString(10)) + } + return _bootyToBEI(bootyAmount.amount) +} + +function _bootyToBEI(booty: string|number|BigNumber): Currency { + return Currency.BEI(new BigNumber(booty).times(BOOTY.amount)) +} diff --git a/modules/client/src/lib/getChannel.ts b/modules/client/src/lib/getChannel.ts new file mode 100644 index 0000000000..2491674fa5 --- /dev/null +++ b/modules/client/src/lib/getChannel.ts @@ -0,0 +1,6 @@ +import { ConnextStore } from '../state/store' +import { ChannelState } from '../types' + +export function getChannel(store: ConnextStore): ChannelState { + return store.getState().persistent.channel +} diff --git a/modules/client/src/lib/getExchangeRates.ts b/modules/client/src/lib/getExchangeRates.ts new file mode 100644 index 0000000000..c51887a57d --- /dev/null +++ b/modules/client/src/lib/getExchangeRates.ts @@ -0,0 +1,14 @@ +import { ExchangeRates } from '../state/ConnextState/ExchangeRates' +import { ConnextState } from '../state/store' + +export const GET_EXCHANGE_RATES_ERROR = 'No exchange rates are set' + + +export default function getExchangeRates(state: ConnextState): ExchangeRates { + const rate = state.runtime.exchangeRate + if (!rate) { + return { } + } + + return rate.rates +} diff --git a/modules/client/src/lib/getLastThreadId.ts b/modules/client/src/lib/getLastThreadId.ts new file mode 100644 index 0000000000..109ba85232 --- /dev/null +++ b/modules/client/src/lib/getLastThreadId.ts @@ -0,0 +1,5 @@ +import { ConnextStore } from "../state/store" + +export function getLastThreadId(store: ConnextStore): number { + return store.getState().persistent.lastThreadId +} diff --git a/modules/client/src/lib/getTxCount.ts b/modules/client/src/lib/getTxCount.ts new file mode 100644 index 0000000000..9c9c296eed --- /dev/null +++ b/modules/client/src/lib/getTxCount.ts @@ -0,0 +1,5 @@ +import { ConnextStore } from "../state/store" + +export default function getTxCount(store: ConnextStore) { + return store.getState().persistent.channel.txCountGlobal +} diff --git a/modules/client/src/lib/getUpdateRequestTimeout.ts b/modules/client/src/lib/getUpdateRequestTimeout.ts new file mode 100644 index 0000000000..62410b15d8 --- /dev/null +++ b/modules/client/src/lib/getUpdateRequestTimeout.ts @@ -0,0 +1,11 @@ +import { ConnextStore } from '../state/store' + +export const GET_UPDATE_REQUEST_TIMEOUT_ERROR = 'No challenge period set' + +export function getUpdateRequestTimeout(store: ConnextStore): number { + const challenge = store.getState().runtime.updateRequestTimeout + if (!challenge) + throw new Error(GET_UPDATE_REQUEST_TIMEOUT_ERROR) + + return challenge +} diff --git a/modules/client/src/lib/math.test.ts b/modules/client/src/lib/math.test.ts new file mode 100644 index 0000000000..a8f682effb --- /dev/null +++ b/modules/client/src/lib/math.test.ts @@ -0,0 +1,71 @@ +import { expect } from 'chai' +import * as math from './math' + +interface TestCase { + mathMethod: 'sub' | 'add' | 'mul' | 'div' | 'eq' | 'lt' | 'lte' | 'gt' | 'gte' + furtherDescription?: string + inputs: [string, string] + expected: any +} + +describe('math', () => { + const testCases: TestCase[] = [ + { + mathMethod: 'sub', + inputs: ['3', '2'], + expected: '1', + }, + { + mathMethod: 'add', + inputs: ['3', '2'], + expected: '5', + }, + { + mathMethod: 'mul', + inputs: ['3', '2'], + expected: '6', + }, + { + mathMethod: 'div', + inputs: ['3', '2'], + expected: '1.5', + }, + { + mathMethod: 'eq', + inputs: ['3', '2'], + expected: false, + }, + { + mathMethod: 'lt', + inputs: ['3', '2'], + expected: false, + }, + { + mathMethod: 'lte', + inputs: ['2', '3'], + expected: true, + }, + { + mathMethod: 'gt', + inputs: ['3', '3'], + expected: false + }, + { + mathMethod: 'gte', + inputs: ['3', '3'], + expected: true, + }, + ] + + testCases.forEach(tc => { + describe(`math.${tc.mathMethod}`, () => { + it(`should work`, () => { + expect( + // NOTE: without casting, has err: "Expected 2 arguments, + // but got 0 or more." + (math[tc.mathMethod] as any)(...(tc.inputs)) + ).equals(tc.expected) + }) + }) + }) +}) \ No newline at end of file diff --git a/modules/client/src/lib/math.ts b/modules/client/src/lib/math.ts new file mode 100644 index 0000000000..47acabb732 --- /dev/null +++ b/modules/client/src/lib/math.ts @@ -0,0 +1,54 @@ +import { BigNumber } from 'bignumber.js' + +function big (num: string|number|BigNumber): BigNumber { + return new BigNumber(num) +} + +export function sub(num1: string, num2: string): string { + return big(num1) + .minus(num2) + .toString(10) +} + +export function add(num1: string, num2: string): string { + return big(num1) + .plus(num2) + .toString(10) +} + +export function mul(num1: string, num2: string): string { + return big(num1) + .times(num2) + .toString(10) +} + +export function div(num1: string, num2: string): string { + return big(num1) + .div(num2) + .toString(10) +} + +export function eq(num1: string, num2: string): boolean { + return big(num1) + .eq(num2) +} + +export function lt(num1: string, num2: string): boolean { + return big(num1) + .lt(num2) +} + +export function lte(num1: string, num2: string): boolean { + return big(num1) + .lte(num2) +} + +export function gt(num1: string, num2: string): boolean { + return big(num1) + .gt(num2) +} + +export function gte(num1: string, num2: string): boolean { + return big(num1) + .gte(num2) +} diff --git a/modules/client/src/lib/poller/Poller.test.ts b/modules/client/src/lib/poller/Poller.test.ts new file mode 100644 index 0000000000..263ead8ac4 --- /dev/null +++ b/modules/client/src/lib/poller/Poller.test.ts @@ -0,0 +1,67 @@ +import {expect} from 'chai' +import { sleep } from '../utils' +import {SinonFakeTimers, SinonSandbox} from 'sinon' +import { Poller } from './Poller' +const sinon = require('sinon') + +describe('Poller', () => { + let poller: Poller + let runs: number + let callback: () => Promise + + beforeEach(() => { + callback = async () => { + runs += 1 + } + poller = new Poller({ + name: 'test-poller', + interval: 2, + callback: () => callback() + }) + runs = 0 + }) + + afterEach(() => { + poller.stop() + }) + + it('should run function once every intervalLength', async () => { + await poller.start() + expect(runs).to.equal(1) + await sleep(10) + expect(runs).greaterThan(3) + }) + + it('should stop running when stop is called', async () => { + await poller.start() + expect(runs).to.equal(1) + poller.stop() + await sleep(10) + expect(runs).to.equal(1) + }) + + it('should time out', async () => { + poller = new Poller({ + name: 'test-poller', + interval: 2, + timeout: 5, + callback: () => { + runs += 1 + return new Promise((res, rej) => null) + } + }) + await poller.start() + await sleep(15) + expect(runs).greaterThan(2) + }) + + it('should handle errors in the callback', async () => { + callback = async () => { + runs += 1 + throw new Error('uhoh') + } + await poller.start() + await sleep(10) + expect(runs).greaterThan(2) + }) +}) diff --git a/modules/client/src/lib/poller/Poller.ts b/modules/client/src/lib/poller/Poller.ts new file mode 100644 index 0000000000..97d36993ae --- /dev/null +++ b/modules/client/src/lib/poller/Poller.ts @@ -0,0 +1,93 @@ +import { maybe, timeoutPromise } from '../utils' + +export type PollerOptions = { + // Name to include in log messages + name: string + + // How often the poller should be run + interval: number + + // Function to call + callback: () => Promise + + // Log an error and reset polling if callback() doesn't resolve within + // 'timeout' (deafult: no timeout) + timeout?: number +} + +/** + * General purpose poller for calling a callback at a particular interval, + * with an optional timeout: + * + * const p = new Poller({ + * name: 'my-poller', + * interval: 60 * 1000, + * callback: () => console.lock('Tick!'), + * timeout: 30 * 1000, + * }) + */ +export class Poller { + private polling = false + private timeout: any = null + + constructor( + private opts: PollerOptions, + ) {} + + public start() { + const { opts } = this + if (this.polling) { + throw new Error(`Poller ${opts.name} was already started`) + } + + this.polling = true + + const poll = async () => { + if (!this.polling) { + return + } + + const startTime = Date.now() + const maybeRes = maybe(opts.callback()) + const [didTimeout, _] = await timeoutPromise(maybeRes, opts.timeout) + if (didTimeout) { + console.error( + `Timeout of ${(opts.timeout! / 1000).toFixed()}s waiting for callback on poller ` + + `${opts.name} to complete. ` + ( + this.polling ? + `It will be called again in ${(opts.interval / 1000).toFixed()}s.` : + `The poller has been stopped and it will not be called again.` + ) + ) + + maybeRes.then(([res, err]) => { + console.error( + `Orphaned poller callback on poller ${opts.name} returned after ` + + `${((Date.now() - startTime) / 1000).toFixed()}s`, + res || err, + ) + }) + } else { + const [_, err] = await maybeRes + if (err) + console.error(`Error polling ${opts.name}:`, err) + } + + this.timeout = setTimeout(poll, opts.interval) + } + + return poll() + } + + public stop = () => { + this.polling = false + + if (this.timeout) { + clearTimeout(this.timeout) + } + } + + public isStarted(): boolean { + return this.polling + } +} diff --git a/modules/client/src/lib/timestamp.ts b/modules/client/src/lib/timestamp.ts new file mode 100644 index 0000000000..3a8a9bbd80 --- /dev/null +++ b/modules/client/src/lib/timestamp.ts @@ -0,0 +1,21 @@ +import { ConnextStore } from '../state/store' +import { getUpdateRequestTimeout } from './getUpdateRequestTimeout'; + +export function validateTimestamp(store: ConnextStore, timeout: number) { + // timeout will be 0 for request collateral + if (timeout === 0) { + return + } + + const maxTimeout = getUpdateRequestTimeout(store) + const now = Math.floor(Date.now() / 1000) + const delta = timeout - now + const allowableClockDrift = maxTimeout * 1.5 + if (delta > maxTimeout + allowableClockDrift || delta < 0) { + // TODO: send an invalidating state back to the hub (REB-12) + return ( + `Proposed timestamp '${timeout}' is too far from now ('${now}') ` + + `by ${delta}s (with maxTimeout of '${maxTimeout}s)'` + ) + } +} diff --git a/modules/client/src/lib/utils.test.ts b/modules/client/src/lib/utils.test.ts new file mode 100644 index 0000000000..83312631e9 --- /dev/null +++ b/modules/client/src/lib/utils.test.ts @@ -0,0 +1,22 @@ +import { timeoutPromise } from './utils' +import { assert } from "../testing"; + +describe('timeoutPromise', () => { + it('should time out', async () => { + const sleeper = new Promise(res => setTimeout(res, 10)) + const [timeout, res] = await timeoutPromise(sleeper, 5) + assert.equal(timeout, true) + assert.equal(res, sleeper) + }) + + it('should resolve', async () => { + const sleeper = new Promise(res => { + setTimeout(() => res(42), 5) + }) + + const [timeout, res] = await timeoutPromise(sleeper, 10) + assert.equal(timeout, false) + assert.equal(res, 42) + }) +}) + diff --git a/modules/client/src/lib/utils.ts b/modules/client/src/lib/utils.ts new file mode 100644 index 0000000000..6a8c093537 --- /dev/null +++ b/modules/client/src/lib/utils.ts @@ -0,0 +1,257 @@ +/** + * A simple lock that can be used with async/await. + * + * For example: + * + * funcLock = Lock.released() + * + * // NOTE: this pattern is implemented by the `synchronized` decorator, below. + * async function lockedFunction() { + * await this.funcLock + * this.funcLock = new Lock() + * try { + * ... do stuff ... + * } finally { + * this.funcLock.release() + * } + * } + * + */ +export class Lock implements PromiseLike { + _resolve: (arg?: T) => void + _p: Promise + + then: any + catch: any + + constructor() { + this._resolve = null as any + this._p = new Promise(res => this._resolve = res) + this.then = this._p.then.bind(this._p) + this.catch = this._p.catch.bind(this._p) + } + + static released() { + return new Lock().release() + } + + release(val?: T) { + this._resolve(val) + return this + } +} + +/** + * Synchronize (ie, lock so as to allow only allow one concurrent caller) a + * method. + * + * For example: + * + * class MyClass { + * + * fooLock = Lock.release() + * + * @synchronized('fooLock') + * async foo(msg: string) { + * await sleep(1000) + * console.log('msg:', msg) + * } + * } + * + * > x = new MyClass() + * > x.foo('first') + * > x.foo('second') + * ... 1 second ... + * msg: first + * ... 1 more second ... + * msg: second + */ +export function synchronized(lockName: string) { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + const oldFunc = descriptor.value + descriptor.value = async function(this: any, ...args: any[]) { + await this[lockName] + this[lockName] = new Lock() + try { + return await oldFunc.apply(this, args) + } finally { + this[lockName].release() + } + } + return descriptor + } +} + +export function isFunction(functionToCheck: any): boolean { + return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]' +} + +/** + * A simple FIFO queue. + * + * For example: + * + * > queue = new Queue([1]) + * > queue.put(2) + * > queue.length + * 2 + * > await queue.shift() + * 1 + * > queue.peek() + * 1 + * > await queue.shift() + * 1 + * > queue.peek() + * Queue.EMPTY + * + */ +export class Queue { + static readonly EMPTY: unique symbol = Symbol('Queue.EMPTY') + + protected _notEmpty = new Lock() + + protected _items: T[] + public length: number + + constructor(items?: T[]) { + this._items = [] + this.length = 0 + this.put(...(items || [])) + } + + put(...items: T[]) { + this._items = [ + ...this._items, + ...items, + ] + this.length = this._items.length + if (this.length > 0) + this._notEmpty.release() + } + + async shift(): Promise { + await this._notEmpty + this.length -= 1 + const item = this._items.shift() + if (this.length == 0) + this._notEmpty = new Lock() + return item! + } + + peek(): T | (typeof Queue)['EMPTY'] { + return this.length > 0 ? this._items[0] : Queue.EMPTY + } + +} + +/** + * A promise that exposes `resolve()` and `reject()` methods. + */ +export class ResolveablePromise implements PromiseLike { + _p: Promise + + then: any + catch: any + resolve: (arg?: T) => void + reject: (err: any) => void + + constructor() { + this.resolve = null as any + this.reject = null as any + this._p = new Promise((res, rej) => { + this.resolve = res + this.reject = rej + }) + this.then = this._p.then.bind(this._p) + this.catch = this._p.catch.bind(this._p) + } +} + +/** + * Catches any exception which might be raised by a promise and returns a + * tuple of [result, error], where either the result or the error will be + * undefined: + * + * let [res, error] = await maybe(someApi.get(...)) + * if (err) { + * return `Oh no there was an error: ${err}` + * } + * console.log('The result:', res) + * + * The result is also an object with `res` and `err` fields: + * + * let someResult = await maybe(someApi.get(...)) + * if (someResult.err) { + * return `Oh no there was an error: ${someResult.err}` + * } + * console.log('The result:', someResult.res) + * + */ +type MaybeRes = [T, any] & { res: T, err: any } +export function maybe(p: Promise): Promise> { + return (p as Promise).then( + res => Object.assign([res, null], { res, err: null }) as any, + err => Object.assign([null, err], { res: null, err }) as any, + ) +} + +/** + * Times out a promise. Waits for either: + * 1) Promise `p` to resolve. If so, `[false, T]` is returned (where T is the + * return value of `p`. + * 2) Timeout `timeout` expires. If so, `[true, p]` is returned (where `p` is + * the original promise. + * + * If timeout is false-y then `[false, T]` will be unconditionally returned. + */ +export function timeoutPromise(p: Promise, timeout: number | null | undefined): Promise< + [false, T] | + [true, Promise] +> { + + if (!timeout) { + return p.then(res => [false, res]) as any + } + + let toClear: any + const res = Promise.race([ + p.then(res => [false, res]), + new Promise(res => { + toClear = setTimeout(() => { + res([true, p]) + }, timeout) + }), + ]) + res.then(null, () => null).then(() => { + toClear && clearTimeout(timeout) + }) + return res as any +} + +/** + * Used to assert at compile time that a statement is unreachable. + * + * For example: + * + * type Option = 'a' | 'b' + * + * function handleOption(o: Option) { + * if (o == 'a') + * return handleA() + * if (o == 'b') + * return handleB() + * assertUnreachable(o) + * } + */ +export function assertUnreachable(x: never): never { + throw new Error('Reached unreachable statement: ' + JSON.stringify(x)) +} + +/** + * Sleep. + * + * await sleep(1000) + */ +export function sleep(t: number) { + return new Promise(res => setTimeout(res, t)) +} diff --git a/modules/client/src/lib/web3/toFinney.test.ts b/modules/client/src/lib/web3/toFinney.test.ts new file mode 100644 index 0000000000..9a9934da28 --- /dev/null +++ b/modules/client/src/lib/web3/toFinney.test.ts @@ -0,0 +1,11 @@ +import {expect} from 'chai' +import toFinney from './toFinney' +import BN = require('bn.js') + +describe('toFinney', () => { + it('should work', () => { + expect( + toFinney(20.99).eq(new BN('20990000000000000')) + ).eq(true) + }) +}) \ No newline at end of file diff --git a/modules/client/src/lib/web3/toFinney.ts b/modules/client/src/lib/web3/toFinney.ts new file mode 100644 index 0000000000..2320832d41 --- /dev/null +++ b/modules/client/src/lib/web3/toFinney.ts @@ -0,0 +1,10 @@ +import { BigNumber } from 'bignumber.js' +import BN = require('bn.js') + +BigNumber.config({DECIMAL_PLACES: 200}) + +const FINNEY = new BigNumber('1000000000000000') // this will not work with decimal finney amounts with BN + +export default function toFinney(n: number): BN { + return new BN(FINNEY.times(n).toString(10)) +} diff --git a/modules/client/src/register/common.ts b/modules/client/src/register/common.ts new file mode 100644 index 0000000000..247e4a3df4 --- /dev/null +++ b/modules/client/src/register/common.ts @@ -0,0 +1,33 @@ +/* + * A common entrypoint for all components. + * + * Does some minimal environment configuration. + */ + +// Bluebird has the ability to include the entire call stack in a Promise (ie, +// including the original caller). +// This incurs a 4x-5x performance penalty, though, so only use it in dev + +// staging... but use Bluebird promises unconditionally to minimize the +// differences between production, staging, and dev. +global.Promise = require('bluebird') +if (process.env.NODE_ENV !== 'production') { + (Promise as any).longStackTraces() +} + +// Enable more verbose debug logging outside of production +if (process.env.NODE_ENV != 'production') { + let debug = require('debug') + debug.enable([ + '*', + '-nodemon', + '-express:application', + '-sequelize:hooks', + '-express:router*', + '-socket.io:namespace', + '-nock.*', + '-mocha:*', + '-sequelize:sql:pg', + '-sequelize:connection:pg', + '-follow-redirects', + ].join(',')) +} diff --git a/modules/client/src/register/testing.ts b/modules/client/src/register/testing.ts new file mode 100644 index 0000000000..56df6123bc --- /dev/null +++ b/modules/client/src/register/testing.ts @@ -0,0 +1,11 @@ +const _Mocha = require('mocha') + +const oldRun = _Mocha.prototype.run +_Mocha.prototype.run = function(fn: any) { + this.suite.on('pre-require', function(context: any, file: any, mocha: any) { + context.describe.only = (name: string) => { + throw new Error(`Don't use 'describe.only'! Instead, run tests with "--grep '${name}'"`) + } + }) + return oldRun.call(this, fn) +} diff --git a/modules/client/src/state/ConnextState/CurrencyTypes.ts b/modules/client/src/state/ConnextState/CurrencyTypes.ts new file mode 100644 index 0000000000..d257f05625 --- /dev/null +++ b/modules/client/src/state/ConnextState/CurrencyTypes.ts @@ -0,0 +1,10 @@ +// TODO replace enums with not enums to be consistent throughout platform +// see DW for how to do this +export enum CurrencyType { + USD = 'USD', + ETH = 'ETH', + WEI = 'WEI', + FINNEY = 'FINNEY', + BOOTY = 'BOOTY', + BEI = 'BEI', +} \ No newline at end of file diff --git a/modules/client/src/state/ConnextState/ExchangeRates.ts b/modules/client/src/state/ConnextState/ExchangeRates.ts new file mode 100644 index 0000000000..fc683a5ad3 --- /dev/null +++ b/modules/client/src/state/ConnextState/ExchangeRates.ts @@ -0,0 +1,11 @@ +import { CurrencyType } from '../ConnextState/CurrencyTypes' +import { BigNumber } from 'bignumber.js' + +export type ExchangeRates = { + [key in CurrencyType]?: string | BigNumber +} + +export interface ExchangeRateState { + lastUpdated: Date + rates: ExchangeRates +} diff --git a/modules/client/src/state/actions.test.ts b/modules/client/src/state/actions.test.ts new file mode 100644 index 0000000000..fb90a49857 --- /dev/null +++ b/modules/client/src/state/actions.test.ts @@ -0,0 +1,18 @@ +import { assert } from '../testing' +import { setterAction } from './actions' + +describe('setterAction', () => { + it('should work with a transform', () => { + const action = setterAction('foo', 'incr', (state, amount, old) => { + assert.equal(old, 1) + assert.equal(amount, 68) + return old + amount + }) + + assert.equal(action.type, 'connext/incr:foo') + + assert.deepEqual(action.handler({ foo: 1 }, 68), { + foo: 69, + }) + }) +}) diff --git a/modules/client/src/state/actions.ts b/modules/client/src/state/actions.ts new file mode 100644 index 0000000000..6d5b7e97f6 --- /dev/null +++ b/modules/client/src/state/actions.ts @@ -0,0 +1,68 @@ +import { SyncControllerState, RuntimeState } from './store' +import actionCreatorFactory, { ActionCreator } from 'typescript-fsa' +//import Wallet from 'ethereumjs-wallet' +import { ChannelState, SyncResult, Address, UpdateRequest, ChannelStatus } from '../types' +import { ConnextState } from '../state/store' +import { ExchangeRateState } from './ConnextState/ExchangeRates' + +const actionCreator = actionCreatorFactory('connext') + +export type ActionCreatorWithHandler = ActionCreator & { + handler: (...args: any[]) => any +} + +function setattr(obj: any, bits: string[], value: any): any { + if (!bits.length) + return value + return { + ...obj, + [bits[0]]: setattr(obj[bits[0]], bits.slice(1), value), + } +} + +function getattr(obj: any, bits: string[]): any { + for (let b of bits) + obj = obj[b] + return obj +} + +export type StateTransform = (state: ConnextState, payload: T, old: any) => any + +export function setterAction(attr: string, transform?: StateTransform): ActionCreatorWithHandler +export function setterAction(attr: string, action: string, transform: StateTransform): ActionCreatorWithHandler +export function setterAction(attr: string, ...args: any[]): ActionCreatorWithHandler { + const transform = args[args.length - 1] + const action = args.length == 1 ? null : args[0] + const res = actionCreator((action || 'set') + ':' + attr) as any + const bits = attr.split('.') + res.handler = (state: any, value: any) => { + if (transform) + value = transform(state, value, getattr(state, bits)) + return setattr(state, bits, value) + } + return res +} + +// Runtime +export const setExchangeRate = setterAction('runtime.exchangeRate') +export const updateCanFields = setterAction>('runtime', 'updateCanFields', (state, fields, prev) => { + return { + ...prev, + ...fields, + } +}) +export const setSortedSyncResultsFromHub = setterAction('runtime.syncResultsFromHub') +export const dequeueSyncResultsFromHub = setterAction('runtime.syncResultsFromHub', 'dequeue', (state, toRemove, prev) => { + return prev.filter((x: any) => x !== toRemove) +}) +export const setChannelStatus = setterAction('runtime.channelStatus') + +// Persistent +export const setLastThreadId = setterAction('persistent.lastThreadId') + +export type SetChannelActionArgs = { + update: UpdateRequest + state: ChannelState +} +export const setChannel = actionCreator('setChannelAndUpdate') +export const setSyncControllerState = setterAction('persistent.syncControllerState') \ No newline at end of file diff --git a/modules/client/src/state/middleware.ts b/modules/client/src/state/middleware.ts new file mode 100644 index 0000000000..cfe62184ed --- /dev/null +++ b/modules/client/src/state/middleware.ts @@ -0,0 +1,84 @@ +import { SyncResult, } from '../types' +import { ConnextState } from './store' +import * as actions from './actions' +import { Utils } from '../Utils' +import { hasPendingOps } from '../hasPendingOps' + + +export function handleStateFlags(args: any): any { + let didInitialUpdate = false + + const utils = new Utils() + const { dispatch, getState } = args + + return (next: any) => (action: any) => { + const res = next(action) + + // iterate the queued updates and set store flags accordingly + // this is to block any action which is already pending + if ( + !didInitialUpdate || + action.type === 'connext/set:runtime.syncResultsFromHub' || + action.type === 'connext/set:persistent.channel' || + action.type === 'connext/set:persistent.syncControllerState' || + action.type === 'connext/dequeue:runtime.syncResultsFromHub' + ) { + didInitialUpdate = true + + const connextState: ConnextState = getState() + const { + runtime: { + canDeposit, + canExchange, + canBuy, + canWithdraw, + syncResultsFromHub + }, + persistent: { + channel, + syncControllerState: { + updatesToSync, + }, + } + } = connextState + + let isUnsigned = false + let hasTimeout = !!channel.timeout + let hasPending = hasPendingOps(channel) + + updatesToSync.forEach(update => { + isUnsigned = isUnsigned || !(update.sigHub && update.sigUser) + hasTimeout = hasTimeout || 'timeout' in update.args ? !!(update.args as any).timeout : false + hasPending = hasPending || ( + update.reason == 'ProposePendingDeposit' || + update.reason == 'ProposePendingWithdrawal' + ) + }) + + syncResultsFromHub.forEach((result: SyncResult) => { + if (result.type != 'channel') + return + const update = result.update + + isUnsigned = isUnsigned || !(update.sigHub && update.sigUser) + hasTimeout = hasTimeout || 'timeout' in update.args ? !!(update.args as any).timeout : false + hasPending = hasPending || ( + update.reason == 'ProposePendingDeposit' || + update.reason == 'ProposePendingWithdrawal' + ) + }) + + const allBlocked = hasTimeout || isUnsigned + dispatch(actions.updateCanFields({ + canDeposit: !(allBlocked || hasPending), + canExchange: !allBlocked, + awaitingOnchainTransaction: hasPending, + canWithdraw: !(allBlocked || hasPending), + canBuy: !allBlocked, + canCollateralize: !(allBlocked || hasPending), + })) + } + + return res + } +} diff --git a/modules/client/src/state/reducers.ts b/modules/client/src/state/reducers.ts new file mode 100644 index 0000000000..8ed203c886 --- /dev/null +++ b/modules/client/src/state/reducers.ts @@ -0,0 +1,44 @@ +import { isFunction } from '../lib/utils' +import {ConnextState} from './store' +import {reducerWithInitialState, ReducerBuilder} from 'typescript-fsa-reducers/dist' +import * as actions from './actions' + +export let reducers = reducerWithInitialState(new ConnextState()) + +// Automatically add all the reducers defined in `actions` to the reducers. +// If other reducers need to be defined, they can be added explicitly like +// this: +// +// reducers = reducers.case(actionCreator('someAction'), (state, action) => { +// return { ...state, someValue: action.value } +// }) + +reducers = reducers.case(actions.setChannel, (state, action) => { + const hasPending = ( + Object.keys(action.state) + .some(field => field.startsWith('pending') && (action.state as any)[field] != '0') + ) + if (!hasPending) { + state = { + ...state, + persistent: { + ...state.persistent, + latestValidState: action.state, + }, + } + } + + return { + ...state, + persistent: { + ...state.persistent, + channel: action.state, + channelUpdate: action.update, + }, + } +}) + +for (let action of Object.values(actions) as any[]) { + if (isFunction(action && action.handler)) + reducers = reducers.case(action, action.handler) +} diff --git a/modules/client/src/state/store.ts b/modules/client/src/state/store.ts new file mode 100644 index 0000000000..735f88d5b3 --- /dev/null +++ b/modules/client/src/state/store.ts @@ -0,0 +1,94 @@ +import { ChannelUpdateReason, ChannelStatus } from '../types' +import { UpdateRequest } from '../types' +//import Wallet from 'ethereumjs-wallet' //typescript doesn't like this module, needs declaration +import { EMPTY_ROOT_HASH } from '../lib/constants' +import { Store } from 'redux' +import { ThreadState, ChannelState } from '../types' +import { SyncResult, Payment } from '../types' +import { ExchangeRateState } from './ConnextState/ExchangeRates' + +export const CHANNEL_ZERO_STATE = { + user: '0x0', + recipient: '0x0', + contractAddress: process.env.CONTRACT_ADDRESS!, + balanceWeiUser: '0', + balanceWeiHub: '0', + balanceTokenUser: '0', + balanceTokenHub: '0', + pendingDepositWeiUser: '0', + pendingDepositWeiHub: '0', + pendingDepositTokenUser: '0', + pendingDepositTokenHub: '0', + pendingWithdrawalWeiUser: '0', + pendingWithdrawalWeiHub: '0', + pendingWithdrawalTokenUser: '0', + pendingWithdrawalTokenHub: '0', + txCountGlobal: 0, + txCountChain: 0, + threadRoot: EMPTY_ROOT_HASH, + threadCount: 0, + timeout: 0, + // To maintain the invariant that the current channel is always signed, add + // non-empty signatures here. Note: this is a valid assumption because: + // 1. The signatures of the current channel state should never need to be + // checked, and + // 2. The initial state (ie, with zll zero values) is indistinguishable from + // some subsequent state which has no value (ie, user and hub have + // withdrawn their entire balance) + sigUser: '0x0', + sigHub: '0x0', +} + +export class SyncControllerState { + // Updates we need to send back to the hub + updatesToSync: UpdateRequest[] = [] +} + +export class RuntimeState { + awaitingOnchainTransaction: boolean = false + canDeposit: boolean = false + canExchange: boolean = false + canWithdraw: boolean = false + canBuy: boolean = false + canCollateralize: boolean = false + exchangeRate: null | ExchangeRateState = null + syncResultsFromHub: SyncResult[] = [] + channelStatus: ChannelStatus = "CS_OPEN" + updateRequestTimeout: number = 60 * 10 // default 10 min +} + +export class PersistentState { + channel: ChannelState = CHANNEL_ZERO_STATE + + // The update that created this channel, or an empty payment if the channel + // update is the initial. + channelUpdate: UpdateRequest = { + reason: 'Payment', + args: { + recipient: 'hub', + amountToken: '0', + amountWei: '0', + }, + txCount: 0, + sigHub: '0x0', + sigUser: '0x0', + } + + // The 'latestValidState' is the latest state with no pending operations + // which will be used by the Invalidation update (since the current channel + // might have pending operations which need to be invalidated). Set by the + // reducer in reducers. + latestValidState: ChannelState = CHANNEL_ZERO_STATE + + threads: ThreadState[] = [] + initialThreadStates: ThreadState[] = [] + lastThreadId: number = 0 + syncControllerState = new SyncControllerState() +} + +export class ConnextState { + persistent = new PersistentState() + runtime = new RuntimeState() +} + +export type ConnextStore = Store diff --git a/modules/client/src/testing/generateExchangeRates.ts b/modules/client/src/testing/generateExchangeRates.ts new file mode 100644 index 0000000000..63efe6a998 --- /dev/null +++ b/modules/client/src/testing/generateExchangeRates.ts @@ -0,0 +1,17 @@ +import { ExchangeRates } from '../state/ConnextState/ExchangeRates' +import { CurrencyType } from '../state/ConnextState/CurrencyTypes' + +type BigString = string + +// NOTE: only used in testing +// usdRate is the price of 1 ETH in USD +export default function generateExchangeRates(usdRate: BigString): ExchangeRates { + return { + [CurrencyType.USD]: usdRate, + [CurrencyType.BOOTY]: usdRate, + [CurrencyType.BEI]: usdRate + '000000000000000000', + [CurrencyType.ETH]: '1', + [CurrencyType.WEI]: '1000000000000000000', + [CurrencyType.FINNEY]: '1000', + } +} diff --git a/modules/client/src/testing/index.test.ts b/modules/client/src/testing/index.test.ts new file mode 100644 index 0000000000..5b3e03c2e2 --- /dev/null +++ b/modules/client/src/testing/index.test.ts @@ -0,0 +1,168 @@ +import * as t from './index' +import { assert } from './index' + +describe('makeSuccinctChannel', () => { + it('should work', () => { + assert.deepEqual( + t.makeSuccinctChannel({ + balanceWeiHub: '1', + balanceWeiUser: '2', + timeout: 69, + }), + { + balanceWei: ['1', '2'], + timeout: 69, + }, + ) + }) +}) + +describe('makeSuccinctThread', () => { + it('should work', () => { + assert.deepEqual( + t.makeSuccinctThread({ + balanceWeiSender: '1', + balanceWeiReceiver: '2', + }), + { + balanceWei: ['1', '2'], + }, + ) + }) +}) + +describe('makeSuccinctExchange', () => { + it('should work', () => { + assert.deepEqual( + t.makeSuccinctExchange({ + tokensToSell: '1', + weiToSell: '2', + }), + { + toSell: ['1', '2'], + }, + ) + }) +}) + +describe('expandSuccinctChannel', () => { + it('should work', () => { + assert.deepEqual( + t.expandSuccinctChannel({ + balanceWei: ['1', '2'], + timeout: 69, + }), + { + balanceWeiHub: '1', + balanceWeiUser: '2', + timeout: 69, + }, + ) + }) +}) + +describe('expandSuccinctThread', () => { + it('should work', () => { + assert.deepEqual( + t.expandSuccinctThread({ + balanceWei: ['1', '2'], + }), + { + balanceWeiSender: '1', + balanceWeiReceiver: '2', + }, + ) + }) +}) + +describe('expandSuccinctExchange', () => { + it('should work', () => { + assert.deepEqual( + t.expandSuccinctExchangeArgs({ + toSell: ['1', '0'], + }), + { + tokensToSell: '1', + weiToSell: '0', + }, + ) + }) +}) + +describe('get pending', () => { + it('should work', () => { + let args = t.getPendingArgs("empty") + + assert.deepEqual(args, { + withdrawalWeiUser: '0', + withdrawalWeiHub: '0', + withdrawalTokenUser: '0', + withdrawalTokenHub: '0', + depositTokenUser: '0', + depositWeiUser: '0', + depositWeiHub: '0', + depositTokenHub: '0', + recipient: t.mkAddress('0xRRR'), + timeout: 0 + }) + + args = t.getPendingArgs("empty", { recipient: t.mkAddress('0xDAD?') }) + + assert.containSubset(args, { recipient: t.mkAddress('0xDAD?') }) + }) +}) + +describe('assertChannelStateEqual', () => { + it('should work', () => { + let state = t.getChannelState('full', { + balanceWei: [100, 200], + }) + + t.assertChannelStateEqual(state, { + balanceWeiHub: '100', + balanceWeiUser: '200', + }) + + state = t.updateObj("channel", state, { + timeout: 69, + balanceWeiUser: 42, + balanceToken: [6, 9], + txCount: [66, 99], + }) + + t.assertChannelStateEqual(state, { + balanceWei: [100, 42], + balanceTokenHub: '6', + balanceTokenUser: '9', + timeout: 69, + txCountGlobal: 66, + txCountChain: 99, + }) + }) +}) + +describe('assertThreadStateEqual', () => { + it('should work', () => { + let state = t.getThreadState('full', { + balanceWei: [100, 200], + }) + + t.assertThreadStateEqual(state, { + balanceWeiReceiver: '200', + balanceWeiSender: '100', + }) + + state = t.updateObj("thread", state, { + balanceWeiReceiver: 42, + balanceToken: [6, 9], + txCount: 66, + }) + + t.assertThreadStateEqual(state, { + balanceWei: [100, 42], + balanceTokenReceiver: '9', + balanceTokenSender: '6', + txCount: 66, + }) + }) +}) \ No newline at end of file diff --git a/modules/client/src/testing/index.ts b/modules/client/src/testing/index.ts new file mode 100644 index 0000000000..7f5d3ed5af --- /dev/null +++ b/modules/client/src/testing/index.ts @@ -0,0 +1,837 @@ +import * as chai from 'chai' +import BN = require('bn.js') +import { + Address, + ChannelState, + ThreadState, + ChannelStateUpdate, + WithdrawalArgs, + PaymentArgs, + ExchangeArgs, + ChannelUpdateReason, + DepositArgs, + ThreadStateUpdate, + ArgsTypes, + ChannelUpdateReasons, + addSigToChannelState, + PendingArgs, +} from '../types' +import { capitalize } from '../helpers/naming'; +import { StateGenerator } from '../StateGenerator'; + +// +// chai +// +chai.use(require('chai-subset')) +chai.use(require('chai-as-promised')) +export const assert = chai.assert + +/* Channel and Thread Succinct Types */ +export type SuccinctChannelState = { + contractAddress: Address + user: Address + recipient: Address + balanceWei: [T, T] + balanceToken: [T, T] + pendingDepositWei: [T, T] + pendingDepositToken: [T, T] + pendingWithdrawalWei: [T, T] + pendingWithdrawalToken: [T, T] + txCount: [number, number] + sigs: [string, string] + threadRoot: string + threadCount: number + timeout: number +} + +export type SuccinctThreadState = { + contractAddress: Address + sender: Address + receiver: Address + threadId: number, + balanceWei: [T, T] + balanceToken: [T, T] + txCount: number + sigA: string +} + +export type SignedOrSuccinctChannel = SuccinctChannelState | ChannelState + +export type SignedOrSuccinctThread = SuccinctThreadState | ThreadState + +export type PartialSignedOrSuccinctChannel = Partial< + SuccinctChannelState & ChannelState +> + +export type PartialSignedOrSuccinctThread = Partial< + SuccinctThreadState & ThreadState +> + +/* Arg Succinct Types */ +export type SuccinctDepositArgs = { + depositWei: [T, T], + depositToken: [T, T], + timeout: number, +} + +export type VerboseOrSuccinctDepositArgs = SuccinctDepositArgs | DepositArgs + +export type PartialVerboseOrSuccinctDepositArgs = Partial< + SuccinctDepositArgs & DepositArgs +> + +/* Arg Succinct Types */ +export type SuccinctPendingArgs = { + depositWei: [T, T], + depositToken: [T, T], + withdrawalWei: [T, T], + withdrawalToken: [T, T], + recipient: Address, + timeout: number, +} + +export type VerboseOrSuccinctPendingArgs = SuccinctPendingArgs | PendingArgs + +export type PartialVerboseOrSuccinctPendingArgs = Partial< + SuccinctPendingArgs & PendingArgs +> + +export type SuccinctWithdrawalArgs = SuccinctDepositArgs & { + exchangeRate: string, + tokensToSell: T, + weiToSell: T, + withdrawalWei: [T, T], + withdrawalTokenHub: T, + recipient: Address, + additionalWeiHubToUser: T, + additionalTokenHubToUser: T, +} + +export type VerboseOrSuccinctWithdrawalArgs = SuccinctWithdrawalArgs | WithdrawalArgs + +export type PartialVerboseOrSuccinctWithdrawalArgs = Partial< + SuccinctWithdrawalArgs & WithdrawalArgs +> + +export type SuccinctPaymentArgs = { + recipient: 'user' | 'hub' // | 'receiver', + amount: [T, T] // [token, wei] +} + +export type VerboseOrSuccinctPaymentArgs = SuccinctPaymentArgs | PaymentArgs + +export type PartialVerboseOrSuccinctPaymentArgs = Partial< + SuccinctPaymentArgs & PaymentArgs +> + +export type SuccinctExchangeArgs = { + exchangeRate: string, // ERC20 / ETH + seller: 'user' | 'hub', // who is initiating trade + toSell: [T, T], +} + +export type VerboseOrSuccinctExchangeArgs = SuccinctExchangeArgs | ExchangeArgs + +export type PartialVerboseOrSuccinctExchangeArgs = Partial< + SuccinctExchangeArgs & ExchangeArgs +> + +export type PartialArgsType = PartialVerboseOrSuccinctDepositArgs | + PartialVerboseOrSuccinctWithdrawalArgs | + PartialVerboseOrSuccinctPaymentArgs | + PartialVerboseOrSuccinctExchangeArgs | + PartialVerboseOrSuccinctPendingArgs + +/* Channel and Thread Functions */ +export function expandSuccinctChannel( + s: SignedOrSuccinctChannel, +): ChannelState +export function expandSuccinctChannel( + s: PartialSignedOrSuccinctChannel, +): Partial> + +export function expandSuccinctChannel( + s: SignedOrSuccinctChannel | Partial, +) { + return expandSuccinct(['Hub', 'User'], s, true) +} + +export function expandSuccinctThread( + s: SignedOrSuccinctThread, +): ThreadState + +export function expandSuccinctThread( + s: PartialSignedOrSuccinctThread, +): Partial> + +export function expandSuccinctThread( + s: SignedOrSuccinctThread | Partial, +) { + return expandSuccinct(['Sender', 'Receiver'], s) +} + +export function expandSuccinctDepositArgs( + s: VerboseOrSuccinctDepositArgs, +): DepositArgs +export function expandSuccinctDepositArgs( + s: PartialVerboseOrSuccinctDepositArgs, +): Partial> +export function expandSuccinctDepositArgs( + s: SuccinctDepositArgs | Partial, +) { + return expandSuccinct(['Hub', 'User'], s) +} + +export function expandSuccinctWithdrawalArgs( + s: VerboseOrSuccinctWithdrawalArgs, +): WithdrawalArgs +export function expandSuccinctWithdrawalArgs( + s: PartialVerboseOrSuccinctWithdrawalArgs, +): Partial> +export function expandSuccinctWithdrawalArgs( + s: SuccinctWithdrawalArgs | Partial, +) { + return expandSuccinct(['Hub', 'User'], s) +} + +export function expandSuccinctPaymentArgs( + s: VerboseOrSuccinctPaymentArgs, +): PaymentArgs +export function expandSuccinctPaymentArgs( + s: PartialVerboseOrSuccinctPaymentArgs, +): Partial> +export function expandSuccinctPaymentArgs( + s: SuccinctPaymentArgs | Partial, +) { + return expandSuccinct(['Token', 'Wei'], s) +} + +export function expandSuccinctExchangeArgs( + s: VerboseOrSuccinctExchangeArgs, +): ExchangeArgs +export function expandSuccinctExchangeArgs( + s: PartialVerboseOrSuccinctExchangeArgs, +): Partial> +export function expandSuccinctExchangeArgs( + s: SuccinctExchangeArgs | Partial, +) { + return expandSuccinct(['tokens', 'wei'], s, false, false) +} + +export function expandSuccinctPendingArgs( + s: VerboseOrSuccinctPendingArgs, +): PendingArgs +export function expandSuccinctPendingArgs( + s: PartialVerboseOrSuccinctPendingArgs, +): Partial> +export function expandSuccinctPendingArgs( + s: SuccinctPendingArgs | Partial, +) { + return expandSuccinct(['Hub', 'User'], s) +} + +/* Common */ +function expandSuccinct( + strs: string[], + s: any, + expandTxCount: boolean = false, + isSuffix: boolean = true, +) { + let res = {} as any + Object.entries(s).forEach(([name, value]) => { + if (Array.isArray(value)) { + let cast = (x: any) => x.toString() + if (expandTxCount && name == 'txCount') { + strs = ['Global', 'Chain'] + cast = (x: any) => x + } + res[isSuffix ? (name + strs[0]) : (strs[0] + capitalize(name))] = cast(value[0]) + res[isSuffix ? (name + strs[1]) : (strs[1] + capitalize(name))] = cast(value[1]) + } else { + const condition = isSuffix ? name.endsWith(strs[0]) || name.endsWith(strs[1]) : name.startsWith(strs[0]) || name.startsWith(strs[1]) + if (condition) + value = !value && value != 0 ? value : value.toString() + res[name] = value + } + }) + return res +} + +export function makeSuccinctChannel( + s: SignedOrSuccinctChannel, +): SuccinctChannelState +export function makeSuccinctChannel( + s: PartialSignedOrSuccinctChannel, +): Partial> +export function makeSuccinctChannel( + s: SignedOrSuccinctChannel | Partial, +) { + return makeSuccinct(['Hub', 'User', 'Global', 'Chain'], s) +} + +export function makeSuccinctThread( + s: SignedOrSuccinctThread, +): SuccinctThreadState +export function makeSuccinctThread( + s: PartialSignedOrSuccinctThread, +): Partial> +export function makeSuccinctThread( + s: SignedOrSuccinctThread | Partial, +) { + return makeSuccinct(['Sender', 'Receiver'], s) +} + +export function makeSuccinctPending( + s: VerboseOrSuccinctPendingArgs, +): SuccinctPendingArgs +export function makeSuccinctPending( + s: PartialVerboseOrSuccinctPendingArgs, +): Partial> +export function makeSuccinctPending( + s: VerboseOrSuccinctPendingArgs | Partial, +) { + return makeSuccinct(['Hub', 'User'], s) +} + +export function makeSuccinctDeposit( + s: VerboseOrSuccinctDepositArgs, +): SuccinctDepositArgs +export function makeSuccinctDeposit( + s: PartialVerboseOrSuccinctDepositArgs, +): Partial> +export function makeSuccinctDeposit( + s: VerboseOrSuccinctDepositArgs | Partial, +) { + return makeSuccinct(['Hub', 'User'], s) +} + +export function makeSuccinctWithdrawal( + s: VerboseOrSuccinctWithdrawalArgs, +): SuccinctWithdrawalArgs +export function makeSuccinctWithdrawal( + s: PartialVerboseOrSuccinctWithdrawalArgs, +): Partial> +export function makeSuccinctWithdrawal( + s: VerboseOrSuccinctWithdrawalArgs | Partial, +) { + return makeSuccinct(['Hub', 'User'], s) +} + +export function makeSuccinctPayment( + s: VerboseOrSuccinctPaymentArgs, +): SuccinctPaymentArgs +export function makeSuccinctPayment( + s: PartialVerboseOrSuccinctPaymentArgs, +): Partial> +export function makeSuccinctPayment( + s: VerboseOrSuccinctPaymentArgs | Partial, +) { + return makeSuccinct(['Token', 'Wei'], s) +} + +export function makeSuccinctExchange( + s: VerboseOrSuccinctExchangeArgs, +): SuccinctExchangeArgs +export function makeSuccinctExchange( + s: PartialVerboseOrSuccinctExchangeArgs, +): Partial> +export function makeSuccinctExchange( + s: VerboseOrSuccinctExchangeArgs | Partial, +) { + return makeSuccinct(['tokens', 'wei'], s, 'toSell') +} + +function makeSuccinct( + strs: string[], + s: any, + replacement: string = '', +) { + let res = {} as any + Object.entries(s).forEach(([name, value]) => { + let didMatchStr = false + strs.forEach((str, idx) => { + const condition = replacement === '' + ? name.endsWith(str) + : name.startsWith(str) + if (condition) { + const key = replacement === '' ? name.replace(str, replacement) : replacement + if (!res[name] && !res[key]) res[key] = ['0', '0'] + res[key][idx % 2] = idx < 2 ? value && value.toString() : value + didMatchStr = true + } + }) + if (!didMatchStr) res[name] = value + }) + + return res +} + +export function mkAddress(prefix: string = '0x'): Address { + return prefix.padEnd(42, '0') +} + +export function mkHash(prefix: string = '0x') { + return prefix.padEnd(66, '0') +} + +export function updateObj(type: "channel", + s: SignedOrSuccinctChannel, + ...rest: PartialSignedOrSuccinctChannel[] +): ChannelState + +export function updateObj(type: "thread", + s: SignedOrSuccinctThread, + ...rest: PartialSignedOrSuccinctThread[] +): ThreadState + +export function updateObj(type: "ProposePendingDeposit", + s: VerboseOrSuccinctDepositArgs, + ...rest: PartialVerboseOrSuccinctDepositArgs[] +): DepositArgs + +export function updateObj(type: "ProposePendingWithdrawal", + s: VerboseOrSuccinctWithdrawalArgs, + ...rest: PartialVerboseOrSuccinctWithdrawalArgs[] +): WithdrawalArgs + +export function updateObj(type: "Payment", + s: VerboseOrSuccinctPaymentArgs, + ...rest: PartialVerboseOrSuccinctPaymentArgs[] +): PaymentArgs + +export function updateObj(type: "Exchange", + s: VerboseOrSuccinctExchangeArgs, + ...rest: PartialVerboseOrSuccinctExchangeArgs[] +): ExchangeArgs + +export function updateObj(type: "Pending", + s: VerboseOrSuccinctPendingArgs, + ...rest: PartialVerboseOrSuccinctPendingArgs[] +): PendingArgs + +export function updateObj( + type: objUpdateType, + s: any, + ...rest: any[] +) { + const transform = updateFns[type] + let res = transform(s) + for (let s of rest) { + res = !s ? res : { + ...res, + ...transform(s), + } + } + return res +} + +const objUpdateTypes = { + channel: 'channel', + thread: 'thread', + Pending: "Pending" +} +type objUpdateType = keyof typeof objUpdateTypes | ChannelUpdateReason + +const updateFns: any = { + 'ProposePendingWithdrawal': expandSuccinctWithdrawalArgs, + 'ProposePendingDeposit': expandSuccinctDepositArgs, + 'Exchange': expandSuccinctExchangeArgs, + 'Payment': expandSuccinctPaymentArgs, + 'Pending': expandSuccinctPendingArgs, + 'channel': expandSuccinctChannel, + 'thread': expandSuccinctThread, +} + +const initialChannelStates = { + full: () => ({ + contractAddress: mkAddress('0xCCC'), + user: mkAddress('0xAAA'), + recipient: mkAddress('0x222'), + balanceWeiHub: '1', + balanceWeiUser: '2', + balanceTokenHub: '3', + balanceTokenUser: '4', + pendingDepositWeiHub: '4', + pendingDepositWeiUser: '5', + pendingDepositTokenHub: '6', + pendingDepositTokenUser: '7', + pendingWithdrawalWeiHub: '8', + pendingWithdrawalWeiUser: '9', + pendingWithdrawalTokenHub: '10', + pendingWithdrawalTokenUser: '11', + txCountGlobal: 13, + txCountChain: 12, + threadRoot: mkHash('0x141414'), + threadCount: 14, + timeout: 15, + sigUser: mkHash('0xA5'), + sigHub: mkHash('0x15'), + }), + + unsigned: () => + ({ + contractAddress: mkAddress('0xCCC'), + user: mkAddress('0xAAA'), + recipient: mkAddress('0x222'), + balanceWeiHub: '0', + balanceWeiUser: '0', + balanceTokenHub: '0', + balanceTokenUser: '0', + pendingDepositWeiHub: '0', + pendingDepositWeiUser: '0', + pendingDepositTokenHub: '0', + pendingDepositTokenUser: '0', + pendingWithdrawalWeiHub: '0', + pendingWithdrawalWeiUser: '0', + pendingWithdrawalTokenHub: '0', + pendingWithdrawalTokenUser: '0', + txCountGlobal: 1, + txCountChain: 1, + threadRoot: mkHash('0x0'), + threadCount: 0, + timeout: 0, + } as ChannelState), + + empty: () => ({ + contractAddress: mkAddress('0xCCC'), + user: mkAddress('0xAAA'), + recipient: mkAddress('0x222'), + balanceWeiHub: '0', + balanceWeiUser: '0', + balanceTokenHub: '0', + balanceTokenUser: '0', + pendingDepositWeiHub: '0', + pendingDepositWeiUser: '0', + pendingDepositTokenHub: '0', + pendingDepositTokenUser: '0', + pendingWithdrawalWeiHub: '0', + pendingWithdrawalWeiUser: '0', + pendingWithdrawalTokenHub: '0', + pendingWithdrawalTokenUser: '0', + txCountGlobal: 1, + txCountChain: 1, + threadRoot: mkHash('0x0'), + threadCount: 0, + timeout: 0, + sigUser: '', + sigHub: '', + }), +} + +const initialThreadStates = { + full: () => ({ + contractAddress: mkAddress('0xCCC'), + sender: mkAddress('0x222'), + receiver: mkAddress('0x333'), + threadId: 69, + balanceWeiSender: '1', + balanceWeiReceiver: '2', + balanceTokenSender: '3', + balanceTokenReceiver: '4', + txCount: 22, + sigA: mkHash('siga'), + }), + + unsigned: () => + ({ + contractAddress: mkAddress('0xCCC'), + sender: mkAddress('0x222'), + receiver: mkAddress('0x333'), + threadId: 69, + balanceWeiSender: '1', + balanceWeiReceiver: '2', + balanceTokenSender: '3', + balanceTokenReceiver: '4', + txCount: 22, + } as ThreadState), + + empty: () => ({ + contractAddress: mkAddress('0xCCC'), + sender: mkAddress('0x222'), + receiver: mkAddress('0x333'), + threadId: 69, + balanceWeiSender: '0', + balanceWeiReceiver: '0', + balanceTokenSender: '0', + balanceTokenReceiver: '0', + txCount: 0, + sigA: '', + }), +} + +type WDInitial = { [key: string]: () => WithdrawalArgs } + +const initialWithdrawalArgs: WDInitial = { + full: () => ({ + exchangeRate: '5', // wei to token + tokensToSell: '1', + seller: "user", + weiToSell: '2', + recipient: mkAddress('0x222'), + targetWeiUser: '3', + targetTokenUser: '4', + targetWeiHub: '5', + targetTokenHub: '6', + additionalWeiHubToUser: '10', + additionalTokenHubToUser: '11', + timeout: 600, + }), + + empty: () => ({ + exchangeRate: '5', // wei to token + seller: "user", + tokensToSell: '0', + weiToSell: '0', + recipient: mkAddress('0x222'), + additionalWeiHubToUser: '0', + additionalTokenHubToUser: '0', + timeout: 6969, + }), +} + +type DepositInitial = { [key: string]: () => DepositArgs } + +const initialDepositArgs: DepositInitial = { + full: () => ({ + depositTokenUser: '6', + depositWeiUser: '7', + depositWeiHub: '8', + depositTokenHub: '9', + timeout: 696969, + sigUser: mkHash('0xsigUser') + }), + + empty: () => ({ + depositTokenUser: '0', + depositWeiUser: '0', + depositWeiHub: '0', + depositTokenHub: '0', + timeout: 696969, + sigUser: mkHash('0xsigUser') + }) +} + +type PaymentInitial = { [key: string]: () => PaymentArgs } + +const initialPaymentArgs: PaymentInitial = { + full: () => ({ + recipient: "hub", + amountToken: '1', + amountWei: '2' + }), + + empty: () => ({ + recipient: "hub", + amountToken: '0', + amountWei: '0' + }) +} + +type ExchangeInitial = { [key: string]: () => ExchangeArgs } + +const initialExchangeArgs: ExchangeInitial = { + full: () => ({ + exchangeRate: '5', + seller: 'user', + tokensToSell: '5', + weiToSell: '0', + }), + + empty: () => ({ + exchangeRate: '5', + seller: 'user', + tokensToSell: '0', + weiToSell: '0', + }) +} + +type PendingInitial = { [key: string]: () => PendingArgs } + +const initialPendingArgs: PendingInitial = { + full: () => ({ + withdrawalWeiUser: '2', + withdrawalWeiHub: '3', + withdrawalTokenUser: '4', + withdrawalTokenHub: '5', + depositTokenUser: '6', + depositWeiUser: '7', + depositWeiHub: '8', + depositTokenHub: '9', + recipient: mkAddress('0xRRR'), + timeout: 696969 + }), + + empty: () => ({ + withdrawalWeiUser: '0', + withdrawalWeiHub: '0', + withdrawalTokenUser: '0', + withdrawalTokenHub: '0', + depositTokenUser: '0', + depositWeiUser: '0', + depositWeiHub: '0', + depositTokenHub: '0', + recipient: mkAddress('0xRRR'), + timeout: 0 + }) +} + +export function getChannelState( + type: keyof typeof initialChannelStates, + ...overrides: PartialSignedOrSuccinctChannel[] +): ChannelState { + return updateObj("channel", initialChannelStates[type](), ...overrides) +} + +export function getThreadState( + type: keyof typeof initialThreadStates, + ...overrides: PartialSignedOrSuccinctThread[] +): ThreadState { + return updateObj("thread", initialThreadStates[type](), ...overrides) +} + +const getInitialArgs: any = { + "ProposePendingDeposit": initialDepositArgs, + "ProposePendingWithdrawal": initialWithdrawalArgs, + "ConfirmPending": () => { }, + "Payment": initialPaymentArgs, + "Exchange": initialExchangeArgs, + "Pending": initialPendingArgs, +} + +export function getChannelStateUpdate( + reason: ChannelUpdateReason, + ...overrides: { + channel: PartialSignedOrSuccinctChannel, + args: PartialArgsType + }[] +): ChannelStateUpdate { + const argOverrides = overrides.map(o => o.args) + const stateOverrides = overrides.map(o => o.channel) + return { + args: updateObj(reason as any, getInitialArgs[reason].empty(), ...argOverrides), + reason, + state: updateObj("channel", initialChannelStates.empty(), ...stateOverrides) + } +} + +export function getPendingArgs( + type: keyof typeof initialPendingArgs, + ...overrides: PartialVerboseOrSuccinctPendingArgs[] +): PendingArgs { + return updateObj("Pending", initialPendingArgs[type](), ...overrides) +} + +export function getDepositArgs( + type: keyof typeof initialDepositArgs, + ...overrides: PartialVerboseOrSuccinctDepositArgs[] +): DepositArgs { + return updateObj("ProposePendingDeposit", initialDepositArgs[type](), ...overrides) +} + +export function getWithdrawalArgs( + type: keyof typeof initialWithdrawalArgs, + ...overrides: PartialVerboseOrSuccinctWithdrawalArgs[] +): WithdrawalArgs { + return updateObj("ProposePendingWithdrawal", initialWithdrawalArgs[type](), ...overrides) +} + +export function getPaymentArgs( + type: keyof typeof initialPaymentArgs, + ...overrides: PartialVerboseOrSuccinctPaymentArgs[] +): PaymentArgs { + return updateObj("Payment", initialPaymentArgs[type](), ...overrides) +} + +export function getExchangeArgs( + type: keyof typeof initialExchangeArgs, + ...overrides: PartialVerboseOrSuccinctExchangeArgs[] +): ExchangeArgs { + return updateObj("Exchange", initialExchangeArgs[type](), ...overrides) +} + +export function assertChannelStateEqual( + actual: ChannelState, + expected: Partial, +): void { + assert.containSubset( + expandSuccinctChannel(actual), + expandSuccinctChannel(expected), + ) +} + +export function assertThreadStateEqual( + actual: ThreadState, + expected: Partial, +): void { + assert.containSubset( + expandSuccinctThread(actual), + expandSuccinctThread(expected), + ) +} + +export function updateStateUpdate( + stateUpdate: ChannelStateUpdate, + ...rest: PartialSignedOrSuccinctChannel[] +): ChannelStateUpdate { + const succinct = makeSuccinctChannel(stateUpdate.state) + const updatedState = updateObj("channel", succinct, ...rest) + + return { + reason: stateUpdate.reason, + state: updateObj("channel", updatedState), + args: stateUpdate.args + } +} + +// TODO: generate previous and resulting state update with +// ability to override +const sg = new StateGenerator() +const stateGeneratorFns: any = { + "Payment": sg.channelPayment, + "Exchange": sg.exchange, + "ProposePendingDeposit": sg.proposePendingDeposit, + "ProposePendingWithdrawal": sg.proposePendingWithdrawal, + "ConfirmPending": sg.confirmPending, +} + +export type TestParamType = { + update: ChannelStateUpdate + prev: ChannelState +} +export function generateParams( + reason: ChannelUpdateReason, + ...overrides: Partial<{ + args: PartialArgsType, + prev: PartialSignedOrSuccinctChannel, + curr: PartialSignedOrSuccinctChannel, + }>[] +): TestParamType { + const argOverrides = Object.assign(overrides.map(o => o.args)) + const prevOverrides = Object.assign(overrides.map(o => o.prev)) + const currOverrides = Object.assign(overrides.map(o => o.curr)) + const prev = getChannelState("empty", ...prevOverrides) + const args = updateObj( + reason as any, + getInitialArgs[reason].empty(), + Object.assign({ timeout: Math.floor(Date.now() / 100) + 696969 }, ...argOverrides, + ) + ) + const curr = stateGeneratorFns[reason](prev, args) + return { + update: { + reason, + args, + state: updateObj("channel" as any, curr, ...currOverrides), + }, + prev: prev.sigHub !== '' && prev.sigUser !== '' ? addSigToChannelState(prev, mkHash('0x15')) : prev, + } +} + +export function parameterizedTests( + inputs: (TestInput & { name: string })[], + func: (input: TestInput) => any +) { + inputs.forEach(input => { + it(input.name, () => func(input)) + }) +} diff --git a/modules/client/src/testing/mocks.ts b/modules/client/src/testing/mocks.ts new file mode 100644 index 0000000000..8a4ec03a73 --- /dev/null +++ b/modules/client/src/testing/mocks.ts @@ -0,0 +1,579 @@ +import { mkHash, getWithdrawalArgs, getExchangeArgs } from '.' +import { IWeb3TxWrapper } from '../Connext' +import { toBN } from '../helpers/bn' +import { + ChannelState, + Address, + ThreadState, + convertThreadState, + convertChannelState, + addSigToChannelState, + UpdateRequest, + WithdrawalParameters, + ChannelManagerChannelDetails, + Sync, + SignedDepositRequestProposal, + Omit, +} from '../types' +import { SyncResult } from '../types' +import { + getThreadState, + PartialSignedOrSuccinctChannel, + PartialSignedOrSuccinctThread, + getPaymentArgs, +} from '.' +import { UnsignedThreadState } from '../types' +import { ExchangeArgs } from '../types' +import { ChannelStateUpdate } from '../types' +import { IHubAPIClient } from '../Connext' +import Web3 = require('web3') +import { ConnextClientOptions } from '../Connext' +import { ConnextInternal, IChannelManager } from '../Connext' +import { mkAddress, getChannelState, getDepositArgs, assert } from '.' +import { + ChannelRow, + ThreadRow, + PurchasePaymentHubResponse, + Payment, + UnsignedChannelState, + ChannelUpdateReason, + ArgsTypes, + PurchasePayment, +} from '../types' +import { ExchangeRates } from '../state/ConnextState/ExchangeRates' +import { ConnextState, PersistentState, RuntimeState } from '../state/store' +import { StateGenerator } from '../StateGenerator' +import { createStore } from 'redux' +import { reducers } from '../state/reducers' +import BN = require('bn.js') +import { EventLog } from 'web3/types' + +export class MockConnextInternal extends ConnextInternal { + mockContract: MockChannelManager + mockHub: MockHub + + constructor(opts: Partial = {}) { + const store = opts.store || new MockStore().createStore() + + const oldDispatch = store.dispatch as any + const actions: any[] = [] + store.dispatch = function (...args: any[]) { + actions.push(args[0]) + return oldDispatch.call(this, ...args) + } + afterEach(function () { + // ignore this as any ts err + // @ts-ignore + if ((this as any).currentTest.state == 'failed') { + console.error( + 'Actions emitted during test: ' + + (actions.length ? '' : '(no actions)'), + ) + actions.forEach(action => { + console.error(' ', JSON.stringify(action)) + }) + } + }) + + super({ + user: mkAddress('0x123'), + contractAddress: mkAddress('0xccc'), + contract: new MockChannelManager(), + web3: new Web3(), + hub: new MockHub(), + hubAddress: mkAddress('0xhhh'), + store, + ...opts, + } as any) + + this.mockContract = this.contract as MockChannelManager + this.mockHub = this.hub as MockHub + + // stub out actual sig recovery methods, only test presence + // sig recover fns with web3 testing in `utils.test` + this.validator.assertChannelSigner = ( + channelState: ChannelState, + signer: 'user' | 'hub' = 'user', + ): void => { + return + } + + this.validator.assertDepositRequestSigner = (req: SignedDepositRequestProposal, signer: string): void => { return } + after(() => this.stop()) + } + + async signChannelState(state: UnsignedChannelState): Promise { + const { user, hubAddress } = this.opts + return addSigToChannelState(state, mkHash('0x987123'), user !== hubAddress) + } + + async signDepositRequestProposal(args: Omit, ): Promise { + return { ...args, sigUser: mkHash('0x987123') } + } + + async getContractEvents(eventName: string, fromBlock: number): Promise { + return [] + } +} + +export class MockWeb3 extends Web3 { + async getBlockNumber(): Promise { + return 500 + } + + async getBlock(blockNum: number): Promise { + return { + timestamp: Math.floor(Date.now() / 1000), + } + } +} + +export class MockWeb3TxWrapper extends IWeb3TxWrapper { + awaitEnterMempool() { + return new Promise(res => setTimeout(res)) as Promise + } + + awaitFirstConfirmation() { + return new Promise(res => setTimeout(res)) as Promise + } +} + +export class MockChannelManager implements IChannelManager { + contractMethodCalls = [] as any[] + + gasMultiple = 1.5 + + assertCalled(method: keyof MockChannelManager, ...args: any[]) { + for (let call of this.contractMethodCalls) { + if (call.name == method) { + try { + assert.containSubset(call.args, args) + return + } catch (e) { + // do nothing + } + } + } + + assert.fail( + `No contract methods calls matching '${method}(${JSON.stringify( + args, + )})' were made!\n` + + `Method calls:\n${this.contractMethodCalls + .map(c => JSON.stringify(c)) + .join('\n')}`, + ) + } + + async userAuthorizedUpdate(state: ChannelState) { + this.contractMethodCalls.push({ + name: 'userAuthorizedUpdate', + args: [state], + }) + return new MockWeb3TxWrapper() + } + + async getPastEvents(user: Address, eventName: string, fromBlock: number) { + return [] + } + + async getChannelDetails(user: string): Promise { + throw new Error('TODO: mock getChannelDetails') + } +} + +export class MockHub implements IHubAPIClient { + receivedUpdateRequests: UpdateRequest[] = [] + + async getChannel(): Promise { + return { id: 0, state: getChannelState('full'), status: 'CS_OPEN' } + } + + async getChannelStateAtNonce(): Promise { + return { + args: {} as ExchangeArgs, + reason: 'Exchange', + state: getChannelState('full'), + } + } + + async getThreadInitialStates(): Promise { + return [getThreadState('full')] + } + + async getIncomingThreads(): Promise { + return [{ id: 1, state: getThreadState('full') }] + } + + async getThreadByParties(): Promise { + return { id: 1, state: getThreadState('full') } + } + + async sync( + txCountGlobal: number, + lastThreadUpdateId: number, + ): Promise { + // needs to be able to take an update from the store, and apply it + return { status: "CS_OPEN", updates: [] } + } + + async buy( + meta: PurchaseMetaType, + payments: PurchasePayment[], + ): Promise { + const updates = payments.map(p => { + if ((p.update as UpdateRequest).sigUser) { + // user signed update, add to recieved + this.receivedUpdateRequests.push(p.update as UpdateRequest) + } + return { + type: 'channel', + update: { + reason: 'Payment', + args: getPaymentArgs('full', { + amountToken: p.amount.amountToken, + amountWei: p.amount.amountWei, + }), + sigHub: mkHash('0x51512'), + sigUser: (p.update as UpdateRequest).sigUser || '', + txCount: (p.update as UpdateRequest).sigUser + ? (p.update as UpdateRequest).txCount! + : (p.update as UpdateRequest).txCount! + 1, + } as UpdateRequest, + } as SyncResult + }) + + return { + purchaseId: 'some-purchase-id', + sync: { status: "CS_OPEN", updates }, + } + } + + async requestDeposit(deposit: SignedDepositRequestProposal, txCount: number, lastThreadUpdateId: number): Promise { + return { + status: 'CS_OPEN', + updates: [{ + type: 'channel', + update: { + reason: 'ProposePendingDeposit', + args: getDepositArgs('full', { + sigUser: deposit.sigUser, + depositWeiUser: deposit.amountWei, + depositTokenUser: deposit.amountToken, + timeout: parseInt('' + (Date.now() / 1000 + 269)), + }), + sigHub: mkHash('0x51512'), + txCount: txCount + 1, + }, + } + ] + } + } + + async requestWithdrawal( + params: WithdrawalParameters, + txCountGlobal: number, + ): Promise { + const { withdrawalWeiUser, withdrawalTokenUser, ...res } = params + return { + status: "CS_OPEN", + updates: [ + { + type: 'channel', + update: { + reason: 'ProposePendingWithdrawal', + args: getWithdrawalArgs('empty', { + ...res, + targetWeiHub: '0', + targetWeiUser: '0', + targetTokenHub: '0', + targetTokenUser: '0', + additionalWeiHubToUser: '0', + additionalTokenHubToUser: '0', + timeout: +(Date.now() / 1000 + 60).toFixed(), + }), + txCount: txCountGlobal + 1, + }, + }, + ] + } + } + + async requestExchange( + weiToSell: string, + tokensToSell: string, + txCountGlobal: number, + ): Promise { + return { + status: "CS_OPEN", + updates: [ + { + type: 'channel', + update: { + reason: 'Exchange', + args: getExchangeArgs('full', { + exchangeRate: '5', + tokensToSell: toBN(tokensToSell), + weiToSell: toBN(weiToSell), + seller: 'user', + }), + txCount: txCountGlobal + 1, + }, + }, + ] + } + } + + async getExchangerRates(): Promise { + return { USD: '5' } + } + + async requestCollateral(txCountGlobal: number): Promise { + return { + status: "CS_OPEN", + updates: [ + { + type: 'channel', + update: { + reason: 'ProposePendingDeposit', + args: getDepositArgs('full', { + depositTokenHub: toBN(69), + depositTokenUser: toBN(0), + depositWeiHub: toBN(420), + depositWeiUser: toBN(0), + timeout: Math.floor(Date.now() / 1000) + 69, + }), + txCount: txCountGlobal + 1, + }, + }, + ] + } + } + + async updateHub( + updates: UpdateRequest[], + lastThreadUpdateId: number, + ): Promise<{ error: null; updates: Sync }> { + this.receivedUpdateRequests = [...this.receivedUpdateRequests, ...updates] + return { + error: null, + updates: { + status: "CS_OPEN", + updates: updates.map(up => ({ + type: 'channel' as 'channel', + update: { + ...up, + sigHub: up.sigHub || '0xMockHubSig', + }, + })) + }, + } + } + + assertReceivedUpdate(expected: PartialUpdateRequest) { + for (let req of this.receivedUpdateRequests as any[]) { + if (typeof expected.sigUser == 'boolean') + req = { ...req, sigUser: !!req.sigUser } + if (typeof expected.sigHub == 'boolean') + req = { ...req, sigHub: !!req.sigHub } + try { + assert.containSubset(req, expected) + return + } catch (e) { + continue + } + } + + console.log('this.receivedUpdateRequests: ', this.receivedUpdateRequests) + + assert.fail( + `Hub did not recieve any updates matching ${JSON.stringify( + expected, + )}. Got:\n` + + this.receivedUpdateRequests.map(x => JSON.stringify(x)).join('\n'), + ) + } +} + +type PartialUpdateRequest = { + reason: ChannelUpdateReason + args: Partial + txCount?: number + sigUser?: number | boolean + sigHub?: number | boolean +} + +export class MockStore { + public _initialState: ConnextState = { + runtime: new RuntimeState(), + persistent: new PersistentState(), + } + + public createStore = () => { + return createStore(reducers, this._initialState) + } + + public setInitialConnextState = (state: ConnextState) => { + this._initialState = state + } + + public setExchangeRate = (rates: ExchangeRates) => { + this._initialState = { + ...this._initialState, + runtime: { + ...this._initialState.runtime, + exchangeRate: { lastUpdated: new Date(), rates }, + }, + } + } + + public setSyncResultsFromHub = (syncResultsFromHub: SyncResult[]) => { + this._initialState = { + ...this._initialState, + runtime: { + ...this._initialState.runtime, + syncResultsFromHub, + }, + } + } + + /* PERSISTENT STORE SETTINGS */ + public setChannel = (overrides: PartialSignedOrSuccinctChannel = {}) => { + this._initialState = { + ...this._initialState, + persistent: { + ...this._initialState.persistent, + channel: getChannelState("empty", { + txCountChain: 0, + txCountGlobal: 0, + sigHub: '0xsig-hub', + sigUser: '0xsig-user', + }, overrides) + } + } + } + + public setLatestValidState = ( + overrides: PartialSignedOrSuccinctChannel = {}, + ) => { + this._initialState = { + ...this._initialState, + persistent: { + ...this._initialState.persistent, + latestValidState: getChannelState("empty", { + txCountChain: 0, + txCountGlobal: 0, + sigHub: '0xsig-hub', + sigUser: '0xsig-user', + }, overrides) + } + } + } + + public setChannelUpdate = (update: UpdateRequest) => { + this._initialState = { + ...this._initialState, + persistent: { + ...this._initialState.persistent, + channelUpdate: update, + }, + } + } + + public addThread = (overrides: PartialSignedOrSuccinctThread) => { + const thread = getThreadState('empty', overrides) + + let { + threads, + initialThreadStates, + channel, + } = this._initialState.persistent + + threads.push(thread) + + const threadBN = convertThreadState('bn', thread) + + const initialThread: ThreadState = convertThreadState('str', { + ...thread, + txCount: 0, + balanceTokenReceiver: toBN(0), + balanceWeiReceiver: toBN(0), + balanceTokenSender: threadBN.balanceTokenSender.add( + threadBN.balanceTokenReceiver, + ), + balanceWeiSender: threadBN.balanceWeiSender.add( + threadBN.balanceWeiReceiver, + ), + }) + initialThreadStates.push(initialThread) + + const newState = new StateGenerator().openThread( + convertChannelState('bn', channel), + initialThreadStates, + threadBN, + ) + + const latestThreadId = this._initialState.persistent.lastThreadId + 1 + + this._initialState = { + ...this._initialState, + persistent: { + ...this._initialState.persistent, + channel: addSigToChannelState(newState), + lastThreadId: latestThreadId, + initialThreadStates, + threads, + }, + } + } + + public setLastThreadId = (lastThreadId: number) => { + this._initialState = { + ...this._initialState, + persistent: { + ...this._initialState.persistent, + lastThreadId, + }, + } + } + + public setSyncControllerState = (updatesToSync: UpdateRequest[]) => { + this._initialState = { + ...this._initialState, + persistent: { + ...this._initialState.persistent, + syncControllerState: { + updatesToSync, + }, + }, + } + } +} + +/** + * Patch a function. + * + * Will set `host[attr]` to a function which will call `func`, providing the + * old function as the frist argument. + * + * For example, to patch `console.log` so all log lines would be prefixed with + * '[LOG]': + * + * patch(console, 'log', (old, ...args) => { + * old.call(this, '[LOG] ', ...args) + * }) + */ +export function patch(host: T, attr: Attr, func: any) { + let old: any = host[attr] + if (!old) { + let suffix = '' + if ((old.prototype || ({} as any))[attr]) + suffix = ` (but its prototype does; did you forget '.prototype'?)` + throw new Error(`${host} has no attribute '${attr}'${suffix}`) + } + host[attr] = function (this: T) { + // NOTE: causes compiler errors in the wallet + return (func as any).call(this, old.bind(this), ...(arguments as any)) + } as any + return old +} diff --git a/modules/client/src/types.test.ts b/modules/client/src/types.test.ts new file mode 100644 index 0000000000..55039266fe --- /dev/null +++ b/modules/client/src/types.test.ts @@ -0,0 +1,55 @@ +import * as t from './testing/index' +import BN = require('bn.js') +import { assert } from './testing/index' +import { convertChannelState, convertThreadState, convertFields } from './types' +import { BigNumber } from 'bignumber.js/bignumber' + +describe('convertChannelState', () => { + it('should work for strings', () => { + const obj = t.getChannelState('empty') + const unsigned = convertChannelState("str-unsigned", obj) + assert.equal(Object.keys(unsigned).indexOf('sigHub'), -1) + assert.equal(Object.keys(unsigned).indexOf('sigUser'), -1) + }) + + it('should work for bignums', () => { + const obj = t.getChannelState('empty') + const unsigned = convertChannelState("bignumber-unsigned", obj) + assert.equal(Object.keys(unsigned).indexOf('sigHub'), -1) + assert.equal(Object.keys(unsigned).indexOf('sigUser'), -1) + }) +}) + +describe('convertThreadState', () => { + it('should work for strings', () => { + const obj = t.getThreadState('empty') + const unsigned = convertThreadState("str-unsigned", obj) + assert.equal(Object.keys(unsigned).indexOf('sigA'), -1) + }) + + it('should work for bignums', () => { + const obj = t.getChannelState('empty') + const unsigned = convertChannelState("bignumber-unsigned", obj) + assert.equal(Object.keys(unsigned).indexOf('sigA'), -1) + }) +}) + +describe('convertFields', () => { + const types = ['str', 'bignumber', 'bn'] + const examples: any = { + 'str': '69', + 'bignumber': new BigNumber('69'), + 'bn': new BN('69'), + } + + for (const fromType of types) { + for (const toType of types) { + it(`should convert ${fromType} -> ${toType}`, () => { + const res = convertFields(fromType as any, toType as any, ['foo'], { foo: examples[fromType] }) + assert.deepEqual(res, { + foo: examples[toType], + }) + }) + } + } +}) diff --git a/modules/client/src/types.ts b/modules/client/src/types.ts new file mode 100644 index 0000000000..56214be729 --- /dev/null +++ b/modules/client/src/types.ts @@ -0,0 +1,995 @@ +import BN = require('bn.js') +import { BigNumber } from 'bignumber.js' +import Web3 = require('web3') + +// define the common interfaces +export type Address = string + +// alias functions +// @ts-ignore +export const isBN = Web3.utils.isBN +// @ts-ignore +export const isBigNum = Web3.utils.isBigNumber + +/********************************* + ****** CONSTRUCTOR TYPES ******** + *********************************/ +// contract constructor options +export interface ContractOptions { + hubAddress: string + tokenAddress: string +} + +/********************************* + ****** HELPER FUNCTIONS ********* + *********************************/ + +export type NumericTypes = { + 'str': string + 'bn': BN + 'bignumber': BigNumber + 'number': number +} + +export type NumericTypeName = keyof NumericTypes + +function getType(input: any): NumericTypeName { + if (typeof input == 'string') + return 'str' + if (isBigNum(input)) + return 'bignumber' + if (isBN(input)) + return 'bn' + if (typeof input == 'number') + return 'number' // used for testing purposes + throw new Error('Unknown input type: ' + typeof input + ', value: ' + JSON.stringify(input)) +} + +const castFunctions: any = { + 'str-bn': (x: string) => new BN(x), + 'str-bignumber': (x: string) => new BigNumber(x), + 'bn-str': (x: BN) => x.toString(), + 'bn-bignumber': (x: BN) => new BigNumber(x.toString()), + 'bignumber-str': (x: BigNumber) => x.toFixed(), + 'bignumber-bn': (x: BigNumber) => new BN(x.toFixed()), + + // Used for testing + 'number-str': (x: number) => '' + x, + 'number-bn': (x: number) => new BN(x), + 'number-bignumber': (x: number) => new BN(x), +} + +export function convertFields(fromType: NumericTypeName, toType: NumericTypeName, fields: string[], input: any) { + if (fromType === toType) + return input + + if (toType === 'number') + throw new Error('Should not convert fields to numbers') + + let key + if (fromType === 'number' && toType === 'str') { + key = `bn-str` + } else if (fromType === 'number') { + key = `str-${toType}` + } + + // casting functions same for strs and number types + const cast = castFunctions[key || [fromType, toType].join('-')] + if (!cast) + throw new Error(`No castFunc for ${fromType} -> ${toType}`) + + const res = { ...input } + for (const field of fields) { + const name = field.split('?')[0] + const isOptional = field.endsWith('?') + if (isOptional && !(name in input)) + continue + res[name] = cast(input[name]) + } + + return res +} + +/********************************* + ********* CONTRACT TYPES ******** + *********************************/ +export type ChannelManagerChannelDetails = { + txCountGlobal: number + txCountChain: number + threadRoot: string + threadCount: number + exitInitiator: string + channelClosingTime: number + status: string +} + +/********************************* + ********* CHANNEL TYPES ********* + *********************************/ +// channel state fingerprint +// this is what must be signed in all channel updates +export type UnsignedChannelState = { + contractAddress: Address + user: Address + recipient: Address + balanceWeiHub: T + balanceWeiUser: T + balanceTokenHub: T + balanceTokenUser: T + pendingDepositWeiHub: T + pendingDepositWeiUser: T + pendingDepositTokenHub: T + pendingDepositTokenUser: T + pendingWithdrawalWeiHub: T + pendingWithdrawalWeiUser: T + pendingWithdrawalTokenHub: T + pendingWithdrawalTokenUser: T + txCountGlobal: number + txCountChain: number + threadRoot: string + threadCount: number + timeout: number +} + +export type UnsignedChannelStateBN = UnsignedChannelState +export type UnsignedChannelStateBigNumber = UnsignedChannelState + +// signed channel state +// this is what must be submitted to any recover functions +// may have either sigUser or sigHub, or both +export type ChannelState = UnsignedChannelState & + ( + | ({ sigUser: string; sigHub: string }) + | ({ sigUser?: string; sigHub: string }) + | ({ sigUser: string; sigHub?: string }) + ) + +export type ChannelStateBN = ChannelState +export type ChannelStateBigNumber = ChannelState + +export const addSigToChannelState = ( + channel: ChannelState | UnsignedChannelState, + sig?: string, + isUser: boolean = true, +): ChannelState => { + // casting to add sigs when they dont exist + const chan = channel as ChannelState + return { + ...channel, + sigUser: sig && isUser ? sig : (chan.sigUser || ''), + sigHub: sig && !isUser ? sig : (chan.sigHub || '') + } +} + +// channel status +export const ChannelStatus = { + CS_OPEN: 'CS_OPEN', + CS_CHANNEL_DISPUTE: 'CS_CHANNEL_DISPUTE', + CS_THREAD_DISPUTE: 'CS_THREAD_DISPUTE', +} +export type ChannelStatus = keyof typeof ChannelStatus + + +// channel update reasons +export const ChannelUpdateReasons: { [key in keyof UpdateRequestTypes]: string } = { + Payment: 'Payment', + Exchange: 'Exchange', + ProposePendingDeposit: 'ProposePendingDeposit', // changes in pending + ProposePendingWithdrawal: 'ProposePendingWithdrawal', // changes in pending + ConfirmPending: 'ConfirmPending', // changes in balance + Invalidation: 'Invalidation', + OpenThread: 'OpenThread', + CloseThread: 'CloseThread', + // unilateral functions + EmptyChannel: 'EmptyChannel', +} +export type ChannelUpdateReason = keyof UpdateRequestTypes + +// exchangeRate is in units of ERC20 / ETH +// since booty is in 1 USD == USD / ETH +export type ExchangeArgs = { + exchangeRate: string // ERC20 / ETH + seller: 'user' | 'hub' // who is initiating trade + tokensToSell: T + weiToSell: T +} +export type ExchangeArgsBN = ExchangeArgs +export type ExchangeArgsBigNumber = ExchangeArgs + +export type PaymentArgs = { + // TODO: this is currently being used for both channel and thread payments, + // but it should not be. The 'receiver' type, below, should be removed. + recipient: 'user' | 'hub' // | 'receiver', + amountToken: T + amountWei: T +} +export type PaymentArgsBN = PaymentArgs +export type PaymentArgsBigNumber = PaymentArgs + +export type DepositArgs = { + depositWeiHub: T, + depositWeiUser: T, + depositTokenHub: T, + depositTokenUser: T, + + timeout: number, + sigUser?: string, // optional for hub proposed deposits + // metadata describing why this deposit was made, used by the hub to track + // credits being made to the user's account (see, ex, CoinPaymentsService) + reason?: any, + +} +export type DepositArgsBN = DepositArgs +export type DepositArgsBigNumber = DepositArgs + +// this type is used to verify that a user has requested a deposit +// sent to it from the hub on sync to alleviate bugs found +// on multidevice. used to submit proposals to hub + +export type SignedDepositRequestProposal = Payment & { + sigUser: string +} +export type SignedDepositRequestProposalBN = SignedDepositRequestProposal +export type SignedDepositRequestProposalBigNumber = SignedDepositRequestProposal + +export type PendingArgs = { + depositWeiUser: T + depositWeiHub: T + depositTokenUser: T + depositTokenHub: T + + withdrawalWeiUser: T + withdrawalWeiHub: T + withdrawalTokenUser: T + withdrawalTokenHub: T + + recipient: Address + timeout: number +} +export type PendingArgsBN = PendingArgs +export type PendingArgsBigNumber = PendingArgs + +export type PendingExchangeArgs = ExchangeArgs & PendingArgs +export type PendingExchangeArgsBN = PendingExchangeArgs +export type PendingExchangeArgsBigNumber = PendingExchangeArgs + +export type WithdrawalArgs = { + seller: 'user' | 'hub' // who is initiating exchange + exchangeRate: string + tokensToSell: T + weiToSell: T + + // The address which should receive the transfer of user funds. Usually the + // user's address. Corresponds to the `recipient` field in the ChannelState. + recipient: Address + + // The final `balance{Wei,Token}User` after the withdrawal: + // + // 1. If this amount is less than `balance{Wei,Token}User`, then the + // difference will be transferred to `recipient` (ie, added to + // `pendingWithdrawal{Wei,Token}User`). + // + // 2. If this amount is greater than `balance{Wei,Token}User`, then the + // difference will be deposited into the user's balance first from any + // pending wei/token sale, then from the hub's reserve (ie, added to + // `pendingDeposit{Wei,Token}User`). + // + // Note: in an exchange, the wei/tokens that are being sold are *not* + // included in this value, so it's likely that callers will want to deduct + // them: + // + // targetTokenUser: current.balanceTokenUser + // .sub(userTokensToSell) + // + // If either value is omitted (or null), the previous balance will be used, + // minus any `{wei,tokens}ToSell`; ie, the default value is: + // target{Wei,Token}User = prev.balance{Wei,Token}User - args.{wei,tokens}ToSell + targetWeiUser?: T + targetTokenUser?: T + + // The final `balance{Wei,Token}Hub` after the withdrawal: + // + // 1. If this amount is less than `balance{Wei,Token}Hub`, then the difference + // will be returned to the reserve (ie, added to + // `pendingWithdrawal{Wei,Token}Hub`). + // + // 2. If this amount is greater than `balance{Wei,Token}Hub`, then the + // difference will be deposited into the hub's balance from the reserve + // (ie, added to `pendingDeposit{Wei,Token}Hub`). + // + // If either value is omitted (or null), the previous balance will be used; + // ie, the default value is: + // target{Wei,Token}Hub = prev.balance{Wei,Token}Hub + targetWeiHub?: T + targetTokenHub?: T + + // During a withdrawal, the hub may opt to send additional wei/tokens to + // the user (out of the goodness of its heart, or to fulfill a custodial + // payment). These will always be 0 until we support custodial payments. + // If no value is provided, '0' will be used. + additionalWeiHubToUser?: T + additionalTokenHubToUser?: T + + timeout: number +} +export type WithdrawalArgsBN = WithdrawalArgs +export type WithdrawalArgsBigNumber = WithdrawalArgs + +// NOTE: the validators enforce event information +// against previous is equivalent to the information +// released by events corresponding to that transaction hash +export type ConfirmPendingArgs = { + transactionHash: Address, +} + +/** + * An Invalidation occurs when both parties want or need to agree that a state + * is not valid. + * + * This can happen for two reasons: + * 1. When the timeout on a state expires. More formally, a block mined with a + * timestamp greater than the state's timeout, but the contract has not + * emitted a `DidUpdateChannel` event with a matching channel and txCount; + * ie, the state has not been sent to chain. + * + * 2. Either party wants to reject a half-signed state sent by the + * counterparty. For example, if an exchange is proposed and half-signed, + * but the counterparty does not agree with the exchange rate. + * + * Rules for state invalidation: + * 1. A fully-signed state can only be invalidated if it has a timeout and that + * timeout has expired (per the definition of "expired", above) + * + * 2. An invalidation must reference the latest valid state (ie, the one which + * should be reverted to) and the latest invalid state. + * + * These will typically be "N - 1" and "N", except in the case of purchases, + * where the client may send multiple half-signed states to the hub*. In + * this case, the hub will invalidate all the states or none of them. + * + * *: in the future, purchases should be simplified so they only send one + * state, so this will no longer be relevant. + * + * 3. The sender must always sign the invalidation before relaying it to the + * counterparty (ie, it never makes sense to have an unsigned Invalidation). + * + * TODO REB-12: do we need to do anything special with invalidating unsigned + * states? (ex, ProposePendingDeposit) + */ + +// channel status +export const InvalidationReason = { + CU_INVALID_TIMEOUT: 'CU_INVALID_TIMEOUT', // The invalid state has timed out + CU_INVALID_REJECTED: 'CU_INVALID_REJECTED', // The state is being rejected (ex, because the exchange rate is invalid) + CU_INVALID_ERROR: 'CU_INVALID_ERROR', // Some other error +} +export type InvalidationReason = keyof typeof InvalidationReason + +export type InvalidationArgs = { + previousValidTxCount: number + lastInvalidTxCount: number + reason: InvalidationReason + message?: string +} + +export type EmptyChannelArgs = ConfirmPendingArgs + +export type ArgsTypes = + | ExchangeArgs + | PaymentArgs + | DepositArgs + | WithdrawalArgs + | ConfirmPendingArgs + | InvalidationArgs + | EmptyChannelArgs + | UnsignedThreadState + | {} + +export type ArgTypesBN = ArgsTypes +export type ArgTypesBigNumber = ArgsTypes + +export type UpdateRequest> = { + // For unsigned updates, the id will be a negative timestamp of when the + // unsigned update was created. This can be used to ensure they are unique. + id?: number + reason: ChannelUpdateReason + args: Args + // the txCount will be null if the update is an unsigned update + txCount: number | null + sigUser?: string + sigHub?: string + // If this update is coming from the hub, this will be the database timestamp + // when the update was created there. + createdOn?: Date +} + +export type UpdateRequestTypes = { + Payment: UpdateRequest + Exchange: UpdateRequest + ProposePendingDeposit: UpdateRequest + ProposePendingWithdrawal: UpdateRequest + ConfirmPending: UpdateRequest + Invalidation: UpdateRequest + EmptyChannel: UpdateRequest + OpenThread: UpdateRequest> + CloseThread: UpdateRequest> +} + +export type UpdateArgTypes = { + Payment: PaymentArgs + Exchange: ExchangeArgs + ProposePendingDeposit: DepositArgs + ProposePendingWithdrawal: WithdrawalArgs + ConfirmPending: ConfirmPendingArgs + Invalidation: InvalidationArgs + EmptyChannel: EmptyChannelArgs, + OpenThread: UnsignedThreadState + CloseThread: UnsignedThreadState +} + +export type UpdateRequestBN = UpdateRequest +export type UpdateRequestBigNumber = UpdateRequest + +// types used when getting or sending states to hub +export type ChannelStateUpdate = { + // If this state corresponds to a DB state, this ID should match + id?: number + reason: ChannelUpdateReason + state: ChannelState // signed or unsigned? + args: ArgsTypes + metadata?: Object +} +export type ChannelStateUpdateBN = ChannelStateUpdate +export type ChannelStateUpdateBigNumber = ChannelStateUpdate + +export const DisputeStatus = { + CD_PENDING: 'CD_PENDING', + CD_IN_DISPUTE_PERIOD: 'CD_IN_DISPUTE_PERIOD', + CD_FAILED: 'CD_FAILED', + CD_FINISHED: 'CD_FINISHED' +} +export type DisputeStatus = keyof typeof DisputeStatus + +export type SyncResult = + | { type: "thread", update: ThreadStateUpdate } + | { type: "channel", update: UpdateRequest } +export type SyncResultBN = SyncResult +export type SyncResultBigNumber = SyncResult + +// this is the typical form of responses from POST +// hub endpoints and the sync endpoint +export type Sync = { + status: ChannelStatus, + updates: SyncResult[] +} + +// hub response for getters, includes an id and status +export type ChannelRow = { + id: number, + status: ChannelStatus, + state: ChannelState +} +export type ChannelRowBN = ChannelRow +export type ChannelRowBigNumber = ChannelRow + +export type ThreadRow = { + id: number, + state: ThreadState +} +export type ThreadRowBN = ThreadRow +export type ThreadRowBigNumber = ThreadRow + +/********************************* + ********* THREAD TYPES ********** + *********************************/ +// this is everything included in a thread update sig +export type UnsignedThreadState = { + contractAddress: Address + sender: Address + receiver: Address + threadId: number + balanceWeiSender: T + balanceWeiReceiver: T + balanceTokenSender: T + balanceTokenReceiver: T + txCount: number +} +export type UnsignedThreadStateBN = UnsignedThreadState +export type UnsignedThreadStateBigNumber = UnsignedThreadState + +// what is submitted to thread recover fns +export type ThreadState = UnsignedThreadState & + ({ + sigA: string + }) +export type ThreadStateBN = ThreadState +export type ThreadStateBigNumber = ThreadState + +// thread status +export const ThreadStatus = { + CT_CLOSED: 'CT_SETTLED', + CT_OPEN: 'CT_OPEN', + CT_EXITING: 'CT_EXITING', +} + +export type ThreadStatus = keyof typeof ThreadStatus + +// thread state update +export type ThreadStateUpdate = { + // reason: "Payment" + id?: number + createdOn?: Date + state: ThreadState // signed or unsigned? + metadata?: Object +} + +export type ThreadStateUpdateBN = ThreadStateUpdate +export type ThreadStateUpdateBigNumber = ThreadStateUpdate + +export const addSigToThreadState = ( + thread: UnsignedThreadState, + sig?: string, +): ThreadState => { + return { + contractAddress: thread.contractAddress, + sender: thread.sender, + receiver: thread.receiver, + threadId: thread.threadId, + balanceWeiSender: thread.balanceWeiSender, + balanceWeiReceiver: thread.balanceWeiReceiver, + balanceTokenSender: thread.balanceTokenSender, + balanceTokenReceiver: thread.balanceTokenReceiver, + txCount: thread.txCount, + sigA: sig ? sig : '', + } +} + +/********************************* + ******** CONTRACT TYPES ********* + *********************************/ +// event types +export const ChannelEventReasons = { + DidUpdateChannel: 'DidUpdateChannel', + DidStartExitChannel: 'DidStartExitChannel', + DidEmptyChannel: 'DidEmptyChannel', +} +export type ChannelEventReason = keyof typeof ChannelEventReasons + +// DidStartExit, DidEmptyChannel +type BaseChannelEvent = { + user: Address, // indexed + senderIdx: "0" | "1", // 0: hub, 1: user + weiBalances: [T, T], // [hub, user] + tokenBalances: [T, T], // [hub, user] + txCount: [string, string], // [global, onchain] + threadRoot: string, + threadCount: string, +} + +export type DidStartExitChannelEvent = BaseChannelEvent +export type DidStartExitChannelEventBN = BaseChannelEvent +export type DidStartExitChannelBigNumber = BaseChannelEvent + +export type DidEmptyChannelEvent = BaseChannelEvent +export type DidEmptyChannelEventBN = BaseChannelEvent +export type DidEmptyChannelEventBigNumber = BaseChannelEvent + +// DidUpdateChannel +export type DidUpdateChannelEvent = BaseChannelEvent & { + pendingWeiUpdates: [T, T, T, T], // [hubDeposit, hubWithdrawal, userDeposit, userWithdrawal] + pendingTokenUpdates: [T, T, T, T], // [hubDeposit, hubWithdrawal, userDeposit, userWithdrawal] +} +export type DidUpdateChannelEventBN = DidUpdateChannelEvent +export type DidUpdateChannelEventBigNumber = DidUpdateChannelEvent + +const BaseChannelEventInputs = [ + { type: 'address', name: 'user', indexed: true }, + { type: 'uint256', name: 'senderIdx' }, + { type: 'uint256[2]', name: 'weiBalances' }, + { type: 'uint256[2]', name: 'tokenBalances' }, + { type: 'uint256[2]', name: 'txCount' }, + { type: 'bytes32', name: 'threadRoot' }, + { type: 'uint256', name: 'threadCount' }, +] + +const DidUpdateChannelEventInputs = [ + { type: 'address', name: 'user', indexed: true }, + { type: 'uint256', name: 'senderIdx' }, + { type: 'uint256[2]', name: 'weiBalances' }, + { type: 'uint256[2]', name: 'tokenBalances' }, + { type: 'uint256[4]', name: 'pendingWeiUpdates' }, + { type: 'uint256[4]', name: 'pendingTokenUpdates' }, + { type: 'uint256[2]', name: 'txCount' }, + { type: 'bytes32', name: 'threadRoot' }, + { type: 'uint256', name: 'threadCount' }, +] + +export const EventInputs = { + 'DidUpdateChannel': DidUpdateChannelEventInputs, + 'DidStartExitChannel': BaseChannelEventInputs, + 'DidEmptyChannel': BaseChannelEventInputs, +} + +export type ChannelEvent = BaseChannelEvent | DidUpdateChannelEvent + +// convert between succinct and verbose event types +export type VerboseChannelEvent = UnsignedChannelState & { + sender: Address, +} +export type VerboseChannelEventBN = VerboseChannelEvent +export type VerboseChannelEventBigNumber = VerboseChannelEvent + +export function makeEventVerbose(obj: ChannelEvent, hubAddress: Address, contractAddress: Address): VerboseChannelEvent { + let ans = {} as any + ans.contractAddress = contractAddress + Object.entries(obj).forEach(([name, val]) => { + // if it contains arrays, expand to named + const value = val as any + switch (name) { + case "senderIdx": + if (value !== "0" && value !== "1") { + throw new Error(`Incorrect senderIdx value detected: ${value}`) + } + ans.sender = value === "1" ? obj.user : hubAddress + break + case "weiBalances": + ans.balanceWeiHub = value[0] + ans.balanceWeiUser = value[1] + break + case "tokenBalances": + ans.balanceTokenHub = value[0] + ans.balanceTokenUser = value[1] + break + case "pendingWeiUpdates": + ans.pendingDepositWeiHub = value[0] + ans.pendingWithdrawalWeiHub = value[1] + ans.pendingDepositWeiUser = value[2] + ans.pendingWithdrawalWeiUser = value[3] + break + case "pendingTokenUpdates": + ans.pendingDepositTokenHub = value[0] + ans.pendingWithdrawalTokenHub = value[1] + ans.pendingDepositTokenUser = value[2] + ans.pendingWithdrawalTokenUser = value[3] + break + case "txCount": + ans.txCountGlobal = parseInt(value[0], 10) + ans.txCountChain = parseInt(value[1], 10) + break + default: + ans[name] = +value >= 0 && !value.startsWith('0x') + ? +value // detected int + : value + } + }) + return ans +} + +export function convertVerboseEvent(to: To, obj: VerboseChannelEvent): VerboseChannelEvent { + const fromType = getType(obj.balanceWeiHub) + return convertFields(fromType, to, channelNumericFields, obj) +} + +/********************************* + ********* WALLET TYPES ********** + *********************************/ + +// what the wallet submits to client createUpdate functions +export type Payment = { + amountWei: T + amountToken: T +} +export type PaymentBN = Payment +export type PaymentBigNumber = Payment + +export type WithdrawalParameters = { + recipient: Address + + // The exchange rate shown to the user at the time of the withdrawal, so the + // hub can reject the exchange if it would result in a large change to the + // amount being withdrawn + exchangeRate: string + + // Amount to transfer from the user's balance to 'recipient' + withdrawalWeiUser: T + + // Amount of tokens to sell and transfer equivilent wei to 'recipient' + tokensToSell: T + + // Optional because, currently, these are not used + weiToSell?: T + withdrawalTokenUser?: T +} +export type WithdrawalParametersBN = WithdrawalParameters +export type WithdrawalParametersBigNumber = WithdrawalParameters + +export const withdrawalParamsNumericFields = [ + 'withdrawalWeiUser', + 'tokensToSell', + 'weiToSell', + 'withdrawalTokenUser', +] + +/********************************* + ******* TYPE CONVERSIONS ******** + *********************************/ + +// util to convert from string to bn for all types +export const channelNumericFields = [ + 'balanceWeiUser', + 'balanceWeiHub', + 'balanceTokenUser', + 'balanceTokenHub', + 'pendingDepositWeiUser', + 'pendingDepositWeiHub', + 'pendingDepositTokenUser', + 'pendingDepositTokenHub', + 'pendingWithdrawalWeiUser', + 'pendingWithdrawalWeiHub', + 'pendingWithdrawalTokenUser', + 'pendingWithdrawalTokenHub', +] + +export function convertChannelState(to: "bn", obj: ChannelState): ChannelStateBN +export function convertChannelState(to: "bignumber", obj: ChannelState): ChannelStateBigNumber +export function convertChannelState(to: "str", obj: ChannelState): ChannelState +export function convertChannelState(to: "bn-unsigned", obj: ChannelState | UnsignedChannelState): UnsignedChannelStateBN +export function convertChannelState(to: "bignumber-unsigned", obj: ChannelState | UnsignedChannelState): UnsignedChannelStateBigNumber +export function convertChannelState(to: "str-unsigned", obj: ChannelState | UnsignedChannelState): UnsignedChannelState +export function convertChannelState( + to: "bn" | "bignumber" | "str" | "bn-unsigned" | "bignumber-unsigned" | "str-unsigned", + obj: ChannelState | UnsignedChannelState, +) { + const [toType, unsigned] = to.split('-') as any + const fromType = getType(obj.balanceWeiHub) + const res = convertFields(fromType, toType, channelNumericFields, obj) + if (!unsigned) + return res + + if (unsigned != 'unsigned') + throw new Error(`Invalid "to": ${to}`) + return unsignedChannel(res) +} + +export function unsignedChannel(obj: ChannelState | UnsignedChannelState): UnsignedChannelState { + const { sigHub, sigUser, ...unsigned } = obj as ChannelState + return unsigned +} + +export function convertThreadState(to: "bn", obj: ThreadState): ThreadStateBN +export function convertThreadState(to: "bignumber", obj: ThreadState): ThreadStateBigNumber +export function convertThreadState(to: "str", obj: ThreadState): ThreadState +export function convertThreadState(to: "bn-unsigned", obj: ThreadState | UnsignedThreadState): UnsignedThreadStateBN +export function convertThreadState(to: "bignumber-unsigned", obj: ThreadState | UnsignedThreadState): UnsignedThreadStateBigNumber +export function convertThreadState(to: "str-unsigned", obj: ThreadState | UnsignedThreadState): UnsignedThreadState +export function convertThreadState( + to: "bn" | "bignumber" | "str" | "bn-unsigned" | "bignumber-unsigned" | "str-unsigned", + obj: ThreadState | UnsignedThreadState, +) { + const fromType = getType(obj.balanceWeiReceiver) + const [toType, unsigned] = to.split('-') as any + const res = convertFields(fromType, toType, argNumericFields.OpenThread, obj) + if (!unsigned) + return res + + if (unsigned != 'unsigned') + throw new Error(`Invalid "to": ${to}`) + + return unsignedThread(res) +} + +export function unsignedThread(obj: ThreadState | UnsignedThreadState): UnsignedThreadState { + const { sigA, ...unsigned } = obj as ThreadState + return unsigned +} + +export const argNumericFields: { [Name in keyof UpdateArgTypes]: (keyof UpdateArgTypes[Name])[] } = { + Payment: ['amountWei', 'amountToken'], + Exchange: ['weiToSell', 'tokensToSell'], + ProposePendingDeposit: [ + 'depositWeiHub', + 'depositWeiUser', + 'depositTokenHub', + 'depositTokenUser', + ], + ProposePendingWithdrawal: [ + 'tokensToSell', + 'weiToSell', + 'targetWeiUser?', + 'targetTokenUser?', + 'targetWeiHub?', + 'targetTokenHub?', + 'additionalWeiHubToUser', + 'additionalTokenHubToUser', + ] as any, + ConfirmPending: [], + Invalidation: [], + EmptyChannel: [], + OpenThread: ['balanceWeiSender', 'balanceWeiReceiver', 'balanceTokenSender', 'balanceTokenReceiver'], + CloseThread: ['balanceWeiSender', 'balanceWeiReceiver', 'balanceTokenSender', 'balanceTokenReceiver'], +} + +export function convertPayment(to: To, obj: PaymentArgs): PaymentArgs +export function convertPayment(to: To, obj: Payment): Payment +export function convertPayment(to: To, obj: PaymentArgs | Payment) { + const fromType = getType(obj.amountToken) + return convertFields(fromType, to, argNumericFields.Payment, obj) +} + +export function convertWithdrawalParameters(to: To, obj: WithdrawalParameters): WithdrawalParameters { + const fromType = getType(obj.tokensToSell) + const numericFields = [ + 'tokensToSell', + 'withdrawalWeiUser', + 'weiToSell?', + 'withdrawalTokenUser?', + ] + return convertFields(fromType, to, numericFields, obj) +} + +export function convertThreadPayment(to: To, obj: Payment): Payment { + const fromType = getType(obj.amountToken) + return convertFields(fromType, to, argNumericFields.Payment, obj) +} + +export function convertExchange(to: To, obj: ExchangeArgs): ExchangeArgs { + const fromType = getType(obj.tokensToSell) + return convertFields(fromType, to, argNumericFields.Exchange, obj) +} + +export function convertDeposit(to: To, obj: DepositArgs): DepositArgs { + const fromType = getType(obj.depositWeiHub) + return convertFields(fromType, to, argNumericFields.ProposePendingDeposit, obj) +} + +export function convertWithdrawal(to: To, obj: WithdrawalArgs): WithdrawalArgs { + const fromType = getType(obj.tokensToSell) + return convertFields(fromType, to, argNumericFields.ProposePendingWithdrawal, obj) +} + +export function convertWithdrawalParams(to: To, obj: WithdrawalParameters): WithdrawalParameters { + const fromType = getType(obj.tokensToSell) + return convertFields(fromType, to, withdrawalParamsNumericFields, obj) +} + +export const proposePendingNumericArgs = [ + 'depositWeiUser', + 'depositWeiHub', + 'depositTokenUser', + 'depositTokenHub', + 'withdrawalWeiUser', + 'withdrawalWeiHub', + 'withdrawalTokenUser', + 'withdrawalTokenHub', +] + +export function convertProposePending(to: To, obj: PendingArgs): PendingArgs { + const fromType = getType(obj.depositWeiUser) + return convertFields(fromType, to, proposePendingNumericArgs, obj) +} + +export const proposePendingExchangeNumericArgs = proposePendingNumericArgs.concat(argNumericFields.Exchange) + +export function convertProposePendingExchange(to: To, obj: PendingExchangeArgs): PendingExchangeArgs { + const fromType = getType(obj.depositWeiUser) + return convertFields(fromType, to, proposePendingExchangeNumericArgs, obj) +} + +const argConvertFunctions: { [name in keyof UpdateArgTypes]: any } = { + Payment: convertPayment, + Exchange: convertExchange, + ProposePendingDeposit: convertDeposit, + ProposePendingWithdrawal: convertWithdrawal, + ConfirmPending: (to: any, args: ConfirmPendingArgs) => args, + Invalidation: (to: any, args: InvalidationArgs) => args, + EmptyChannel: (to: any, args: EmptyChannelArgs) => args, + OpenThread: convertThreadState, + CloseThread: convertThreadState, +} + +export function convertArgs< + Reason extends keyof UpdateArgTypes, + To extends NumericTypeName, + >(to: To, reason: Reason, args: UpdateArgTypes[Reason]): UpdateArgTypes[Reason] { + return argConvertFunctions[reason](to, args) +} + +/********************************* + ****** PAYMENT & PURCHASE ******* + *********************************/ + +/* +POST /payments/purchase + +Accepts: + + { + metadata: MetadataType, + payments: PurchasePayment[], + } + +Returns: + + { + purchaseId: string, + updates: SyncResponse, + } + +*/ + +export type PurchasePaymentType = 'PT_CHANNEL' | 'PT_THREAD' | 'PT_CUSTODIAL' + + +export interface PurchaseRequest { + meta: MetadataType + payments: PurchasePaymentRequest[] +} + +export type PurchasePaymentRequest = Omit, 'update'> + +export interface Purchase { + // a unique ID for this purchase, generated by the Hub (payments being sent + // by the wallet will not include this; it will be generated and returned + // from the `/payments/purchase` endpoint.) + purchaseId: string + + // merchantId: string // the merchant's ID (or similar; does not exist yet, but will down the road) + + // Metadata related to the purchase. For example: camshowId, performerId, etc. + meta: MetadataType + + // A convenience field summarizing the total amount of this purchase. + // This will be exactly the sum of the amount of each payment: + // amount = sum(payment.amount for payment in payments) + amount: Payment + + payments: PurchasePayment[] +} + +export type PurchasePayment = ({ + // The address of the recipient. For custodial payments, this will be the + // final recipient. For non-custodial payments (ie, thread updates), this + // will be the thread recipient. + recipient: string + + // A convenience field summarizing the change in balance of the underlying + // channel or thread. + // For example, if this is a non-custodial payment for 1 BOOTY, the `amount` + // will be `{ wei: 0, token: 1e18 }`. If this is a custodial ETH <> BOOTY + // exchange, the `amount` will be `{ wei: -1e18, token 2e20 }` (ie, the + // sender is paying 1 ETH for 200 BOOTY). + amount: Payment + + // Metadata related to the Payment. For example `{ type: 'TIP' | 'FEE' }` + meta: MetadataType +} & ( + { + type: 'PT_CHANNEL' + // When a purchase is being sent from the Wallet -> Hub the update should + // be signed by the wallet. + // The hub's counter-signed updates will be included in the SyncResponse. + update: UpdateRequest + } | + { + type: 'PT_CUSTODIAL' + update: UpdateRequest + } | + { + type: 'PT_THREAD' + // See note above + update: ThreadStateUpdate + } + )) + +export type Omit = Pick> +export type PurchasePaymentSummary = Omit, 'update'> + +// this is the structure of the expected +// response from the hub when submitting a purchase +export type PurchasePaymentHubResponse = ({ + purchaseId: string, + sync: Sync +}) + +export type PurchasePaymentHubResponseBN = PurchasePaymentHubResponse +export type PurchasePaymentHubResponseBigNumber = PurchasePaymentHubResponse diff --git a/modules/client/src/validator.test.ts b/modules/client/src/validator.test.ts new file mode 100644 index 0000000000..d951d56b72 --- /dev/null +++ b/modules/client/src/validator.test.ts @@ -0,0 +1,1313 @@ +import { assert } from './testing/index' +import * as t from './testing/index' +import { Validator } from './validator'; +import * as sinon from 'sinon' +import { Utils } from './Utils'; +import { + convertChannelState, + convertPayment, + ChannelEventReason, + PaymentArgs, + PaymentArgsBN, + convertThreadState, + UnsignedThreadState, + ChannelStateBN, + WithdrawalArgsBN, + convertWithdrawal, + ExchangeArgsBN, + EmptyChannelArgs, + EventInputs, + PendingArgs, + proposePendingNumericArgs, + convertProposePending, + PendingArgsBN, + PendingExchangeArgsBN, + InvalidationArgs, + DepositArgsBN +} from './types'; +import { toBN, mul } from './helpers/bn'; +import Web3 = require('web3') +import { EMPTY_ROOT_HASH } from './lib/constants'; +import { HttpProvider } from 'web3/providers'; + +const sampleAddr = "0x0bfa016abfa8f627654b4989da4620271dc77b1c" + +const createMockedTxReceipt: { [name in ChannelEventReason]: (sender: "user" | "hub", web3: Web3, type?: "deposit" | "withdrawal", ...overrides: any[]) => any } = { + DidEmptyChannel: (sender, web3, ...overrides: any[]) => + createMockedEmptyChannelTxReceipt(sender, web3, ...overrides) + , + + DidStartExitChannel: (sender, web3, ...overrides: any[]) => + createMockedStartExitChannelTxReceipt(sender, web3, ...overrides) + , + + DidUpdateChannel: (sender, web3, type, ...overrides: any[]) => { + // default to deposit tx + return createMockedUpdateChannelTxReceipt( + sender, + web3, + type || "deposit", + ...overrides + ) + }, +} + +function createMockedEmptyChannelTxReceipt(sender: "user" | "hub", web3: Web3, ...overrides: any[]) { + const vals = _generateTransactionReceiptValues({ + user: sampleAddr, + senderIdx: sender === "user" ? '1' : '0', // default to user wei deposit 5, + txCount: ["420", "69"], + }, ...overrides) + + return _createMockedTransactionReceipt("DidEmptyChannel", web3, vals) +} + +function createMockedStartExitChannelTxReceipt(sender: "user" | "hub", web3: Web3, ...overrides: any[]) { + const vals = _generateTransactionReceiptValues({ + user: sampleAddr, + senderIdx: sender === "user" ? '1' : '0', // default to user wei deposit 5, + txCount: ["420", "69"], + }, ...overrides) + return _createMockedTransactionReceipt("DidStartExitChannel", web3, vals) +} + +function createMockedUpdateChannelTxReceipt(sender: "user" | "hub", web3: Web3, type: "deposit" | "withdrawal", ...overrides: any[]) { + switch (type) { + case "deposit": + return createMockedDepositTxReceipt(sender, web3, overrides) + case "withdrawal": + return createMockedWithdrawalTxReceipt(sender, web3, overrides) + default: + throw new Error('Unrecognized type:' + type) + } +} + +/* Overrides for these fns function must be in the contract format +as they are used in solidity decoding. Returns tx with default deposit +values of all 5s +*/ +function createMockedWithdrawalTxReceipt(sender: "user" | "hub", web3: Web3, ...overrides: any[]) { + const vals = _generateTransactionReceiptValues({ + senderIdx: sender === "user" ? '1' : '0', // default to user wei deposit 5 + pendingWeiUpdates: ['0', '5', '0', '5'], + pendingTokenUpdates: ['0', '5', '0', '5'], + txCount: ['4', '3'] + }, ...overrides) + + return _createMockedTransactionReceipt("DidUpdateChannel", web3, vals) +} + +function createMockedDepositTxReceipt(sender: "user" | "hub", web3: Web3, ...overrides: any[]) { + const vals = _generateTransactionReceiptValues({ + senderIdx: sender === "user" ? '1' : '0', // default to user wei deposit 5 + pendingWeiUpdates: ['5', '0', '5', '0'], + pendingTokenUpdates: ['5', '0', '5', '0'], + }, ...overrides) + + return _createMockedTransactionReceipt("DidUpdateChannel", web3, vals) +} + +function _generateTransactionReceiptValues(...overrides: any[]) { + return Object.assign({ + user: sampleAddr, + senderIdx: '1', // default to user wei deposit 5 + weiBalances: ['0', '0'], + tokenBalances: ['0', '0'], + txCount: ['1', '1'], + threadRoot: EMPTY_ROOT_HASH, + threadCount: '0', + }, ...overrides) +} + +function _createMockedTransactionReceipt(event: ChannelEventReason, web3: Web3, vals: any) { + const eventTopic = web3.eth.abi.encodeEventSignature({ + name: event, + type: 'event', + inputs: EventInputs[event], + }) + + const addrTopic = web3.eth.abi.encodeParameter('address', vals.user) + + // put non-indexed values in same order as types + const nonIndexedTypes = EventInputs[event].filter(val => Object.keys(val).indexOf('indexed') === -1).map(e => e.type) + let nonIndexedVals: any = [] + EventInputs[event].forEach(val => { + if (val.indexed) { + return + } + nonIndexedVals.push(vals[val.name]) + }) + + const data = web3.eth.abi.encodeParameters(nonIndexedTypes, nonIndexedVals) + + return { + status: true, + contractAddress: t.mkAddress('0xCCC'), + transactionHash: t.mkHash('0xHHH'), + logs: [{ + data: web3.utils.toHex(data), + topics: [eventTopic, addrTopic] + }] + } +} + +function createPreviousChannelState(...overrides: t.PartialSignedOrSuccinctChannel[]) { + const state = t.getChannelState('empty', Object.assign({ + user: sampleAddr, + recipient: sampleAddr, + sigUser: t.mkHash('booty'), + sigHub: t.mkHash('errywhere'), + }, ...overrides)) + return convertChannelState("bn", state) +} + +function createThreadPaymentArgs(...overrides: Partial>[]) { + const { recipient, ...amts } = createPaymentArgs(...overrides) + return amts +} + +function createPaymentArgs( + ...overrides: Partial>[] +): PaymentArgsBN { + const args = Object.assign({ + amountWei: '0', + amountToken: '0', + recipient: "user", + }, ...overrides) as any + + return convertPayment("bn", { ...convertPayment("str", args) }) +} + +function createProposePendingArgs(overrides?: Partial>): PendingArgsBN { + const res = { + recipient: '0x1234', + timeout: 0, + } as PendingArgs + proposePendingNumericArgs.forEach((a: string) => (res as any)[a] = 0) + return convertProposePending('bn', { + ...res, + ...(overrides || {}), + }) +} + +function createThreadState(...overrides: t.PartialSignedOrSuccinctThread[]) { + let opts = Object.assign({}, ...overrides) + const thread = t.getThreadState("empty", { + sigA: t.mkHash('0xtipz'), + balanceWei: [5, 0], + balanceToken: [5, 0], + receiver: t.mkAddress('0xAAA'), + sender: sampleAddr, + ...opts + }) + return convertThreadState("bn", thread) +} + +/* + Use this function to create an arbitrary number of thread states as indicated by the targetThreadCount parameter. Override each thread state that gets returned with provided override arguments. Example usage and output: + + > createChannelThreadOverrides(2, { threadId: 87, receiver: t.mkAddress('0xAAA') }) + > { threadCount: 2, + initialThreadStates: + [ { contractAddress: '0xCCC0000000000000000000000000000000000000', + sender: '0x0bfA016aBFa8f627654b4989DA4620271dc77b1C', + receiver: '0xAAA0000000000000000000000000000000000000', + threadId: 87, + balanceWeiSender: '5', + balanceWeiReceiver: '0', + balanceTokenSender: '5', + balanceTokenReceiver: '0', + txCount: 0 }, + { contractAddress: '0xCCC0000000000000000000000000000000000000', + sender: '0x0bfA016aBFa8f627654b4989DA4620271dc77b1C', + receiver: '0xAAA0000000000000000000000000000000000000', + threadId: 87, + balanceWeiSender: '5', + balanceWeiReceiver: '0', + balanceTokenSender: '5', + balanceTokenReceiver: '0', + txCount: 0 } ], + threadRoot: '0xbb97e9652a4754f4e543a7ed79b654dc5e5914060451f5d87e0b9ab1bde73bef' } + */ +function createChannelThreadOverrides(targetThreadCount: number, ...overrides: any[]) { + const utils = new Utils() + if (!targetThreadCount) { + return { + threadCount: 0, + initialThreadStates: [], + threadRoot: EMPTY_ROOT_HASH + } + } + + let initialThreadStates = [] as UnsignedThreadState[] + for (let i = 0; i < targetThreadCount; i++) { + initialThreadStates.push(convertThreadState("str-unsigned", createThreadState(Object.assign({ + receiver: t.mkAddress(`0x${i + 1}`), + threadId: 69 + i, + }, ...overrides) + ))) + } + return { + threadCount: targetThreadCount, + initialThreadStates, + threadRoot: utils.generateThreadRootHash(initialThreadStates) + } +} + +describe('validator', () => { + let web3 = new Web3() /* NOTE: all functional aspects of web3 are mocked */ + const validator = new Validator(web3, t.mkAddress('0xHHH')) + + describe('channelPayment', () => { + const prev = createPreviousChannelState({ + balanceToken: [5, 5], + balanceWei: [5, 5], + }) + + const paymentTestCases = [ + { + name: 'valid hub to user payment', + args: createPaymentArgs({ + amountToken: 1, + amountWei: '1', + }), + valid: true + }, + { + name: 'valid user to hub payment', + args: createPaymentArgs({ recipient: "hub" }), + valid: true + }, + { + name: 'should return a string payment args are negative', + args: createPaymentArgs({ amountToken: -1, amountWei: -1 }), + valid: false, + }, + { + name: 'should return a string if payment exceeds available channel balance', + args: createPaymentArgs({ amountToken: 10, amountWei: 10 }), + valid: false, + } + ] + + paymentTestCases.forEach(({ name, args, valid }) => { + it(name, () => { + if (valid) + assert.isNull(validator.channelPayment(prev, args)) + else + assert.exists(validator.channelPayment(prev, args)) + }) + }) + }) + + function getExchangeCases() { + const prev = createPreviousChannelState({ + balanceToken: [5, 5], + balanceWei: [5, 5], + }) + + let baseWeiToToken = { + weiToSell: toBN(1), + tokensToSell: toBN(0), + exchangeRate: '5', + seller: "user" + } + + let baseTokenToWei = { + weiToSell: toBN(0), + tokensToSell: toBN(5), + exchangeRate: '5', + seller: "user" + } + + return [ + { + name: 'valid token for wei exchange seller is user', + prev, + args: baseTokenToWei, + valid: true, + }, + { + name: 'valid token for wei exchange seller is hub', + prev, + args: { ...baseTokenToWei, seller: "hub" }, + valid: true, + }, + { + name: 'valid wei for token exchange seller is user', + prev, + args: baseWeiToToken, + valid: true, + }, + { + name: 'valid wei for token exchange seller is user', + prev, + args: { ...baseWeiToToken, seller: "hub" }, + valid: true, + }, + { + name: 'should return a string if both toSell values are zero', + prev, + args: { ...baseWeiToToken, weiToSell: toBN(0) }, + valid: false, + }, + { + name: 'should return a string if neither toSell values are zero', + prev, + args: { ...baseWeiToToken, tokensToSell: toBN(1) }, + valid: false, + }, + { + name: 'should return a string if negative wei to sell is provided', + prev, + args: { ...baseWeiToToken, weiToSell: toBN(-5) }, + valid: false, + }, + { + name: 'should return a string if negative tokens to sell is provided', + prev, + args: { ...baseTokenToWei, tokensToSell: toBN(-5) }, + valid: false, + }, + { + name: 'should return a string if seller cannot afford tokens for wei exchange', + prev, + args: { ...baseTokenToWei, tokensToSell: toBN(10) }, + valid: false, + }, + { + name: 'should return a string if seller cannot afford wei for tokens exchange', + prev, + args: { ...baseWeiToToken, weiToSell: toBN(10) }, + valid: false, + }, + { + name: 'should return a string if payor cannot afford wei for tokens exchange', + prev, + args: { ...baseWeiToToken, weiToSell: toBN(2), }, + valid: false, + }, + { + name: 'should return a string if payor as hub cannot afford tokens for wei exchange', + prev: { ...prev, balanceWeiHub: toBN(0) }, + args: { ...baseTokenToWei, weiToSell: toBN(10) }, + valid: false, + }, + { + name: 'should return a string if payor as user cannot afford tokens for wei exchange', + prev: { ...prev, balanceWeiUser: toBN(0) }, + args: { ...baseTokenToWei, weiToSell: toBN(10), seller: "user" }, + valid: false, + }, + ] + } + + describe('exchange', () => { + getExchangeCases().forEach(({ name, prev, args, valid }) => { + it(name, () => { + if (valid) { + assert.isNull(validator.exchange(prev, args as ExchangeArgsBN)) + } else { + assert.exists(validator.exchange(prev, args as ExchangeArgsBN)) + } + }) + }) + }) + + describe('proposePendingDeposit', () => { + const prev = createPreviousChannelState({ + balanceToken: [5, 5], + balanceWei: [5, 5] + }) + const args: DepositArgsBN = { + depositWeiHub: toBN(1), + depositWeiUser: toBN(1), + depositTokenHub: toBN(1), + depositTokenUser: toBN(1), + sigUser: t.mkHash('0xsigUser'), + timeout: 6969, + } + + const proposePendingDepositCases = [ + { + name: 'should work', + prev, + args, + valid: true + }, + { + name: 'should work if 0 timeout provided (hub authorized deposits)', + prev, + args: { ...args, timeout: 0 }, + valid: true + }, + { + name: 'should return a string if pending operations exist on the previous state', + prev: { ...prev, pendingDepositWeiUser: toBN(5) }, + args, + valid: false + }, + { + name: 'should return a string for negative deposits', + prev, + args: { ...args, depositWeiUser: toBN(-5) }, + valid: false + }, + { + name: 'should return a string if negative timeout provided', + prev, + args: { ...args, timeout: -5 }, + valid: false + }, + { + name: 'should fail if an invalid signer is provided', + prev, + args, + valid: false, + sigRecover: true, + }, + ] + + proposePendingDepositCases.forEach(({ name, prev, args, valid, sigRecover = false }) => { + it(name, () => { + if (sigRecover) { + console.log('validator will throw error') + validator.assertDepositRequestSigner = (args: any, signer: string) => { throw new Error('Invalid signer') } + } else { + validator.assertDepositRequestSigner = (args: any, signer: string) => { return } + } + if (valid) { + assert.isNull(validator.proposePendingDeposit(prev, args)) + } else { + assert.exists(validator.proposePendingDeposit(prev, args)) + } + }) + }) + }) + + describe('proposePendingWithdrawal', () => { + const prev: ChannelStateBN = createPreviousChannelState({ + balanceWei: [10, 5], + balanceToken: [5, 10] + }) + const args: WithdrawalArgsBN = convertWithdrawal("bn", t.getWithdrawalArgs("empty", { + exchangeRate: '2', + tokensToSell: 10, + targetWeiUser: 0, + targetWeiHub: 5, + })) + + const withdrawalCases: { name: any, prev: ChannelStateBN, args: WithdrawalArgsBN, valid: boolean }[] = [ + { + name: 'should work', + prev, + args, + valid: true + }, + { + name: 'should return a string if there are pending ops in prev', + prev: { ...prev, pendingDepositWeiUser: toBN(10) }, + args, + valid: false + }, + { + name: 'should return a string if the args have a negative value', + prev, + args: { ...args, weiToSell: toBN(-5) }, + valid: false + }, + { + name: 'should return a string if resulting state has negative values', + prev, + args: { ...args, tokensToSell: toBN(20) }, + valid: false + }, + { + name: 'should return a string if the args result in an invalid transition', + prev, + args: { ...args, weiToSell: toBN(10), tokensToSell: toBN(0), additionalWeiHubToUser: toBN(30) }, + valid: false + }, + // TODO: find out which args may result in this state from the + // withdrawal function (if any) from wolever + // { + // name: 'should return a string if hub collateralizes an exchange and withdraws with the same currency', + // prev, + // args: '', + // valid: false + // }, + ] + + withdrawalCases.forEach(({ name, prev, args, valid }) => { + it(name, () => { + const res = validator.proposePendingWithdrawal(prev, args) + if (valid) { + assert.isNull(res) + } else { + assert.exists(res) + } + }) + }) + }) + + describe('confirmPending', () => { + const depositReceipt = createMockedDepositTxReceipt("user", web3) + const wdReceipt = createMockedWithdrawalTxReceipt("user", web3) + const multipleReceipt = { ...depositReceipt, logs: depositReceipt.logs.concat(wdReceipt.logs) } + + const prevDeposit = createPreviousChannelState({ + pendingDepositToken: [5, 5], + pendingDepositWei: [5, 5], + }) + const prevWd = createPreviousChannelState({ + pendingWithdrawalToken: [5, 5], + pendingWithdrawalWei: [5, 5], + recipient: t.mkAddress('0xAAA'), + txCount: [4, 3] + }) + + const tx = { + blockHash: t.mkHash('0xBBB'), + to: prevDeposit.contractAddress, + } + + const confirmCases = [ + { + name: 'should work for deposits', + prev: prevDeposit, + stubs: [tx, depositReceipt], + valid: true, + }, + { + name: 'should work for withdrawals', + prev: prevWd, + stubs: [tx, wdReceipt], + valid: true, + }, + { + name: 'should work depsite casing differences', + prev: { ...prevDeposit, user: prevDeposit.user.toUpperCase(), recipient: prevDeposit.user.toUpperCase() }, + stubs: [tx, depositReceipt], + valid: true, + }, + { + name: 'should work if given multiple channel events in logs', + prev: prevDeposit, + stubs: [tx, multipleReceipt], + valid: true, + }, + { + name: 'should return a string if no transaction is found with that hash', + prev: prevWd, + stubs: [null, depositReceipt], + valid: false, + }, + { + name: 'should return a string if transaction is not sent to contract', + prev: prevDeposit, + stubs: [{ ...tx, to: t.mkAddress('0xfail') }, depositReceipt], + valid: false, + }, + { + name: 'should return a string if transaction is not sent by participants', + prev: { ...prevDeposit, user: t.mkAddress('0xUUU'), }, + stubs: [tx, depositReceipt], + valid: false, + }, + { + name: 'should return a string if user is not same in receipt and previous', + prev: { ...prevDeposit, user: t.mkAddress('0xUUU'), }, + stubs: [tx, createMockedDepositTxReceipt("hub", web3)], + valid: false, + }, + // { + // name: 'should return a string if balance wei hub is not same in receipt and previous', + // prev: { ...prevDeposit, balanceWeiHub: toBN(5) }, + // stubs: [tx, depositReceipt], + // valid: false, + // }, + // { + // name: 'should return a string if balance wei user is not same in receipt and previous', + // prev: { ...prevDeposit, balanceWeiUser: toBN(5) }, + // stubs: [tx, depositReceipt], + // valid: false, + // }, + // { + // name: 'should return a string if balance token hub is not same in receipt and previous', + // prev: { ...prevDeposit, balanceTokenHub: toBN(5) }, + // stubs: [tx, depositReceipt], + // valid: false, + // }, + // { + // name: 'should return a string if balance token user is not same in receipt and previous', + // prev: { ...prevDeposit, balanceTokenUser: toBN(5) }, + // stubs: [tx, depositReceipt], + // valid: false, + // }, + { + name: 'should return a string if pending deposit wei hub is not same in receipt and previous', + prev: { ...prevDeposit, pendingDepositWeiHub: toBN(3) }, + stubs: [tx, depositReceipt], + valid: false, + }, + { + name: 'should return a string if pending deposit wei user is not same in receipt and previous', + prev: { ...prevDeposit, pendingDepositWeiUser: toBN(3) }, + stubs: [tx, depositReceipt], + valid: false, + }, + { + name: 'should return a string if pending deposit token hub is not same in receipt and previous', + prev: { ...prevDeposit, pendingDepositTokenHub: toBN(3) }, + stubs: [tx, depositReceipt], + valid: false, + }, + { + name: 'should return a string if pending deposit token user is not same in receipt and previous', + prev: { ...prevDeposit, pendingDepositTokenUser: toBN(3) }, + stubs: [tx, depositReceipt], + valid: false, + }, + { + name: 'should return a string if pending withdrawal wei hub is not same in receipt and previous', + prev: { ...prevWd, pendingWithdrawalWeiHub: toBN(10) }, + stubs: [tx, wdReceipt], + valid: false, + }, + { + name: 'should return a string if pending withdrawal wei user is not same in receipt and previous', + prev: { ...prevWd, pendingWithdrawalWeiUser: toBN(10) }, + stubs: [tx, wdReceipt], + valid: false, + }, + { + name: 'should return a string if pending withdrawal token hub is not same in receipt and previous', + prev: { ...prevWd, pendingWithdrawalTokenHub: toBN(10) }, + stubs: [tx, wdReceipt], + valid: false, + }, + { + name: 'should return a string if pending withdrawal token user is not same in receipt and previous', + prev: { ...prevWd, pendingWithdrawalTokenUser: toBN(10) }, + stubs: [tx, wdReceipt], + valid: false, + }, + // { + // name: 'should return a string if tx count global is not same in receipt and previous', + // prev: { ...prevDeposit, txCountGlobal: 7 }, + // stubs: [tx, depositReceipt], + // valid: false, + // }, + { + name: 'should return a string if tx count chain is not same in receipt and previous', + prev: { ...prevDeposit, txCountChain: 7 }, + stubs: [tx, depositReceipt], + valid: false, + }, + // { + // name: 'should return a string if thread root is not same in receipt and previous', + // prev: { ...prevDeposit, threadRoot: t.mkHash('0xROOTZ') }, + // stubs: [tx, depositReceipt], + // valid: false, + // }, + // { + // name: 'should return a string if thread count is not same in receipt and previous', + // prev: { ...prevDeposit, threadCount: 7 }, + // stubs: [tx, depositReceipt], + // valid: false, + // }, + ] + + confirmCases.forEach(async ({ name, prev, stubs, valid }) => { + it(name, async () => { + // set tx receipt stub + validator.web3.eth.getTransaction = sinon.stub().returns(stubs[0]) + validator.web3.eth.getTransactionReceipt = sinon.stub().returns(stubs[1]) + // set args + const transactionHash = stubs[1] && (stubs[1] as any).transactionHash === depositReceipt.transactionHash ? depositReceipt.transactionHash : wdReceipt.transactionHash + if (valid) { + assert.isNull(await validator.confirmPending(prev, { transactionHash })) + } else { + assert.exists(await validator.confirmPending(prev, { transactionHash })) + } + }) + }) + }) + + describe('invalidation', () => { + const prev = createPreviousChannelState({ + txCount: [1, 1] + }) + + const args: InvalidationArgs = { + previousValidTxCount: prev.txCountGlobal, + lastInvalidTxCount: prev.txCountGlobal + 1, + reason: "CU_INVALID_ERROR", + } + + const invalidationCases = [ + { + name: 'should work', + prev, + args, + valid: true + }, + { + name: 'should return string if previous nonce is higher than nonce to be invalidated', + prev, + args: { ...args, previousValidTxCount: 3 }, + valid: false + }, + { + name: 'should return string if previous state nonce and nonce in args do not match', + prev: { ...prev, txCountGlobal: 5 }, + args: { ...args, previousValidTxCount: 3, lastInvalidTxCount: 3 }, + valid: false + }, + { + name: 'should return string if previous state has pending ops', + prev: { ...prev, pendingDepositWeiUser: toBN(5) }, + args, + valid: false + }, + { + name: 'should return string if previous state is missing sigHub', + prev: { ...prev, sigHub: '' }, + args, + valid: false + }, + { + name: 'should return string if previous state is missing sigUser', + prev: { ...prev, sigUser: '' }, + args, + valid: false + }, + ] + + invalidationCases.forEach(({ name, prev, args, valid }) => { + it(name, () => { + if (valid) { + assert.isNull(validator.invalidation(prev, args)) + } else { + assert.exists(validator.invalidation(prev, args)) + } + }) + }) + }) + + describe('emptyChannel', () => { + const emptyReceipt = createMockedTxReceipt.DidEmptyChannel("user", web3) + const nonzeroReceipt = createMockedTxReceipt.DidEmptyChannel("user", web3, undefined, { weiBalances: ["1", "0"] }) + const depositReceipt = createMockedDepositTxReceipt("user", web3) + + const multipleReceipt = { ...emptyReceipt, logs: emptyReceipt.logs.concat(depositReceipt.logs) } + + const prev = createPreviousChannelState({ + txCount: [420, 69], + }) + + const args: EmptyChannelArgs = { + transactionHash: emptyReceipt.transactionHash, + } + + const tx = { + blockHash: t.mkHash('0xBBB'), + to: prev.contractAddress, + } + + const emptyChannelCases = [ + { + name: "should work", + prev, + args, + stubs: [tx, emptyReceipt], + valid: true, + }, + { + name: "should work with multiple events", + prev, + args, + stubs: [tx, multipleReceipt], + valid: true, + }, + { + name: "should fail if it cannot find tx", + prev, + args, + stubs: [null, emptyReceipt], + valid: false, + }, + { + name: "should fail if tx has no blockhash", + prev, + args, + stubs: [{ ...tx, blockHash: null }, emptyReceipt], + valid: false, + }, + { + name: "should fail if tx to contract not found", + prev: { ...prev, contractAddress: t.mkAddress('0xfail') }, + args, + stubs: [tx, emptyReceipt], + valid: false, + }, + { + name: "should fail if no event is found", + prev, + args, + stubs: [tx, { ...emptyReceipt, logs: null }], + valid: false, + }, + { + name: "should fail if no matching event is found", + prev: { ...prev, user: t.mkAddress('0xfail') }, + args, + stubs: [tx, emptyReceipt], + valid: false, + }, + { + name: "should fail if event has nonzero fields", + prev, + args, + stubs: [tx, nonzeroReceipt], + valid: false, + }, + { + name: "should fail if previous has a higher txCountGlobal", + prev: { ...prev, txCountGlobal: 5000 }, + args, + stubs: [tx, emptyReceipt], + valid: false, + }, + ] + + emptyChannelCases.forEach(async ({ name, prev, args, stubs, valid }) => { + it(name, async () => { + // set tx receipt stub + validator.web3.eth.getTransaction = sinon.stub().returns(stubs[0]) + validator.web3.eth.getTransactionReceipt = sinon.stub().returns(stubs[1]) + if (valid) { + assert.isNull(await validator.emptyChannel(prev, args)) + } else { + assert.exists(await validator.emptyChannel(prev, args)) + } + }) + }) + }) + + describe.skip('openThread', () => { + + const params = createChannelThreadOverrides(2, { threadId: 70, receiver: t.mkAddress() }) + // contains 2 threads, one where user is sender + // one where user is receiver + const initialThreadStates = params.initialThreadStates + const { threadRoot, threadCount, ...res } = params + + const prev = createPreviousChannelState({ + threadCount, + threadRoot, + balanceToken: [10, 10], + balanceWei: [10, 10] + }) + + const args = createThreadState() + + const cases = [ + { + name: 'should work with first thread', + prev: { ...prev, threadRoot: EMPTY_ROOT_HASH, threadCount: 0 }, + initialThreadStates: [], + sigErr: false, + args, + valid: true, + }, + { + name: 'should work for additional threads', + prev, + initialThreadStates, + sigErr: false, + args, + valid: true, + }, + { + name: 'should return a string if an incorrect signer is detected', + prev, + initialThreadStates, + sigErr: true, + args, + valid: false, + }, + { + name: 'should return a string if the tx count is non-zero', + prev, + initialThreadStates, + sigErr: false, + args: { ...args, txCount: 7 }, + valid: false, + }, + { + name: 'should return a string if the contract address is not the same as channel', + prev, + initialThreadStates, + sigErr: false, + args: { ...args, contractAddress: t.mkAddress('0xFFF') }, + valid: false, + }, + { + name: 'should return a string if the receiver wei balance is non-zero', + prev, + initialThreadStates, + sigErr: false, + args: { ...args, balanceWeiReceiver: toBN(2) }, + valid: false, + }, + { + name: 'should return a string if the receiver token balance is non-zero', + prev, + initialThreadStates, + sigErr: false, + args: { ...args, balanceTokenReceiver: toBN(2) }, + valid: false, + }, + { + name: 'should return a string if the thread sender (as hub) cannot afford to create the thread', + prev, + initialThreadStates, + sigErr: false, + args: { ...args, balanceWeiSender: toBN(20), balanceTokenSender: toBN(20), receiver: prev.user, sender: t.mkAddress('0xAAA') }, + valid: false, + }, + { + name: 'should return a string if the thread sender (as user) cannot afford to create the thread', + prev, + initialThreadStates, + sigErr: false, + args: { ...args, balanceWeiSender: toBN(20), balanceTokenSender: toBN(20) }, + valid: false, + }, + ] + + cases.forEach(async ({ name, prev, initialThreadStates, sigErr, args, valid }) => { + it(name, async () => { + // ignore recovery by defaul + validator.assertThreadSigner = sinon.stub().returns(null) + if (sigErr) + validator.assertThreadSigner = sinon.stub().throws(new Error(`Incorrect signer`)) + + if (valid) { + assert.isNull(await validator.openThread(prev, initialThreadStates, args)) + } else { + assert.exists(await validator.openThread(prev, initialThreadStates, args)) + } + + }) + }) + }) + + describe.skip('closeThread', () => { + const params = createChannelThreadOverrides(2, { sender: t.mkAddress('0x18'), receiver: sampleAddr }) + // contains 2 threads, one where user is sender + // one where user is receiver + const initialThreadStates = params.initialThreadStates + const { threadRoot, threadCount, ...res } = params + + const prev = createPreviousChannelState({ + threadCount, + threadRoot, + balanceToken: [10, 10], + balanceWei: [10, 10] + }) + + const args = createThreadState({ + ...initialThreadStates[0], // user is receiver + balanceWei: [3, 2], + balanceToken: [2, 3] + }) + + const cases = [ + { + name: 'should work', + prev, + initialThreadStates, + args, + sigErr: false, // stubs out sig recover in tests + valid: true, + }, + { + name: 'should return a string if the args provided is not included in initial states', + prev, + initialThreadStates: [initialThreadStates[1]], + args, + sigErr: false, + valid: false, + }, + { + name: 'should return a string if the signer did not sign args', + prev, + initialThreadStates, + args, + sigErr: true, // stubs out sig recover in tests + valid: false, + }, + { + name: 'should return a string if the final state wei balance is not conserved', + prev, + initialThreadStates, + args: { ...args, balanceWeiSender: toBN(10) }, + sigErr: false, // stubs out sig recover in tests + valid: false, + }, + { + name: 'should return a string if the final state token balance is not conserved', + prev, + initialThreadStates, + args: { ...args, balanceTokenSender: toBN(10), balanceWeiSender: toBN(10) }, + sigErr: false, // stubs out sig recover in tests + valid: false, + }, + ] + cases.forEach(async ({ name, prev, initialThreadStates, sigErr, args, valid }) => { + it(name, async () => { + // ignore recovery by defaul + validator.assertThreadSigner = sinon.stub().returns(null) + if (sigErr) + validator.assertThreadSigner = sinon.stub().throws(new Error(`Incorrect signer`)) + + if (valid) { + assert.isNull(await validator.closeThread(prev, initialThreadStates, args)) + } else { + assert.exists(await validator.closeThread(prev, initialThreadStates, args)) + } + + }) + }) + }) + + function getProposePendingCases() { + const prev = createPreviousChannelState({ + balanceToken: [5, 5], + balanceWei: [5, 5], + }) + const args = createProposePendingArgs() + + return [ + { + name: 'should work', + prev, + args, + valid: true, + }, + { + name: 'should return a string if args are negative', + prev, + args: createProposePendingArgs({ + depositWeiUser: -1, + }), + valid: false, + }, + { + name: 'should error if withdrawal exceeds balance', + prev, + args: createProposePendingArgs({ + withdrawalWeiUser: 100, + }), + valid: false, + }, + { + name: 'should error if timeout is negative', + prev, + args: createProposePendingArgs({ + timeout: -1, + }), + valid: false, + }, + ] + } + + describe('proposePending', () => { + getProposePendingCases().forEach(async ({ name, prev, args, valid }) => { + it(name, async () => { + if (valid) { + assert.isNull(await validator.proposePending(prev, args)) + } else { + assert.exists(await validator.proposePending(prev, args)) + } + }) + }) + }) + + describe('proposePendingExchange', () => { + const prev = createPreviousChannelState({ + balanceToken: [5, 5], + balanceWei: [5, 5], + }) + const args: PendingExchangeArgsBN = { + exchangeRate: '2', + weiToSell: toBN(0), + tokensToSell: toBN(0), + seller: "user", + ...createProposePendingArgs(), + } + + function runCase(tc: { name: string, prev: ChannelStateBN, args: PendingExchangeArgsBN, valid: boolean }) { + it(tc.name, async () => { + if (tc.valid) { + assert.isNull(await validator.proposePendingExchange(tc.prev, tc.args)) + } else { + assert.exists(await validator.proposePendingExchange(tc.prev, tc.args)) + } + }) + } + + const proposePendingExchangeCases = [ + { + name: 'exchange + withdrawal makes balance 0', + prev, + args: { + ...args, + tokensToSell: toBN(2), + withdrawalTokenUser: toBN(3), + }, + valid: true, + }, + + { + name: 'exchange + withdrawal makes balance negative', + prev, + args: { + ...args, + tokensToSell: toBN(4), + withdrawalTokenUser: toBN(4), + }, + valid: false, + }, + + { + name: 'hub withdraws sold tokens', + prev, + args: { + ...args, + tokensToSell: toBN(5), + withdrawalTokenHub: toBN(7), + }, + valid: true, + }, + + { + name: 'user withdraws purchased wei', + prev, + args: { + ...args, + tokensToSell: toBN(4), + withdrawalWeiUser: toBN(7), + }, + valid: true, + }, + + ] + + proposePendingExchangeCases.forEach(runCase) + + describe('with pending cases', () => { + getProposePendingCases().forEach(tc => { + runCase({ ...tc, args: { ...args, weiToSell: toBN(1), ...tc.args } }) + }) + }) + + describe('with exchange cases', () => { + getExchangeCases().forEach(tc => { + runCase({ ...tc, args: { ...args, ...tc.args as ExchangeArgsBN } }) + }) + }) + }) + + describe.skip('threadPayment', () => { + const prev = createThreadState() + const args = createThreadPaymentArgs() + + const threadPaymentCases = [ + { + name: 'should work', + args, + valid: true, + }, + { + name: 'should return a string if payment args are negative', + args: createThreadPaymentArgs({ + amountToken: -1, + amountWei: -1, + }), + valid: false, + }, + { + name: 'should return a string if payment exceeds available thread balance', + args: createThreadPaymentArgs({ + amountToken: 20, + amountWei: 20, + }), + valid: false, + }, + ] + + threadPaymentCases.forEach(async ({ name, args, valid }) => { + it(name, async () => { + if (valid) { + assert.isNull(await validator.threadPayment(prev, args)) + } else { + assert.exists(await validator.threadPayment(prev, args)) + } + }) + }) + }) +}) + + +/* EG of tx receipt obj string, use JSON.parse +const txReceipt = { + "transactionHash": "${t.mkHash('0xhash')}", + "transactionIndex": 0, + "blockHash": "0xe352de5c890efc61876e239e15ed474f93604fdbc5f542ff28c165c25b0b6d55", "blockNumber": 437, + "gasUsed": 609307, + "cumulativeGasUsed": 609307, + "contractAddress": "${t.mkAddress('0xCCC')}", + "logs": + [{ + "logIndex": 0, + "transactionIndex": 0, + "transactionHash": "0xae51947afec970dd134ce1d8589c924b99bfa6a3b7f2d61cb95a447804a196a7", + "blockHash": "${t.mkHash('0xblock')}", + "blockNumber": 437, + "address": "0x9378e143606A4666AD5F20Ac8865B44e703e321e", + "data": "0x0000000000000000000000000000000000000000000000000000000000000000", + "topics": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x0000000000000000000000002da565caa7037eb198393181089e92181ef5fb53", "0x0000000000000000000000003638eeb7733ed1fb83cf028200dfb2c41a6d9da8"], + "type": "mined", + "id": "log_9f6b5361" + }, + { + "logIndex": 1, + "transactionIndex": 0, + "transactionHash": "0xae51947afec970dd134ce1d8589c924b99bfa6a3b7f2d61cb95a447804a196a7", + "blockHash": "0xe352de5c890efc61876e239e15ed474f93604fdbc5f542ff28c165c25b0b6d55", + "blockNumber": 437, + "address": "0x9378e143606A4666AD5F20Ac8865B44e703e321e", + "data": "0x0000000000000000000000000000000000000000000000000000000000000000", + "topics": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x0000000000000000000000003638eeb7733ed1fb83cf028200dfb2c41a6d9da8", "0x0000000000000000000000002da565caa7037eb198393181089e92181ef5fb53"], + "type": "mined", + "id": "log_18b4ce0a" + }, + { + "logIndex": 2, + "transactionIndex": 0, + "transactionHash": "0xae51947afec970dd134ce1d8589c924b99bfa6a3b7f2d61cb95a447804a196a7", + "blockHash": "0xe352de5c890efc61876e239e15ed474f93604fdbc5f542ff28c165c25b0b6d55", + "blockNumber": 437, + "address": "0x3638EEB7733ed1Fb83Cf028200dfb2C41A6D9DA8", + "data": "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "topics": ["0xeace9ecdebd30bbfc243bdc30bfa016abfa8f627654b4989da4620271dc77b1c", "0x0000000000000000000000002da565caa7037eb198393181089e92181ef5fb53"], + "type": "mined", + "id": "log_bc5572a6" + }], + "status": true, + "logsBloom": "0x00000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000020000000000000000000020000000000000000000008000000000000000000040000000000000008000000420000000000000000000000000000080000000000000000000810000000000000000000000000000000000000000000020000000000000000000000000000000100000000000000000000000000000000000000000000000000040000000000000002000008000000000000000000000000000000000000000000000000000000008000000000000000000000000000001000000000000000000000000000" +} +*/ diff --git a/modules/client/src/validator.ts b/modules/client/src/validator.ts new file mode 100644 index 0000000000..d98c4eb785 --- /dev/null +++ b/modules/client/src/validator.ts @@ -0,0 +1,1031 @@ +import { subOrZero, objMap } from './StateGenerator' +import { convertProposePending, InvalidationArgs, ArgsTypes, SignedDepositRequestProposal, EmptyChannelArgs, VerboseChannelEventBN, ChannelEventReason, EventInputs, convertVerboseEvent, makeEventVerbose } from './types' +import { PendingArgs } from './types' +import { PendingArgsBN } from './types' +import Web3 = require('web3') +import BN = require('bn.js') +import { + Address, + proposePendingNumericArgs, + channelNumericFields, + ChannelState, + ChannelStateBN, + convertChannelState, + convertPayment, + convertThreadState, + DepositArgsBN, + ExchangeArgsBN, + PaymentArgsBN, + PaymentBN, + ThreadState, + ThreadStateBN, + UnsignedChannelState, + UnsignedThreadState, + WithdrawalArgsBN, + UpdateRequest, + argNumericFields, + PendingExchangeArgsBN, + UnsignedChannelStateBN, + PendingExchangeArgs, + convertProposePendingExchange, + ChannelUpdateReason, + PaymentArgs, + ExchangeArgs, + convertExchange, + DepositArgs, + convertDeposit, + WithdrawalArgs, + convertWithdrawal, + ConfirmPendingArgs, + convertThreadPayment, + Payment, + convertArgs, + WithdrawalParametersBN, + withdrawalParamsNumericFields, + convertWithdrawalParams +} from './types' +import { StateGenerator } from './StateGenerator' +import { Utils } from './Utils' +import { toBN, maxBN } from './helpers/bn' +import { capitalize } from './helpers/naming' +import { TransactionReceipt } from 'web3/types' + +// this constant is used to not lose precision on exchanges +// the BN library does not handle non-integers appropriately +export const DEFAULT_EXCHANGE_MULTIPLIER = 1000000 + +const w3utils = (Web3 as any).utils + +/* +This class will validate whether or not the args are deemed sensible. +Will validate args outright, where appropriate, and against determined state +arguments in other places. + +(i.e. validate recipient from arg, validate if channel balance conserved on withdrawal based on current) +*/ +export class Validator { + private utils: Utils + + private stateGenerator: StateGenerator + + private generateHandlers: { [name in ChannelUpdateReason]: any } + + web3: any + + hubAddress: Address + + constructor(web3: Web3, hubAddress: Address) { + this.utils = new Utils() + this.stateGenerator = new StateGenerator() + this.web3 = web3 + this.hubAddress = hubAddress.toLowerCase() + this.generateHandlers = { + 'Payment': this.generateChannelPayment.bind(this), + 'Exchange': this.generateExchange.bind(this), + 'ProposePendingDeposit': this.generateProposePendingDeposit.bind(this), + 'ProposePendingWithdrawal': this.generateProposePendingWithdrawal.bind(this), + 'ConfirmPending': this.generateConfirmPending.bind(this), + 'Invalidation': this.generateInvalidation.bind(this), + 'EmptyChannel': this.generateEmptyChannel.bind(this), + 'OpenThread': () => { throw new Error('REB-36: enbable threads!') }, + 'CloseThread': () => { throw new Error('REB-36: enbable threads!') }, + } + } + + public async generateChannelStateFromRequest(prev: ChannelState, request: UpdateRequest): Promise { + return await this.generateHandlers[request.reason](prev, request.args) + } + + public channelPayment(prev: ChannelStateBN, args: PaymentArgsBN): string | null { + // no negative values in payments + const { recipient, ...amounts } = args + const errs = [ + // implicitly checked from isValid, but err message nicer this way + this.cantAffordFromBalance(prev, amounts, recipient === "user" ? "hub" : "user"), + this.isValidStateTransitionRequest( + (prev), + { args, reason: "Payment", txCount: prev.txCountGlobal } + ), + this.hasTimeout(prev), + ].filter(x => !!x)[0] + + if (errs) { + return errs + } + + return null + } + + public generateChannelPayment(prevStr: ChannelState, argsStr: PaymentArgs): UnsignedChannelState { + const prev = convertChannelState("bn", prevStr) + const args = convertPayment("bn", argsStr) + const error = this.channelPayment(prev, args) + if (error) { + throw new Error(error) + } + + return this.stateGenerator.channelPayment(prev, args) + } + + public exchange(prev: ChannelStateBN, args: ExchangeArgsBN): string | null { + const errs = [ + this.cantAffordFromBalance( + prev, + { + amountWei: args.weiToSell, + amountToken: args.tokensToSell + }, + args.seller + ), + this.isValidStateTransitionRequest( + (prev), + { args, reason: "Exchange", txCount: prev.txCountGlobal } + ), + this.hasTimeout(prev), + ].filter(x => !!x)[0] + + if (errs) { + return errs + } + + // either wei or tokens to sell must be 0, both cant be 0 + if (args.tokensToSell.gt(toBN(0)) && args.weiToSell.gt(toBN(0)) || + args.tokensToSell.isZero() && args.weiToSell.isZero() + ) { + return `Exchanges cannot sell both wei and tokens simultaneously (args: ${JSON.stringify(args)}, prev: ${this.logChannel(prev)})` + } + + return null + } + + public generateExchange(prevStr: ChannelState, argsStr: ExchangeArgs): UnsignedChannelState { + const prev = convertChannelState("bn", prevStr) + const args = convertExchange("bn", argsStr) + const error = this.exchange(prev, args) + if (error) { + throw new Error(error) + } + + return this.stateGenerator.exchange(prev, args) + } + + public proposePendingDeposit(prev: ChannelStateBN, args: DepositArgsBN): string | null { + const errs = [ + this.isValidStateTransitionRequest( + (prev), + { args, reason: "ProposePendingDeposit", txCount: prev.txCountGlobal } + ), + this.hasTimeout(prev), + this.hasPendingOps(prev), + ].filter(x => !!x)[0] + + if (errs) { + return errs + } + + if (args.timeout < 0) { + return `Timeouts must be zero or greater when proposing a deposit. (args: ${JSON.stringify(args)}, prev: ${this.logChannel(prev)})` + } + + // ensure the deposit is correctly signed by user if the sig user + // exists + if (args.sigUser) { + try { + const argsStr = convertDeposit("str", args) + const proposal: SignedDepositRequestProposal = { + amountToken: argsStr.depositTokenUser, + amountWei: argsStr.depositWeiUser, + sigUser: args.sigUser, + } + this.assertDepositRequestSigner(proposal, prev.user) + } catch (e) { + return `Invalid signer detected. ` + e.message + ` (prev: ${this.logChannel(prev)}, args: ${this.logArgs(args, "ProposePendingDeposit")}` + } + } + + return null + } + + public generateProposePendingDeposit(prevStr: ChannelState, argsStr: DepositArgs): UnsignedChannelState { + const prev = convertChannelState("bn", prevStr) + const args = convertDeposit("bn", argsStr) + const error = this.proposePendingDeposit(prev, args) + if (error) { + throw new Error(error) + } + + return this.stateGenerator.proposePendingDeposit(prev, args) + } + + private _pendingValidator = ( + prev: ChannelStateBN, + args: PendingArgsBN | PendingExchangeArgsBN, + proposedStr: UnsignedChannelState, + ): string | null => { + + const errs = [ + this.hasTimeout(prev), + this.hasPendingOps(prev), + this.hasNegative(args, proposePendingNumericArgs), + this.hasNegative(proposedStr, channelNumericFields), + args.timeout < 0 ? `timeout is negative: ${args.timeout}` : null, + ].filter(x => !!x)[0] + if (errs) + return errs + + return null + } + + public proposePending = (prev: ChannelStateBN, args: PendingArgsBN): string | null => { + return this._pendingValidator(prev, args, this.stateGenerator.proposePending(prev, args)) + } + + public generateProposePending = (prevStr: ChannelState, argsStr: PendingArgs): UnsignedChannelState => { + const prev = convertChannelState("bn", prevStr) + const args = convertProposePending("bn", argsStr) + const error = this.proposePending(prev, args) + if (error) { + throw new Error(error) + } + + return this.stateGenerator.proposePending(prev, args) + } + + public proposePendingExchange = (prev: ChannelStateBN, args: PendingExchangeArgsBN): string | null => { + const err = this.exchange(prev, args) + if (err) + return err + return this._pendingValidator(prev, args, this.stateGenerator.proposePendingExchange(prev, args)) + } + + public generateProposePendingExchange = (prevStr: ChannelState, argsStr: PendingExchangeArgs): UnsignedChannelState => { + const prev = convertChannelState("bn", prevStr) + const args = convertProposePendingExchange("bn", argsStr) + const error = this.proposePendingExchange(prev, args) + if (error) { + throw new Error(error) + } + + return this.stateGenerator.proposePendingWithdrawal(prev, args) + } + + public withdrawalParams = (params: WithdrawalParametersBN): string | null => { + if (+params.exchangeRate != +params.exchangeRate || +params.exchangeRate < 0) + return 'invalid exchange rate: ' + params.exchangeRate + return this.hasNegative(params, withdrawalParamsNumericFields) + } + + public payment = (params: PaymentBN): string | null => { + return this.hasNegative(params, argNumericFields.Payment) + } + + public proposePendingWithdrawal = (prev: ChannelStateBN, args: WithdrawalArgsBN): string | null => { + const errs = [ + this.isValidStateTransitionRequest( + (prev), + { args, reason: "ProposePendingWithdrawal", txCount: prev.txCountGlobal } + ), + this.hasTimeout(prev), + this.hasPendingOps(prev), + ].filter(x => !!x)[0] + + if (errs) { + return errs + } + + return null + } + + public generateProposePendingWithdrawal(prevStr: ChannelState, argsStr: WithdrawalArgs): UnsignedChannelState { + const prev = convertChannelState("bn", prevStr) + const args = convertWithdrawal("bn", argsStr) + const error = this.proposePendingWithdrawal(prev, args) + if (error) { + throw new Error(error) + } + + return this.stateGenerator.proposePendingWithdrawal(prev, args) + } + + public async confirmPending(prev: ChannelStateBN, args: ConfirmPendingArgs): Promise { + // apply .toLowerCase to all strings on the prev object + // (contractAddress, user, recipient, threadRoot, sigHub) + prev = objMap(prev, (k, v) => typeof v == 'string' ? v.toLowerCase() : v) as any + + // validate on chain information + // const txHash = args.transactionHash + const tx = await this.web3.eth.getTransaction(args.transactionHash) as any + + // return out of fn on transaction errors + if (!tx || !tx.blockHash) { + return `Transaction to contract not found. (txHash: ${args.transactionHash}, prev: ${this.logChannel(prev)})` + } + + if (tx.to.toLowerCase() !== prev.contractAddress.toLowerCase()) { + return `Transaction is not for the correct channel manager contract. (txHash: ${args.transactionHash}, contractAddress: ${tx.contractAddress}, prev: ${this.logChannel(prev)})` + } + + // parse event values + const receipt = await this.web3.eth.getTransactionReceipt(args.transactionHash) + let events = [] + try { + events = this.parseChannelEventTxReceipt("DidUpdateChannel", receipt, prev.contractAddress) + } catch (e) { + return e.message + } + if (events.length === 0) { + return `Event not able to be parsed or not found. Args: ${args}` + } + + // find matching event + const matchingEvent = this.findMatchingEvent(prev, events) + if (!matchingEvent) { + return `No event matching the contractAddress, user, and txCountChain of the previous state could be found in the events parsed. Tx: ${args.transactionHash}, prev: ${this.logChannel(prev)}, events: ${JSON.stringify(events)}` + } + + if (matchingEvent.sender.toLowerCase() !== prev.user && + matchingEvent.sender.toLowerCase() !== this.hubAddress) { + return `Transaction sender is not member of the channel (txHash: ${args.transactionHash}, event: ${JSON.stringify(event)}, prev: ${this.logChannel(prev)})` + } + + // all pending values from the event as well as the txCountChain and + // timeout must all be equivalent. other fields may have been updated + // with allowable offchain state updates. also exclude fields in event + // that are not in the channel (`sender`) + const keysToCheck = channelNumericFields.filter(f => f.startsWith('pending')) + const str = this.hasInequivalent( + [matchingEvent, prev], + keysToCheck + ) + if (str) { + return `Decoded tx event values are not properly reflected in the previous state. ` + str + `. (txHash: ${args.transactionHash}, events[${events.indexOf(matchingEvent)}]: ${JSON.stringify(matchingEvent)}, prev: ${this.logChannel(prev)})` + } + + // add to args to create request + const err = this.isValidStateTransitionRequest( + prev, + { + reason: "ConfirmPending", + args, + txCount: prev.txCountGlobal, + } + ) + if (err) { + return err + } + + return null + } + + public async generateConfirmPending(prevStr: ChannelState, args: ConfirmPendingArgs): Promise { + const prev = convertChannelState("bn", prevStr) + const error = await this.confirmPending(prev, args) + if (error) { + throw new Error(error) + } + + return this.stateGenerator.confirmPending(prev) + } + + public async emptyChannel(prev: ChannelStateBN, args: EmptyChannelArgs): Promise { + // apply .toLowerCase to all strings on the prev object + // (contractAddress, user, recipient, threadRoot, sigHub) + prev = objMap(prev, (k, v) => typeof v == 'string' ? v.toLowerCase() : v) as any + + // compare event values to expected by transactionHash + // validate on chain information + const txHash = args.transactionHash + const tx = await this.web3.eth.getTransaction(txHash) as any + const receipt = await this.web3.eth.getTransactionReceipt(txHash) + + if (!tx || !tx.blockHash) { + return `Transaction to contract not found. Event not able to be parsed or does not exist.(txHash: ${txHash}, prev: ${JSON.stringify(prev)})` + } + + if (tx.to.toLowerCase() !== prev.contractAddress) { + return `Transaction is not for the correct channel manager contract. (txHash: ${txHash}, contractAddress: ${tx.contractAddress}, prev: ${JSON.stringify(prev)})` + } + + // parse event values + let events = [] + try { + events = this.parseChannelEventTxReceipt("DidEmptyChannel", receipt, prev.contractAddress) + } catch (e) { + return e.message + } + + if (events.length === 0) { + return `Event not able to be parsed or does not exist. Args: ${args}` + } + + // handle all events gracefully if multiple + // find matching event + const matchingEvent = this.findMatchingEvent(prev, events, "txCountChain") + if (!matchingEvent) { + return `No event matching the contractAddress and user of the previous state could be found in the events parsed. Tx: ${args.transactionHash}, prev: ${this.logChannel(prev)}, events: ${JSON.stringify(events)}` + } + + // all channel fields should be 0 + const hasNonzero = this.hasNonzero(matchingEvent, channelNumericFields) + if (hasNonzero) { + return `Nonzero event values were decoded.` + hasNonzero + `. (txHash: ${args.transactionHash}, events[${events.indexOf(matchingEvent)}]: ${JSON.stringify(events)}, prev: ${this.logChannel(prev)})` + } + + // there is no guarantee that the previous state supplied here is + // correct. error if there is a replay attack, i.e. the previous state + // has a higher txCountGlobal + if (prev.txCountGlobal > matchingEvent.txCountGlobal) { + return `Previous state has a higher txCountGlobal than the decoded event. (transactionHash: ${args.transactionHash}, prev: ${this.logChannel(prev)}, ${JSON.stringify(events)}` + } + + return null + } + + public async generateEmptyChannel(prevStr: ChannelState, args: EmptyChannelArgs): Promise { + const prev = convertChannelState("bn", prevStr) + const error = await this.emptyChannel(prev, args) + if (error) { + throw new Error(error) + } + // NOTE: the validator retrieves the event and notes all on chain + // validation from the event. The stateGenerator does NOT. + // Anaologous to confirmPending. To remain consistent with what + // exists onchain, must use path that contains validation + + const receipt = await this.web3.eth.getTransactionReceipt(args.transactionHash) + const events = this.parseChannelEventTxReceipt("DidEmptyChannel", receipt, prev.contractAddress) + const matchingEvent = this.findMatchingEvent(prev, events, "txCountChain") + if (!matchingEvent) { + throw new Error(`This should not happen, matching event not found even though it was found in the validator.`) + } + + // For an empty channel, the generator should rely on an empty channel + // event. All channel information should be reset from the contract. + return this.stateGenerator.emptyChannel(matchingEvent) + } + + // NOTE: the prev here is NOT the previous state in the state-chain + // of events. Instead it is the previously "valid" update, meaning the + // previously double signed upate with no pending ops + public invalidation(latestValidState: ChannelStateBN, args: InvalidationArgs) { + // state should not + if (args.lastInvalidTxCount < args.previousValidTxCount) { + return `Previous valid nonce is higher than the nonce of the state to be invalidated. ${this.logChannel(latestValidState)}, args: ${this.logArgs(args, "Invalidation")}` + } + + // prev state must have same tx count as args + if (latestValidState.txCountGlobal !== args.previousValidTxCount) { + return `Previous state nonce does not match the provided previousValidTxCount. ${this.logChannel(latestValidState)}, args: ${this.logArgs(args, "Invalidation")}` + } + + // ensure the state provided is double signed, w/o pending ops + if (this.hasPendingOps(latestValidState) || !latestValidState.sigHub || !latestValidState.sigUser) { + return `Previous state has pending operations, or is missing a signature. See the notes on the previous state supplied to invalidation in source. (prev: ${this.logChannel(latestValidState)}, args: ${this.logArgs(args, "Invalidation")})` + } + + // NOTE: fully signed states can only be invalidated if timeout passed + // this is out of scope of the validator library + + return null + } + + public generateInvalidation(prevStr: ChannelState, argsStr: InvalidationArgs): UnsignedChannelState { + const prev = convertChannelState("bn", prevStr) + const args = convertArgs("bn", "Invalidation", argsStr) + const error = this.invalidation(prev, args) + if (error) { + throw new Error(error) + } + + return this.stateGenerator.invalidation(prev, args) + } + + public openThread(prev: ChannelStateBN, initialThreadStates: UnsignedThreadState[], args: ThreadStateBN): string | null { + // NOTE: tests mock web3. signing is tested in Utils + const userIsSender = args.sender === prev.user + try { + this.assertThreadSigner(convertThreadState("str", args)) + } catch (e) { + return e.message + } + + const errs = [ + this.cantAffordFromBalance( + prev, + { amountToken: args.balanceTokenSender, amountWei: args.balanceWeiSender }, + userIsSender ? "user" : "hub"), + this.isValidStateTransitionRequest( + prev, + { args, reason: "OpenThread", txCount: prev.txCountGlobal } + ), + this.hasTimeout(prev), + this.hasNonzero( + args, + ['txCount', 'balanceWeiReceiver', 'balanceTokenReceiver'] + ), + ].filter(x => !!x)[0] + + if (errs) { + return errs + } + + if (prev.contractAddress !== args.contractAddress) { + return `Invalid initial thread state for channel: ${prev.contractAddress !== args.contractAddress ? 'contract address of thread invalid' : '' + this.hasNonzero(args, ['txCount', 'balanceWeiReceiver', 'balanceTokenReceiver'])}. (args: ${JSON.stringify(args)}, prev: ${this.logChannel(prev)})` + } + + // must be channel user, cannot open with yourself + if (!userIsSender && args.receiver !== prev.user || userIsSender && args.receiver === args.sender) { + return `Invalid thread members (args: ${JSON.stringify(args)}, initialThreadStates: ${JSON.stringify(initialThreadStates)}, prev: ${this.logChannel(prev)})` + } + + + // NOTE: no way to check if receiver has a channel with the hub + // must be checked wallet-side and hub-side, respectively + // - potential attack vector: + // - hub could potentially have fake "performer" accounts, + // and steal money from the user without them knowing + + // NOTE: threadID must be validated hub side and client side + // there is no way for the validators to have information about this + // - potential attack vector: + // - double spend of threads with same IDs (?) + + return null + } + + public generateOpenThread(prevStr: ChannelState, initialThreadStates: UnsignedThreadState[], argsStr: ThreadState): UnsignedChannelState { + const prev = convertChannelState("bn", prevStr) + const args = convertThreadState("bn", argsStr) + const error = this.openThread(prev, initialThreadStates, args) + if (error) { + throw new Error(error) + } + + return this.stateGenerator.openThread(prev, initialThreadStates, args) + } + + public closeThread(prev: ChannelStateBN, initialThreadStates: UnsignedThreadState[], args: ThreadStateBN): string | null { + const e = this.isValidStateTransitionRequest( + prev, + { args, reason: "CloseThread", txCount: prev.txCountGlobal } + ) + if (e) { + return e + } + // NOTE: the initial thread states are states before the thread is + // closed (corr. to prev open threads) + const initialState = initialThreadStates.filter(thread => thread.threadId === args.threadId)[0] + if (!initialState) { + return `Thread is not included in channel open threads. (args: ${JSON.stringify(args)}, initialThreadStates: ${JSON.stringify(initialThreadStates)}, prev: ${this.logChannel(prev)})` + } + + // NOTE: in other places previous states are not validated, and technically + // the args in this case are a previously signed thread state. We are + // performing sig and balance conservation verification here, however, + // since the major point of the validators is to ensure the args provided + // lead to a valid current state if applied + + // validate the closing thread state is signed + try { + this.assertThreadSigner(convertThreadState("str", args)) + } catch (e) { + return e.message + } + if (this.hasTimeout(prev)) { + return this.hasTimeout(prev) + } + + // and balance is conserved + const initAmts = { + amountWei: toBN(initialState.balanceWeiSender), + amountToken: toBN(initialState.balanceTokenSender) + } + const finalAmts = { + amountWei: args.balanceWeiReceiver.add(args.balanceWeiSender), + amountToken: args.balanceTokenReceiver.add(args.balanceTokenSender) + } + if (this.hasInequivalent([initAmts, finalAmts], Object.keys(finalAmts))) { + return `Balances in closing thread state are not conserved. (args: ${JSON.stringify(args)}, initialThreadStates: ${JSON.stringify(initialThreadStates)}, prev: ${this.logChannel(prev)})` + } + + return null + } + + public generateCloseThread(prevStr: ChannelState, initialThreadStates: UnsignedThreadState[], argsStr: ThreadState): UnsignedChannelState { + const prev = convertChannelState("bn", prevStr) + const args = convertThreadState("bn", argsStr) + const error = this.closeThread(prev, initialThreadStates, args) + if (error) { + throw new Error(error) + } + + return this.stateGenerator.closeThread(prev, initialThreadStates, args) + } + + public threadPayment(prev: ThreadStateBN, args: { amountToken: BN, amountWei: BN }): string | null { + // no negative values in payments + const errs = [ + // TODO: REB-36, threads. API input + this.hasNegative(args, argNumericFields.Payment), + this.cantAffordFromBalance(prev, args, "sender") + ].filter(x => !!x)[0] + if (errs) + return errs + + return null + } + + public generateThreadPayment(prevStr: ThreadState, argsStr: PaymentArgs): UnsignedThreadState { + const prev = convertThreadState("bn", prevStr) + const args = convertThreadPayment("bn", argsStr) + const error = this.threadPayment(prev, args) + if (error) { + throw new Error(error) + } + + return this.stateGenerator.threadPayment(prev, args) + } + + public validateAddress(adr: Address): null | string { + if (!w3utils.isAddress(adr)) { + return `${adr} is not a valid ETH address.` + } + + return null + } + + public assertChannelSigner(channelState: ChannelState, signer: "user" | "hub" = "user"): void { + const sig = signer === "hub" ? channelState.sigHub : channelState.sigUser + const adr = signer === "hub" ? this.hubAddress : channelState.user + if (!sig) { + throw new Error(`Channel state does not have the requested signature. channelState: ${channelState}, sig: ${sig}, signer: ${signer}`) + } + if (this.utils.recoverSignerFromChannelState(channelState, sig) !== adr) { + throw new Error(`Channel state is not correctly signed by ${signer}. channelState: ${JSON.stringify(channelState)}, sig: ${sig}`) + } + } + + public assertThreadSigner(threadState: ThreadState): void { + if (this.utils.recoverSignerFromThreadState(threadState, threadState.sigA) !== threadState.sender) { + throw new Error(`Thread state is not correctly signed. threadState: ${JSON.stringify(threadState)}`) + } + } + + public assertDepositRequestSigner(req: SignedDepositRequestProposal, signer: Address): void { + if (!req.sigUser) { + throw new Error(`No signature detected on deposit request. (request: ${JSON.stringify(req)}, signer: ${signer})`) + } + if (this.utils.recoverSignerFromDepositRequest(req) !== signer) { + throw new Error(`Deposit request proposal is not correctly signed by intended signer. (request: ${JSON.stringify(req)}, signer: ${signer})`) + } + } + + private cantAffordFromBalance(state: ChannelStateBN, value: Partial, payor: "hub" | "user", currency?: "token" | "wei"): string | null + private cantAffordFromBalance(state: ThreadStateBN, value: Partial, payor: "sender", currency?: "token" | "wei"): string | null + private cantAffordFromBalance(state: ChannelStateBN | ThreadStateBN, value: Partial, payor: "hub" | "user" | "sender", currency?: "token" | "wei"): string | null { + const prefix = "balance" + const currencies = currency ? [currency] : ["token", "wei"] + + let fields = [] as any + currencies.forEach(c => fields.push(prefix + capitalize(c) + capitalize(payor))) + + let failedAmounts = [] as string[] + for (const field of fields) { + // get amount + for (const key of Object.keys(value) as (keyof Payment)[]) { + const valCurrency = key.substring('amount'.length) + // currency of values provided in currency types + if (field.indexOf(valCurrency) !== -1 && (state as any)[field].lt(value[key])) { + failedAmounts.push(valCurrency) + } + } + } + + if (failedAmounts.length > 0) { + return `${capitalize(payor)} does not have sufficient ${failedAmounts.join(', ')} balance for a transfer of value: ${JSON.stringify(convertPayment("str", value as any))} (state: ${JSON.stringify(state)})` + } + + return null + } + + private conditions: any = { + 'non-zero': (x: any) => w3utils.isBN(x) ? !x.isZero() : parseInt(x, 10) !== 0, + 'zero': (x: any) => w3utils.isBN(x) ? x.isZero() : parseInt(x, 10) === 0, + 'non-negative': (x: any) => w3utils.isBN(x) ? !x.isNeg() : parseInt(x, 10) >= 0, + 'negative': (x: any) => w3utils.isBN(x) ? x.isNeg() : parseInt(x, 10) < 0, + 'equivalent': (x: any, val: BN | string | number) => w3utils.isBN(x) ? x.eq(val) : x === val, + 'non-equivalent': (x: any, val: BN | string | number) => w3utils.isBN(x) ? !x.eq(val) : x !== val, + } + + // NOTE: objs are converted to lists if they are singular for iterative + // purposes + private evaluateCondition(objs: any[], fields: string[], condition: string): string | null { + let ans = [] as any + const fn = this.conditions[condition] + + fields.forEach(field => { + if (fields.indexOf(field) > -1 && fn(...objs.map((o: any) => o[field]))) + ans.push({ field, value: objs.map(o => o[field]).join(', ') }) + }) + + if (ans.length > 0) { + return `There were ${ans.length} ${condition} fields detected (detected fields and values: ${JSON.stringify(ans)}` + } + return null + } + + private hasZeroes(obj: any, numericFields: string[]): string | null { + return this.evaluateCondition([obj], numericFields, 'zero') + } + + private hasNonzero(obj: any, numericFields: string[]): string | null { + return this.evaluateCondition([obj], numericFields, 'non-zero') + } + + private hasPositive(obj: any, numericFields: string[]): string | null { + return this.evaluateCondition([obj], numericFields, 'non-negative') + } + + private hasNegative(obj: any, numericFields: string[]): string | null { + return this.evaluateCondition([obj], numericFields, 'negative') + } + + private hasEquivalent(objs: any[], fields: string[]): string | null { + return this.evaluateCondition(objs, fields, "equivalent") + } + + private hasInequivalent(objs: any[], fields: string[]): string | null { + return this.evaluateCondition(objs, fields, "non-equivalent") + } + + private hasTimeout(prev: ChannelStateBN): string | null { + if (prev.timeout !== 0) { + return `Previous state contains a timeout, must use Invalidation or ConfirmPending paths. Previous; ${JSON.stringify(convertChannelState("str", prev))}` + } + + return null + } + + public hasPendingOps(state: ChannelStateBN | UnsignedChannelStateBN): string | null { + // validate there are no pending ops + const pendingFields = channelNumericFields.filter(x => x.startsWith('pending')) + return this.hasNonzero(state, pendingFields) + } + + private enforceDelta(objs: any[], delta: number | BN, fields: string[]) { + // gather deltas into objects + let deltas: any = {} + let k: any = {} // same fields, all val is given delta + + fields.forEach(f => { + deltas[f] = typeof delta === 'number' + ? objs[1][f] - objs[0][f] + : objs[1][f].sub(objs[0][f]) + k[f] = delta + }) + + return this.hasInequivalent([deltas, k], fields) + } + + /** NOTE: this function is called within every validator function EXCEPT for the invalidation generator. This is update is an offchain construction to recover from invalid updates without disputing or closing your channel. For this reason, the contract would never see it's transition of de-acknowledgment as "valid" without advance knowledge that it was an invalidation update or a unless it was double signed. + */ + private isValidStateTransition(prev: ChannelStateBN, curr: UnsignedChannelStateBN): string | null { + let errs = [ + this.hasNegative(curr, channelNumericFields), + this.enforceDelta([prev, curr], 1, ['txCountGlobal']) + ] as (string | null)[] + // assume the previous should always have at least one sig + if (prev.txCountChain > 0 && !prev.sigHub && !prev.sigUser) { + errs.push(`No signature detected on the previous state. (prev: ${this.logChannel(prev)}, curr: ${JSON.stringify(curr)})`) + } + + const prevPending = this.hasPendingOps(prev) + const currPending = this.hasPendingOps(curr) + // pending ops only added to current state if the current state + // is of a "ProposePending" request type (indicated by gain of pending ops) + if (currPending && !prevPending) { + errs.push(this.enforceDelta([prev, curr], 1, ['txCountChain'])) + } else { + // NOTE: on confirmPending validator, equivalence between event + // values and previous values is enforced + // it is also enforced that there are pending values + errs.push(this.enforceDelta([prev, curr], 0, ['txCountChain'])) + } + + // calculate the out of channel balance that could be used in + // transition. could include previous pending updates and the + // reserves. + // + // hub will use reserves if it cannot afford the current withdrawal + // requested by user from the available balance that exists in the + // channel state + // + // out of channel balance amounts should be "subtracted" from + // channel balance calculations. This way, we can enforce that + // out of channel balances are accounted for in the + // previous balance calculations + let reserves = { + amountWei: toBN(0), + amountToken: toBN(0), + } + let compiledPending = { + amountWei: toBN(0), + amountToken: toBN(0), + } + + // if the previous operation has pending operations, and current + // does not, then the current op is either a confirmation or an + // invalidation (this code should NOT be used for invalidation updates) + if (prevPending && !currPending) { + // how much reserves were added into contract? + reserves = { + amountWei: maxBN( + curr.pendingWithdrawalWeiUser.sub(prev.balanceWeiHub), + toBN(0) + ), + amountToken: maxBN( + curr.pendingWithdrawalTokenUser.sub(prev.balanceTokenHub), + toBN(0), + ) + } + + // what pending updates need to be included? + // if you confirm a pending withdrawal, that + // balance is removed from the channel and + // channel balance is unaffected. + // + // if you confirm a pending deposit, that balance + // is absorbed into the channel balance + compiledPending = { + amountWei: prev.pendingDepositWeiHub + .add(prev.pendingDepositWeiUser) + .sub(prev.pendingWithdrawalWeiHub) + .sub(prev.pendingWithdrawalWeiUser), + amountToken: prev.pendingDepositTokenHub + .add(prev.pendingDepositTokenUser) + .sub(prev.pendingWithdrawalTokenHub) + .sub(prev.pendingWithdrawalTokenUser), + } + + } + + // reserves are only accounted for in channel balances in propose + // pending states, where they are deducted to illustrate their + // brief lifespan in the channel where they are + // immediately deposited and withdrawn + const prevBal = this.calculateChannelTotals(prev, reserves) + const currBal = this.calculateChannelTotals(curr, compiledPending) + + errs.push(this.enforceDelta([prevBal, currBal], toBN(0), Object.keys(prevBal))) + if (errs) { + return errs.filter(x => !!x)[0] + } + return null + } + + private isValidStateTransitionRequest(prev: ChannelStateBN, request: UpdateRequest): string | null { + // @ts-ignore TODO: wtf + const args = convertArgs("bn", request.reason, request.args) + // will fail on generation in wd if negative args supplied + let err = this.hasNegative(args, (argNumericFields)[request.reason]) + if (err) { + return err + } + // apply update + const currStr = this.stateGenerator.createChannelStateFromRequest(prev, request) + + const curr = convertChannelState("bn-unsigned", currStr) + + err = this.isValidStateTransition(prev, curr) + if (err) { + return err + } + return null + } + + public calculateChannelTotals(state: ChannelStateBN | UnsignedChannelStateBN, outOfChannel: PaymentBN) { + // calculate the total amount of wei and tokens in the channel + // the operational balance is any balance existing minus + // out of channel balance (reserves and previous deposits) + + const total = { + totalChannelWei: state.balanceWeiUser + .add(state.balanceWeiHub) + .add(subOrZero(state.pendingWithdrawalWeiUser, state.pendingDepositWeiUser)) + .add(subOrZero(state.pendingWithdrawalWeiHub, state.pendingDepositWeiHub)) + .sub(outOfChannel.amountWei), + totalChannelToken: state.balanceTokenUser + .add(state.balanceTokenHub) + .add(subOrZero(state.pendingWithdrawalTokenUser, state.pendingDepositTokenUser)) + .add(subOrZero(state.pendingWithdrawalTokenHub, state.pendingDepositTokenHub)) + .sub(outOfChannel.amountToken), + } + return total + } + + private findMatchingEvent(prev: ChannelStateBN, events: VerboseChannelEventBN[], fieldsToExclude: string = ""): VerboseChannelEventBN | null { + const compFields = ["user", "contractAddress", "txCountChain"].filter(f => f !== fieldsToExclude) + return events.filter(e => { + // only return events whos contractAddress, txCountChain, + // and user address are the same as the previous state. + return this.hasInequivalent([e, prev], compFields) === null + })[0] + } + + private parseChannelEventTxReceipt(name: ChannelEventReason, txReceipt: TransactionReceipt, contractAddress: string): VerboseChannelEventBN[] { + + if (!txReceipt.logs) { + throw new Error('Uh-oh! No Tx logs found. Are you sure the receipt is correct?') + } + + const inputs = EventInputs[name] + if (!inputs) { + // indicates invalid name provided + throw new Error(`Uh-oh! No inputs found. Are you sure you did typescript good? Check 'ChannelEventReason' in 'types.ts' in the source. Event name provided: ${name}`) + } + + const eventTopic = this.web3.eth.abi.encodeEventSignature({ + name, + type: 'event', + inputs, + }) + + /* + ContractEvent.fromRawEvent({ + log: log, + txIndex: log.transactionIndex, + logIndex: log.logIndex, + contract: this.contract._address, + sender: txsIndex[log.transactionHash].from, + timestamp: blockIndex[log.blockNumber].timestamp * 1000 + }) + */ + + let parsed: VerboseChannelEventBN[] = [] + txReceipt.logs.forEach((log) => { + // logs have the format where multiple topics + // can adhere to the piece of data you are looking for + // only seach the logs if the topic is contained + let raw = {} as any + if (log.topics[0] !== eventTopic) { + return + } + // will be returned with values double indexed, one under + // their field names, and one under an `_{index}` value, where + // there index is a numeric value in the list corr to the order + // in which they are emitted/defined in the contract + let tmp = this.web3.eth.abi.decodeLog(inputs, log.data, log.topics) as any + // store only the descriptive field names + Object.keys(tmp).forEach((field) => { + if (!field.match(/\d/g) && !field.startsWith('__')) { + raw[field] = tmp[field] + } + }) + + // NOTE: The second topic in the log with the events topic + // is the indexed user. This is valid for all Channel events in contract + raw.user = '0x' + log.topics[1].substring('0x'.length + 12 * 2).toLowerCase() + parsed.push(convertVerboseEvent("bn", makeEventVerbose( + raw, + this.hubAddress, + contractAddress) + )) + }) + + return parsed + } + + /* + event DidStartExitChannel ( + address indexed user, + uint256 senderIdx, // 0: hub, 1: user + uint256[2] weiBalances, // [hub, user] + uint256[2] tokenBalances, // [hub, user] + uint256[2] txCount, // [global, onchain] + bytes32 threadRoot, + uint256 threadCount + ); + + event DidEmptyChannel ( + address indexed user, + uint256 senderIdx, // 0: hub, 1: user + uint256[2] weiBalances, // [hub, user] + uint256[2] tokenBalances, // [hub, user] + uint256[2] txCount, // [global, onchain] + bytes32 threadRoot, + uint256 threadCount + ); + */ + + private logChannel(prev: ChannelStateBN | UnsignedChannelStateBN) { + if (!(prev as ChannelStateBN).sigUser) { + return JSON.stringify(convertChannelState("str-unsigned", prev)) + } else { + return JSON.stringify(convertChannelState("str", prev as ChannelStateBN)) + } + } + + private logArgs(args: ArgsTypes, reason: ChannelUpdateReason) { + return JSON.stringify(convertArgs("str", reason, args as any)) + } +} diff --git a/modules/client/tsconfig.json b/modules/client/tsconfig.json new file mode 100644 index 0000000000..77905fcbbf --- /dev/null +++ b/modules/client/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "baseUrl": ".", + "declaration": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "jsx": "react", + "module": "commonjs", + "noImplicitAny": true, + "outDir": "./dist/", + "sourceMap": true, + "target": "es6", + "lib": [ + "es5", + "dom.iterable", + "es2015", + "es2015.iterable", + "es6", + "dom", + "es2017" + ], + "typeRoots": [ + "node_modules/@types", + "types" + ], + "paths": { + "@src/*": [ + "./src/*", + "./dist/*" + ] + } + }, + "include": [ + "src/**/*.ts" + ] +} From 134090ab0dbb7b0649488dfaa22354f9b2171e1e Mon Sep 17 00:00:00 2001 From: David Wolever Date: Wed, 6 Mar 2019 19:18:21 -0500 Subject: [PATCH 2/4] Add 'copy-sc-client' script --- modules/client/copy-sc-client | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100755 modules/client/copy-sc-client diff --git a/modules/client/copy-sc-client b/modules/client/copy-sc-client new file mode 100755 index 0000000000..a5774aed19 --- /dev/null +++ b/modules/client/copy-sc-client @@ -0,0 +1,12 @@ +#!/bin/bash +# STOP! Before going any further, think: are you going to regret the decision +# to write this script? +# Deciding to write this in bash was not one of my better decisions. +# -- https://twitter.com/alex_gaynor/status/369892494114164736 + +IFS="`printf "\n\t"`" +set -eu +cd "$(dirname "$0")" + +set -x +rsync -avl --exclude 'dist' --exclude 'node_modules' --exclude 'copy-sc-client' --prune-empty-dirs ../../../camsite/client/ . From bd9483e722427889721a11eeccc94729007494ae Mon Sep 17 00:00:00 2001 From: David Wolever Date: Wed, 6 Mar 2019 19:22:30 -0500 Subject: [PATCH 3/4] Copy from SpankChain client --- modules/client/.babelrc | 15 ---- modules/client/.eslintrc | 55 ------------ modules/client/.gitignore | 11 +-- modules/client/.sc-from-rev | 1 + modules/client/.sc-from-rev-pretty | 30 +++++++ modules/client/.vscode/settings.json | 3 - modules/client/CONTRIBUTING.md | 112 ------------------------ modules/client/ISSUE_TEMPLATE.md | 32 ------- modules/client/LICENSE.md | 21 ----- modules/client/PULL_REQUEST_TEMPLATE.md | 28 ------ modules/client/README.md | 0 modules/client/copy-sc-client | 8 +- 12 files changed, 39 insertions(+), 277 deletions(-) delete mode 100644 modules/client/.babelrc delete mode 100644 modules/client/.eslintrc create mode 100644 modules/client/.sc-from-rev create mode 100644 modules/client/.sc-from-rev-pretty delete mode 100644 modules/client/.vscode/settings.json delete mode 100644 modules/client/CONTRIBUTING.md delete mode 100644 modules/client/ISSUE_TEMPLATE.md delete mode 100644 modules/client/LICENSE.md delete mode 100644 modules/client/PULL_REQUEST_TEMPLATE.md delete mode 100644 modules/client/README.md diff --git a/modules/client/.babelrc b/modules/client/.babelrc deleted file mode 100644 index 659c0e9e4d..0000000000 --- a/modules/client/.babelrc +++ /dev/null @@ -1,15 +0,0 @@ -{ - "presets": [ - "react", - "env" - ], - "plugins": [ - [ - "transform-runtime", - { - "polyfill": false, - "regenerator": true - } - ] - ] -} diff --git a/modules/client/.eslintrc b/modules/client/.eslintrc deleted file mode 100644 index c7d8a3c899..0000000000 --- a/modules/client/.eslintrc +++ /dev/null @@ -1,55 +0,0 @@ -{ - "parser": "babel-eslint", - // To give you an idea how to override rule options: - "rules": { - "strict": 0, - "no-underscore-dangle": 0, - "no-unused-vars": [1, {"args": "none"}], - "curly": 0, - "no-multi-spaces": 0, - "key-spacing": 0, - "no-return-assign": 0, - "consistent-return": 0, - "no-shadow": 0, - "no-comma-dangle": 0, - "no-use-before-define": 0, - "no-empty": 0, - "new-parens": 0, - "no-cond-assign": 0, - "quotes": 0, - "camelcase": 0, - "new-cap": 0, - "no-undef": 2, - "comma-dangle": 0, - "space-infix-ops": 0, - "no-loop-func": 0, - "dot-notation": 0, - "semi": 0, - "semi-spacing": 1, - "no-spaced-func": 0, - "no-extra-semi": 0, - "no-trailing-spaces": 0, - "no-new-wrappers": 1, - "no-alert": 1, - "no-unused-expressions": 0, - "comma-spacing": 0, - "no-redeclare": 1, - "no-sequences": 1, - "no-debugger": 1, - "yoda": 0, - "eqeqeq": 0 - }, - "env": { - "node": true, - "es6": true, - }, - "globals": { - "web3": true, - "assert": true, - "window": true, - "document": true, - "navigator": true, - "location": true, - "fetch": true, - } -} diff --git a/modules/client/.gitignore b/modules/client/.gitignore index 26b6496b86..93cc1e8b27 100644 --- a/modules/client/.gitignore +++ b/modules/client/.gitignore @@ -1,12 +1,3 @@ -node_modules/ -dist/ -notes.txt -test/tests.txt -test/apiRoutes.txt -test/tests.txt -.env -*.sw[opq] -tags .yalc/ yalc.lock -.env +.env \ No newline at end of file diff --git a/modules/client/.sc-from-rev b/modules/client/.sc-from-rev new file mode 100644 index 0000000000..48dff5196b --- /dev/null +++ b/modules/client/.sc-from-rev @@ -0,0 +1 @@ +bfc927ac27ae13078b134ddb86cc7539b9248422 diff --git a/modules/client/.sc-from-rev-pretty b/modules/client/.sc-from-rev-pretty new file mode 100644 index 0000000000..b9b9ef1f2b --- /dev/null +++ b/modules/client/.sc-from-rev-pretty @@ -0,0 +1,30 @@ +commit bfc927ac27ae13078b134ddb86cc7539b9248422 +Author: David Wolever +Date: Tue Mar 5 18:45:21 2019 -0500 + + Fix toString() in doRequestDeposit + + Otherwise it formats as scientific notation + +diff --git a/hub/src/ChannelsService.ts b/hub/src/ChannelsService.ts +index 310e30ec6..ac44f3d90 100644 +--- a/hub/src/ChannelsService.ts ++++ b/hub/src/ChannelsService.ts +@@ -98,8 +98,8 @@ export default class ChannelsService { + + // assert user signed parameters + this.validator.assertDepositRequestSigner({ +- amountToken: depositToken.toString(), +- amountWei: depositWei.toString(), ++ amountToken: depositToken.toFixed(), ++ amountWei: depositWei.toFixed(), + sigUser, + }, user) + +On branch develop +Your branch is up to date with 'origin/develop'. + +You are currently bisecting, started from branch 'develop'. + (use "git bisect reset" to get back to the original branch) + +nothing to commit, working tree clean diff --git a/modules/client/.vscode/settings.json b/modules/client/.vscode/settings.json deleted file mode 100644 index c022e41338..0000000000 --- a/modules/client/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "restructuredtext.confPath": "" -} \ No newline at end of file diff --git a/modules/client/CONTRIBUTING.md b/modules/client/CONTRIBUTING.md deleted file mode 100644 index 1a39a5e6ee..0000000000 --- a/modules/client/CONTRIBUTING.md +++ /dev/null @@ -1,112 +0,0 @@ -**Copy of https://github.com/ConnextProject/docs/wiki/Contributing** - -We value curiosity, enthusiasm, and determination. Anyone who is excited about state channels and our vision is welcome to contribute to Connext. While we're always looking for developers, there are plenty of other ways to contribute through documentation, design, marketing, and even translation. If you're not sure how best to contribute, reach out to a team member and we'll be happy to find a way for you to help out. - -Our [documentation](https://github.com/ConnextProject/docs/wiki/Connext-Overview-and-Developer-Guide) includes information on Ethereum and state channels for both part-time and core contributors to the project, while [LearnChannels](https://learnchannels.org) provides a comprehensive introduction to state channel technology. - -Feel free to fork our repo and start creating PR’s after assigning yourself to an issue in the repo. We are always chatting on [Discord](https://discord.gg/yKkzZZm), so drop us a line there if you want to get more involved or have any questions on our implementation! - -We encourage contributors to: - -- Engage in Discord conversations and questions on how to begin contributing to the project -- Open up github issues to express interest in code to implement -- Open up PRs referencing any open issue in the repo. PRs should include: - - Detailed context of what would be required for merge - - Tests that are consistent with how other tests are written in our implementation - - Proper labels, milestones, and projects (see other closed PRs for reference) -- Follow up on open PRs -- Have an estimated timeframe to completion and let the core contributors know if a PR will take longer than expected -- We do not expect all part-time contributors to be experts on all the latest documentation, but all contributors should at least be familiarized with our documentation and LearnChannels. - -## Contribution Steps - -When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. - -Please note we have a code of conduct, please follow it in all your interactions with the project. - -Our pull request workflow is as follows: - -1. Ensure any install or build dependencies are removed before the last commit. -2. Update the README.md with details of changes to the interface, this includes new environment - variables, exposed ports, useful file locations and container parameters. -3. Increase the version numbers in any examples files and the README.md to the new version that this - Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). -4. You should sign your commits if possible with GPG. -5. Commits should be atomic changes with clear code comments and commit messages. -6. All builds must be passing, new functionality should be covered with tests. -7. Once you commit your changes, please tag a core team member to review them. -8. You may merge the Pull Request in once you have the sign-off of at least one core team member, or if you do not have permission to do that, you may request the reviewer to merge it for you. - -## Code of Conduct - -### Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -### Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -### Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -### Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -### Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at support@connext.network. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -### Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org diff --git a/modules/client/ISSUE_TEMPLATE.md b/modules/client/ISSUE_TEMPLATE.md deleted file mode 100644 index be4f816851..0000000000 --- a/modules/client/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,32 +0,0 @@ - - -## Expected Behavior - - - -## Current Behavior - - - -## Possible Solution - - - -## Steps to Reproduce (for bugs) - - -1. -2. -3. -4. - -## Context - - - -## Your Environment - -* Version used: -* Browser Name and version: -* Operating System and version (desktop or mobile): -* Link to your project: diff --git a/modules/client/LICENSE.md b/modules/client/LICENSE.md deleted file mode 100644 index 248e7294d5..0000000000 --- a/modules/client/LICENSE.md +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 Connext - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/modules/client/PULL_REQUEST_TEMPLATE.md b/modules/client/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 3d240bf1a7..0000000000 --- a/modules/client/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,28 +0,0 @@ - - -## Description - - -## Motivation and Context - - - -## How Has This Been Tested? - - - - -## Screenshots (if appropriate): - -## Types of changes - -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - -## Checklist: - - -- [ ] My code follows the code style of this project. -- [ ] My change requires a change to the documentation. -- [ ] I have updated the documentation accordingly. diff --git a/modules/client/README.md b/modules/client/README.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/modules/client/copy-sc-client b/modules/client/copy-sc-client index a5774aed19..23feba009f 100755 --- a/modules/client/copy-sc-client +++ b/modules/client/copy-sc-client @@ -9,4 +9,10 @@ set -eu cd "$(dirname "$0")" set -x -rsync -avl --exclude 'dist' --exclude 'node_modules' --exclude 'copy-sc-client' --prune-empty-dirs ../../../camsite/client/ . + +clientdir=../../../camsite/client/ + +rsync -avl --delete --exclude 'dist' --exclude '.sc-from-*' --exclude 'node_modules' --exclude 'copy-sc-client' --prune-empty-dirs "$clientdir" . +git -C "$clientdir" rev-parse HEAD > .sc-from-rev +git -C "$clientdir" show HEAD > .sc-from-rev-pretty +git -C "$clientdir" status >> .sc-from-rev-pretty From e78c565ee80abdb311b5c19f248382ca20584787 Mon Sep 17 00:00:00 2001 From: David Wolever Date: Wed, 6 Mar 2019 19:39:33 -0500 Subject: [PATCH 4/4] Merge in BigNumber fixes, intcomma --- modules/client/.sc-from-rev | 2 +- modules/client/.sc-from-rev-pretty | 24 +++---------- .../client/src/lib/currency/Currency.test.ts | 15 ++++++++ modules/client/src/lib/currency/Currency.ts | 6 ++-- modules/client/src/register/bignumber.test.ts | 34 +++++++++++++++++++ modules/client/src/register/bignumber.ts | 34 +++++++++++++++++++ modules/client/src/register/common.ts | 2 ++ 7 files changed, 94 insertions(+), 23 deletions(-) create mode 100644 modules/client/src/register/bignumber.test.ts create mode 100644 modules/client/src/register/bignumber.ts diff --git a/modules/client/.sc-from-rev b/modules/client/.sc-from-rev index 48dff5196b..9b2ceb7942 100644 --- a/modules/client/.sc-from-rev +++ b/modules/client/.sc-from-rev @@ -1 +1 @@ -bfc927ac27ae13078b134ddb86cc7539b9248422 +310119eb1ee22764b39a0512a44b9b64fbac4022 diff --git a/modules/client/.sc-from-rev-pretty b/modules/client/.sc-from-rev-pretty index b9b9ef1f2b..313357a616 100644 --- a/modules/client/.sc-from-rev-pretty +++ b/modules/client/.sc-from-rev-pretty @@ -1,26 +1,12 @@ -commit bfc927ac27ae13078b134ddb86cc7539b9248422 +commit 310119eb1ee22764b39a0512a44b9b64fbac4022 +Merge: 7090322c0 6c984b977 Author: David Wolever -Date: Tue Mar 5 18:45:21 2019 -0500 +Date: Wed Mar 6 19:39:00 2019 -0500 - Fix toString() in doRequestDeposit + Merge pull request #1385 from SpankChain/bignumber-to-string - Otherwise it formats as scientific notation + Error if BigNumber.toString is used -diff --git a/hub/src/ChannelsService.ts b/hub/src/ChannelsService.ts -index 310e30ec6..ac44f3d90 100644 ---- a/hub/src/ChannelsService.ts -+++ b/hub/src/ChannelsService.ts -@@ -98,8 +98,8 @@ export default class ChannelsService { - - // assert user signed parameters - this.validator.assertDepositRequestSigner({ -- amountToken: depositToken.toString(), -- amountWei: depositWei.toString(), -+ amountToken: depositToken.toFixed(), -+ amountWei: depositWei.toFixed(), - sigUser, - }, user) - On branch develop Your branch is up to date with 'origin/develop'. diff --git a/modules/client/src/lib/currency/Currency.test.ts b/modules/client/src/lib/currency/Currency.test.ts index 5d730a3800..f109b4c37a 100644 --- a/modules/client/src/lib/currency/Currency.test.ts +++ b/modules/client/src/lib/currency/Currency.test.ts @@ -2,6 +2,7 @@ import {assert, expect} from 'chai' import Currency from './Currency' import CurrencyConvertable from './CurrencyConvertable'; import { CurrencyType } from '../../state/ConnextState/CurrencyTypes' +import { parameterizedTests } from '../../testing' describe('Currency', () => { it('should return formatted currency', () => { @@ -53,4 +54,18 @@ describe('Currency', () => { .amount ).eq('69') }) + + describe('format', () => { + parameterizedTests([ + { name: 'zeros', input: 1, opts: { showTrailingZeros: true, decimals: 2 }, expected: '$1.00' }, + { name: 'no zeros 1', input: 1, opts: { showTrailingZeros: false, decimals: 2 }, expected: '$1' }, + { name: 'no zeros 2', input: 1.1, opts: { showTrailingZeros: false, decimals: 2 }, expected: '$1.1' }, + { name: 'decimals 1', input: 1.234, opts: { decimals: 2 }, expected: '$1.23' }, + { name: 'decimals 2', input: 1.234, opts: { decimals: 0 }, expected: '$1' }, + { name: 'decimals 3', input: 1.234, opts: undefined, expected: '$1.23' }, + { name: 'intcomma', input: 1234567.89, opts: undefined, expected: '$1,234,567.89' }, + ], t => { + assert.equal(Currency.USD(t.input).format(t.opts), t.expected) + }) + }) }) diff --git a/modules/client/src/lib/currency/Currency.ts b/modules/client/src/lib/currency/Currency.ts index 08cd391f4b..b5ec2a2c93 100644 --- a/modules/client/src/lib/currency/Currency.ts +++ b/modules/client/src/lib/currency/Currency.ts @@ -159,11 +159,11 @@ export default class Currency implements IC } let amount = options.decimals === undefined - ? amountBigNum.toString(10) - : amountBigNum.toNumber().toFixed(options.decimals) + ? amountBigNum.toFormat() + : amountBigNum.toFormat(options.decimals) if (!options.showTrailingZeros) { - amount = parseFloat(amount).toString() + amount = amount.replace(/\.?0*$/, '') } return `${symbol}${amount}` diff --git a/modules/client/src/register/bignumber.test.ts b/modules/client/src/register/bignumber.test.ts new file mode 100644 index 0000000000..62ffad03fc --- /dev/null +++ b/modules/client/src/register/bignumber.test.ts @@ -0,0 +1,34 @@ +import { assert } from '../testing' +import { BigNumber } from 'bignumber.js' + +describe('BigNumber', () => { + const num = new BigNumber('6.9e69') + + describe('toString', () => { + const origNodeEnv = process.env.NODE_ENV + afterEach(() => process.env.NODE_ENV = origNodeEnv) + + it('throws an error in test', () => { + assert.throws(() => num.toString(), /use .toFixed/i) + }) + + it('throws an error in development', () => { + process.env.NODE_ENV = 'development' + assert.throws(() => num.toString(), /use .toFixed/i) + }) + + it('forces base 10 in production', () => { + process.env.NODE_ENV = 'production' + assert.match(num.toString(), /690{68}/) + }) + }) + + it('uses fixed-point when converting to JSON', () => { + assert.match(JSON.stringify([num]), /\["690{68}"\]/) + }) + + it('uses fixed-point for .valueOf()', () => { + assert.match(num.valueOf(), /690{68}/) + }) + +}) diff --git a/modules/client/src/register/bignumber.ts b/modules/client/src/register/bignumber.ts new file mode 100644 index 0000000000..b9bc149bd7 --- /dev/null +++ b/modules/client/src/register/bignumber.ts @@ -0,0 +1,34 @@ +import {BigNumber} from "bignumber.js" + +const oldBigNumberToString = BigNumber.prototype.toString +BigNumber.prototype.toString = function(base?: number) { + // BigNumber.toString will only use exponential notation if a base is not + // specified. In dev, throw an error so it can be replaced with `.toFixed()`, + // or in production, log an error and force base 10. + if (!base) { + const err = new Error('Potentially unsafe of BigNumber.toString! Use .toFixed() instead.') + if (process.env.NODE_ENV != 'staging' && process.env.NODE_ENV != 'production') + throw err + + // In production, log an error and force base 10 to ensure the result is + // fixed-point. + console.error(err.stack) + base = 10 + } + + return oldBigNumberToString.call(this, base) +} + +BigNumber.prototype.toJSON = function() { + // By default BigNumber.toJSON will only use exponential notation for + // sufficiently large and small numbers. This is undesierable. Instead, + // force it to use fixed point. + return this.toFixed() +} + +BigNumber.prototype.valueOf = function() { + // By default BigNumber.valueOf will only use exponential notation for + // sufficiently large and small numbers. This is undesierable. Instead, + // force it to use fixed point. + return this.toFixed() +} diff --git a/modules/client/src/register/common.ts b/modules/client/src/register/common.ts index 247e4a3df4..3ca18aa023 100644 --- a/modules/client/src/register/common.ts +++ b/modules/client/src/register/common.ts @@ -4,6 +4,8 @@ * Does some minimal environment configuration. */ +require('./bignumber.ts') + // Bluebird has the ability to include the entire call stack in a Promise (ie, // including the original caller). // This incurs a 4x-5x performance penalty, though, so only use it in dev +