From 8e93be70e1f13692d1696653908796c786206eaa Mon Sep 17 00:00:00 2001 From: Liam Horne Date: Thu, 27 Dec 2018 17:06:52 +0000 Subject: [PATCH] [all] Introduction of AppRegistry and significant code improvements to facilitate it (#163) * wip * wip * passing tests2 * apps folder tests pass * lint * cleaner code * cleaner code * install protocol tests * set state tests * test improvements * setup protocol tests * some fixes * tests pass for all cf ops * touch ups * code cleanup * start of cleaning up proposer tests * update waffle * clean up tests for install proposer * continue to ensure tests pass * remove useless file * remove bad test * tests for all proposers * parallelize tests * rearrange deps * still wip * remove warning on yarn * some cleanups to package * considerable improvements to code quality in machine * remove 4447 json network * checkpointing stuff * commitment tests passing * mostly tested machine functionality * better testing * make explicit unused files * nice script stuff * fix tests after ethers update * updates * clean up tests to handle immutable * code cleanup * add comment * remove files that should not be here * remove files that should not be here * remove yarn clean * remove machine dependency on cf.js * minor readme touch up * fix lint * remove bold and normal in build.sh * Update packages/machine/src/models/state-channel.ts Co-Authored-By: snario * Update packages/machine/src/ethereum/install.ts Co-Authored-By: snario * Update packages/machine/src/models/app-instance.ts Co-Authored-By: snario * move utils to module import * typo * add doc * typo * replace console.log with tests * import from ethers/* * change dependencies to devDepencies for apps package * switch to resolver map * type arbitrary state objects better * use template literals for error messages * inline Map mutations for cleaner code * abridge if statement * Delegatecall -> DelegateCall * Update packages/machine/src/ethereum/utils/free-balance.ts Co-Authored-By: snario * remove unused class * Update packages/machine/src/ethereum/uninstall.ts Co-Authored-By: snario * Update packages/machine/src/ethereum/uninstall.ts Co-Authored-By: snario * Update packages/machine/src/ethereum/install.ts Co-Authored-By: snario * symlink .soliumrc.json * use ethers module imports some more * shorten tsconfig in @apps * add --detectOpenHandles back * shorten lines in .gitignore * re-sort .soliumrc.json file * uncomment critical line in StateChannelTransaction * Update packages/contracts/contracts/libs/LibStaticCall.sol Co-Authored-By: snario * better documentation of appStates mapping * better documentation of appResolutions mapping * rename _id to id in all .sol files * better docs on setState method * remove test:windows because its the same * re-version @types to 0.0.1 * rename TIMEOUT variable to ONCHAIN_CHALLENGE_TIMEOUT in a test * import Wallet in constants.ts * remove some commented out code * fresh conflict-free yarn.lock file * rename monotonicSeqNo according to Xuanjis's proposed scheme 'https://github.com/counterfactual/monorepo/pull/163#discussion_r242798293' * use ReadonlyMap not Readonly * Revert syntax error from github suggestion * Remove duplicate code * yarn.lock * fix solium to 1.1.8 * update waffle --- .circleci/config.yml | 9 +- .soliumrc.json | 28 + README.md | 15 +- build.sh | 6 +- package.json | 24 +- packages/apps/.gitignore | 1 + packages/apps/.solcover.js | 11 + packages/apps/.soliumignore | 2 + packages/apps/.soliumrc.json | 1 + .../contracts}/CommitRevealApp.sol | 2 +- .../contracts}/CountingApp.sol | 2 +- .../Nim.sol => apps/contracts/NimApp.sol} | 4 +- .../contracts}/PaymentApp.sol | 2 +- .../contracts/TicTacToeApp.sol} | 4 +- packages/apps/package.json | 34 + packages/apps/test/nim.spec.ts | 117 ++ packages/apps/test/payment.spec.ts | 47 + .../test/tictactoe.spec.ts} | 127 +- packages/apps/tsconfig.json | 4 + packages/apps/tslint.json | 6 + packages/apps/waffle.json | 3 + packages/cf.js/package.json | 11 +- packages/cf.js/rollup.config.js | 10 +- packages/cf.js/src/index.ts | 15 +- packages/cf.js/src/legacy/app-instance.ts | 12 +- packages/cf.js/src/legacy/app/index.ts | 50 +- packages/cf.js/src/legacy/channel.ts | 8 - .../src/legacy/eth-balance-refund-app.ts | 14 +- packages/cf.js/src/legacy/index.ts | 3 +- packages/cf.js/src/legacy/network.ts | 63 - packages/cf.js/src/legacy/node/index.ts | 4 +- packages/cf.js/src/legacy/types.ts | 4 +- .../cf.js/src/legacy/utils/free-balance.ts | 19 +- packages/cf.js/src/legacy/utils/index.ts | 13 - packages/cf.js/src/legacy/utils/nonce.ts | 4 +- .../cf.js/src/legacy/utils/peer-balance.ts | 17 +- packages/cf.js/src/legacy/utils/serializer.ts | 4 +- packages/cf.js/src/utils/abi.ts | 10 +- packages/cf.js/src/utils/signature.ts | 21 +- packages/cf.js/test/app-factory.spec.ts | 14 +- .../cf.js/test/legacy/app-instance.spec.ts | 4 +- packages/cf.js/test/utils/signature.spec.ts | 8 +- packages/common-types/package.json | 3 +- packages/common-types/src/node-protocol.ts | 11 + packages/contracts/.gitignore | 4 + packages/contracts/.solcover.js | 13 +- packages/contracts/.soliumrc.json | 29 +- packages/contracts/README.md | 22 +- packages/contracts/contracts/AppInstance.sol | 551 ----- packages/contracts/contracts/AppRegistry.sol | 20 + .../contracts/ConditionalTransaction.sol | 32 + .../{Registry.sol => ContractRegistry.sol} | 6 +- .../ETHVirtualAppAgreement.sol | 25 +- .../{delegateTargets => }/MultiSend.sol | 0 ...action.sol => StateChannelTransaction.sol} | 46 +- .../ETHBalanceRefundApp.sol | 2 +- .../contracts/default-apps/ETHBucket.sol | 39 + .../LibCondition.sol} | 15 +- .../Signatures.sol => libs/LibSignature.sol} | 10 +- .../contracts/libs/LibStateChannelApp.sol | 54 + .../StaticCall.sol => libs/LibStaticCall.sol} | 13 +- .../contracts/{lib => libs}/Transfer.sol | 0 .../contracts/contracts/mixins/MAppCaller.sol | 86 + .../contracts/mixins/MAppRegistryCore.sol | 114 ++ .../contracts/mixins/MixinAppRegistryCore.sol | 54 + .../contracts/mixins/MixinCancelChallenge.sol | 59 + .../contracts/mixins/MixinSetResolution.sol | 49 + .../contracts/mixins/MixinSetState.sol | 98 + .../mixins/MixinSetStateWithAction.sol | 153 ++ .../contracts/{external => proxies}/Proxy.sol | 5 +- .../{external => proxies}/ProxyFactory.sol | 0 .../MinimumViableMultisig.sol | 8 +- .../DelegateProxy.sol | 0 .../DolphinCoin.sol | 0 .../{fixtures => test-fixtures}/Echo.sol | 0 .../ExampleCondition.sol | 8 +- .../ExampleTransfer.sol | 2 +- .../ResolveToPay5WeiApp.sol} | 8 +- .../TestCaller.sol | 13 +- packages/contracts/index.ts | 1 - .../migrations/1_initial_migration.js | 11 +- .../migrations/2_deploy_libraries.js | 16 +- .../migrations/3_deploy_contracts.js | 32 +- packages/contracts/package.json | 38 +- packages/contracts/scripts/test.sh | 24 + .../contracts/test/app-registry-new.spec.ts | 82 + packages/contracts/test/app-registry.spec.ts | 221 ++ .../test/conditional-transaction.spec.ts | 141 ++ packages/contracts/test/constants.ts | 13 + .../contracts/test/contract-registry.spec.ts | 146 ++ .../test/eth-virtual-app-agreement.spec.ts | 263 +++ .../test/integration/commit-reveal.spec.ts | 206 -- .../test/integration/counting-app.spec.ts | 311 --- .../eth-virtual-app-agreement.spec.ts | 148 -- .../contracts/test/integration/nim.spec.ts | 115 -- .../integration/two-party-payments.spec.ts | 163 -- packages/contracts/test/libCondition.spec.ts | 116 ++ .../contracts/test/nonce-registry.spec.ts | 76 + packages/contracts/test/staticCall.spec.ts | 104 + packages/contracts/test/transfer.spec.ts | 164 ++ .../contracts/test/unit/appInstance.spec.ts | 221 -- .../contracts/test/unit/conditional.spec.ts | 122 -- .../test/unit/conditionalTransaction.spec.ts | 135 -- .../contracts/test/unit/nonceRegistry.spec.ts | 69 - packages/contracts/test/unit/registry.spec.ts | 156 -- .../contracts/test/unit/staticCall.spec.ts | 97 - packages/contracts/test/unit/transfer.spec.ts | 149 -- packages/contracts/test/utils/index.ts | 185 ++ packages/contracts/truffle-config.js | 10 +- packages/contracts/tsconfig.json | 3 +- packages/contracts/utils/appInstance.ts | 193 -- packages/contracts/utils/buildArtifacts.ts | 67 - packages/contracts/utils/contract.ts | 194 -- packages/contracts/utils/index.ts | 17 - packages/contracts/utils/misc.ts | 139 -- packages/contracts/utils/multisig.ts | 141 -- packages/contracts/utils/structEncoding.ts | 24 - packages/dapp-high-roller/tsconfig.json | 4 +- packages/machine/.env.example | 5 + packages/machine/.gitignore | 1 + packages/machine/README.md | 21 +- packages/machine/bin/deploy_contracts | 7 - packages/machine/bin/test | 6 - packages/machine/bin/test-debug | 7 - packages/machine/build | 1 + packages/machine/jest.config.js | 27 + packages/machine/launch.json | 17 - packages/machine/package.json | 58 +- packages/machine/rollup.config.js | 4 +- packages/machine/scripts/rollup.sh | 5 - packages/machine/scripts/test.sh | 61 + packages/machine/src/action.ts | 78 - packages/machine/src/flows/index.ts | 30 + .../machine/src/flows/install-virtual-app.ts | 56 +- packages/machine/src/flows/install.ts | 169 +- packages/machine/src/flows/setup.ts | 115 +- .../machine/src/flows/signature-forwarder.ts | 15 + packages/machine/src/flows/uninstall.ts | 161 +- packages/machine/src/flows/update.ts | 124 +- packages/machine/src/index.ts | 20 +- packages/machine/src/instruction-executor.ts | 311 ++- packages/machine/src/instructions.ts | 15 - packages/machine/src/middleware.ts | 54 + packages/machine/src/middleware/middleware.ts | 83 - .../middleware/protocol-operation/common.ts | 31 - .../middleware/protocol-operation/index.ts | 16 +- .../protocol-operation/multi-send-op.ts | 95 - .../protocol-operation/multisig-tx-op.ts | 55 - .../protocol-operation/op-generator.ts | 224 --- .../protocol-operation/op-install.ts | 99 +- .../protocol-operation/op-set-state.ts | 86 +- .../middleware/protocol-operation/op-setup.ts | 87 +- .../protocol-operation/op-uninstall.ts | 67 +- .../middleware/protocol-operation/types.ts | 83 +- .../protocol-operation/utils/app-identity.ts | 8 + .../protocol-operation/utils/encodings.ts | 35 + .../protocol-operation/utils/free-balance.ts | 69 + .../protocol-operation/utils/index.ts | 3 + .../protocol-operation/utils/multi-send-op.ts | 61 + .../utils/multisend-decoder.ts | 79 + .../utils/multisend-encoder.ts | 13 + .../utils/multisig-tx-op.ts | 46 + .../protocol-operation/utils/signature.ts | 26 + .../state-transition/install-proposer.ts | 158 -- .../state-transition/setup-proposer.ts | 55 - .../state-transition/uninstall-proposer.ts | 45 - .../state-transition/update-proposer.ts | 26 - packages/machine/src/mixins/apply.ts | 9 - packages/machine/src/mixins/index.ts | 4 - packages/machine/src/mixins/observable.ts | 25 - packages/machine/src/models/app-instance.ts | 238 +++ packages/machine/src/models/index.ts | 4 + packages/machine/src/models/state-channel.ts | 239 +++ packages/machine/src/node.ts | 91 - packages/machine/src/opcodes.ts | 7 +- packages/machine/src/protocol-types-tbd.ts | 59 + packages/machine/src/types.ts | 55 +- .../test/integration/cf-operations.spec.ts | 593 ------ .../test/integration/lifecycle/depositor.ts | 229 --- .../integration/lifecycle/setup-protocol.ts | 110 - .../lifecycle/three-player-lifecycle.spec.ts | 66 - .../lifecycle/tic-tac-toe-simulator.ts | 252 --- .../lifecycle/two-player-lifecycle.spec.ts | 53 - .../integration/setup-then-set-state.spec.ts | 204 ++ .../test/integration/test-commitment-store.ts | 255 --- .../test/integration/test-io-provider.ts | 101 - .../test/integration/test-key-value-store.ts | 57 - .../test/integration/test-response-sink.ts | 227 --- packages/machine/test/mocks.ts | 3 + .../machine/test/unit/cf-operations.spec.ts | 70 - .../test/unit/cf-operations/fixture.ts | 152 -- .../test/unit/cf-operations/install.spec.ts | 121 -- .../test/unit/cf-operations/set-state.spec.ts | 82 - .../test/unit/cf-operations/setup.spec.ts | 75 - .../test/unit/ethereum/install.spec.ts | 244 +++ .../test/unit/ethereum/set-state.spec.ts | 148 ++ .../machine/test/unit/ethereum/setup.spec.ts | 104 + .../test/unit/ethereum/uninstall.spec.ts | 217 ++ .../test/unit/instruction-executor.spec.ts | 112 ++ .../models/app-instance/app-instance.spec.ts | 47 + .../unit/models/state-channel/install.spec.ts | 115 ++ .../models/state-channel/set-state.spec.ts | 95 + .../state-channel/setup-channel.spec.ts | 115 ++ .../state-channel/state-channel.spec.ts | 22 + .../state-channel/uninstall-app.spec.ts | 89 + .../test/unit/state-transition.spec.ts | 184 -- packages/machine/test/utils/environment.ts | 19 - packages/machine/truffle-config.js | 19 + packages/machine/tsconfig.json | 9 +- packages/machine/tsconfig.test.json | 12 - packages/node-provider/package.json | 1 - packages/node/package.json | 3 +- packages/node/src/channels.ts | 4 +- packages/node/src/models.ts | 7 +- packages/node/src/node.ts | 6 +- packages/node/src/utils.ts | 8 +- .../integration/get-app-instances.spec.ts | 4 +- .../integration/node-communication.spec.ts | 4 +- .../node/test/integration/node-store.spec.ts | 17 +- packages/node/test/integration/utils.ts | 8 +- packages/playground/tsconfig.json | 4 +- packages/types/.gitignore | 1 + packages/types/package.json | 14 + packages/types/src/globals.d.ts | 6 + packages/types/src/index.ts | 61 + packages/types/tsconfig.json | 4 + packages/types/tslint.json | 3 + packages/typescript-typings/package.json | 2 + .../types/counterfactual/index.d.ts | 30 - .../types/ethereum-waffle/index.d.ts | 40 + tslint.json | 6 + yarn.lock | 1773 ++++++++++++++++- 232 files changed, 8159 insertions(+), 8323 deletions(-) create mode 100644 .soliumrc.json create mode 100644 packages/apps/.gitignore create mode 100644 packages/apps/.solcover.js create mode 100644 packages/apps/.soliumignore create mode 120000 packages/apps/.soliumrc.json rename packages/{contracts/contracts/appDefinitions => apps/contracts}/CommitRevealApp.sol (97%) rename packages/{contracts/contracts/appDefinitions => apps/contracts}/CountingApp.sol (96%) rename packages/{contracts/contracts/appDefinitions/Nim.sol => apps/contracts/NimApp.sol} (95%) rename packages/{contracts/contracts/appDefinitions => apps/contracts}/PaymentApp.sol (91%) rename packages/{contracts/contracts/appDefinitions/TicTacToe.sol => apps/contracts/TicTacToeApp.sol} (98%) create mode 100644 packages/apps/package.json create mode 100644 packages/apps/test/nim.spec.ts create mode 100644 packages/apps/test/payment.spec.ts rename packages/{contracts/test/integration/tic-tac-toe.spec.ts => apps/test/tictactoe.spec.ts} (52%) create mode 100644 packages/apps/tsconfig.json create mode 100644 packages/apps/tslint.json create mode 100644 packages/apps/waffle.json delete mode 100644 packages/cf.js/src/legacy/network.ts mode change 100644 => 120000 packages/contracts/.soliumrc.json delete mode 100644 packages/contracts/contracts/AppInstance.sol create mode 100644 packages/contracts/contracts/AppRegistry.sol create mode 100644 packages/contracts/contracts/ConditionalTransaction.sol rename packages/contracts/contracts/{Registry.sol => ContractRegistry.sol} (94%) rename packages/contracts/contracts/{delegateTargets => }/ETHVirtualAppAgreement.sol (87%) rename packages/contracts/contracts/{delegateTargets => }/MultiSend.sol (100%) rename packages/contracts/contracts/{delegateTargets/ConditionalTransaction.sol => StateChannelTransaction.sol} (51%) rename packages/contracts/contracts/{appDefinitions => default-apps}/ETHBalanceRefundApp.sol (95%) create mode 100644 packages/contracts/contracts/default-apps/ETHBucket.sol rename packages/contracts/contracts/{Conditional.sol => libs/LibCondition.sol} (82%) rename packages/contracts/contracts/{lib/Signatures.sol => libs/LibSignature.sol} (91%) create mode 100644 packages/contracts/contracts/libs/LibStateChannelApp.sol rename packages/contracts/contracts/{lib/StaticCall.sol => libs/LibStaticCall.sol} (90%) rename packages/contracts/contracts/{lib => libs}/Transfer.sol (100%) create mode 100644 packages/contracts/contracts/mixins/MAppCaller.sol create mode 100644 packages/contracts/contracts/mixins/MAppRegistryCore.sol create mode 100644 packages/contracts/contracts/mixins/MixinAppRegistryCore.sol create mode 100644 packages/contracts/contracts/mixins/MixinCancelChallenge.sol create mode 100644 packages/contracts/contracts/mixins/MixinSetResolution.sol create mode 100644 packages/contracts/contracts/mixins/MixinSetState.sol create mode 100644 packages/contracts/contracts/mixins/MixinSetStateWithAction.sol rename packages/contracts/contracts/{external => proxies}/Proxy.sol (91%) rename packages/contracts/contracts/{external => proxies}/ProxyFactory.sol (100%) rename packages/contracts/contracts/{ => state-deposit-holders}/MinimumViableMultisig.sol (96%) rename packages/contracts/contracts/{fixtures => test-fixtures}/DelegateProxy.sol (100%) rename packages/contracts/contracts/{fixtures => test-fixtures}/DolphinCoin.sol (100%) rename packages/contracts/contracts/{fixtures => test-fixtures}/Echo.sol (100%) rename packages/contracts/contracts/{fixtures => test-fixtures}/ExampleCondition.sol (71%) rename packages/contracts/contracts/{fixtures => test-fixtures}/ExampleTransfer.sol (87%) rename packages/contracts/contracts/{fixtures/ResolveToPay5ETHApp.sol => test-fixtures/ResolveToPay5WeiApp.sol} (83%) rename packages/contracts/contracts/{fixtures => test-fixtures}/TestCaller.sol (62%) delete mode 100644 packages/contracts/index.ts create mode 100755 packages/contracts/scripts/test.sh create mode 100644 packages/contracts/test/app-registry-new.spec.ts create mode 100644 packages/contracts/test/app-registry.spec.ts create mode 100644 packages/contracts/test/conditional-transaction.spec.ts create mode 100644 packages/contracts/test/constants.ts create mode 100644 packages/contracts/test/contract-registry.spec.ts create mode 100644 packages/contracts/test/eth-virtual-app-agreement.spec.ts delete mode 100644 packages/contracts/test/integration/commit-reveal.spec.ts delete mode 100644 packages/contracts/test/integration/counting-app.spec.ts delete mode 100644 packages/contracts/test/integration/eth-virtual-app-agreement.spec.ts delete mode 100644 packages/contracts/test/integration/nim.spec.ts delete mode 100644 packages/contracts/test/integration/two-party-payments.spec.ts create mode 100644 packages/contracts/test/libCondition.spec.ts create mode 100644 packages/contracts/test/nonce-registry.spec.ts create mode 100644 packages/contracts/test/staticCall.spec.ts create mode 100644 packages/contracts/test/transfer.spec.ts delete mode 100644 packages/contracts/test/unit/appInstance.spec.ts delete mode 100644 packages/contracts/test/unit/conditional.spec.ts delete mode 100644 packages/contracts/test/unit/conditionalTransaction.spec.ts delete mode 100644 packages/contracts/test/unit/nonceRegistry.spec.ts delete mode 100644 packages/contracts/test/unit/registry.spec.ts delete mode 100644 packages/contracts/test/unit/staticCall.spec.ts delete mode 100644 packages/contracts/test/unit/transfer.spec.ts create mode 100644 packages/contracts/test/utils/index.ts delete mode 100644 packages/contracts/utils/appInstance.ts delete mode 100644 packages/contracts/utils/buildArtifacts.ts delete mode 100644 packages/contracts/utils/contract.ts delete mode 100644 packages/contracts/utils/index.ts delete mode 100644 packages/contracts/utils/misc.ts delete mode 100644 packages/contracts/utils/multisig.ts delete mode 100644 packages/contracts/utils/structEncoding.ts create mode 100644 packages/machine/.env.example delete mode 100755 packages/machine/bin/deploy_contracts delete mode 100755 packages/machine/bin/test delete mode 100755 packages/machine/bin/test-debug create mode 120000 packages/machine/build create mode 100644 packages/machine/jest.config.js delete mode 100644 packages/machine/launch.json delete mode 100755 packages/machine/scripts/rollup.sh create mode 100755 packages/machine/scripts/test.sh delete mode 100644 packages/machine/src/action.ts create mode 100644 packages/machine/src/flows/index.ts create mode 100644 packages/machine/src/flows/signature-forwarder.ts delete mode 100644 packages/machine/src/instructions.ts create mode 100644 packages/machine/src/middleware.ts delete mode 100644 packages/machine/src/middleware/middleware.ts delete mode 100644 packages/machine/src/middleware/protocol-operation/common.ts delete mode 100644 packages/machine/src/middleware/protocol-operation/multi-send-op.ts delete mode 100644 packages/machine/src/middleware/protocol-operation/multisig-tx-op.ts delete mode 100644 packages/machine/src/middleware/protocol-operation/op-generator.ts create mode 100644 packages/machine/src/middleware/protocol-operation/utils/app-identity.ts create mode 100644 packages/machine/src/middleware/protocol-operation/utils/encodings.ts create mode 100644 packages/machine/src/middleware/protocol-operation/utils/free-balance.ts create mode 100644 packages/machine/src/middleware/protocol-operation/utils/index.ts create mode 100644 packages/machine/src/middleware/protocol-operation/utils/multi-send-op.ts create mode 100644 packages/machine/src/middleware/protocol-operation/utils/multisend-decoder.ts create mode 100644 packages/machine/src/middleware/protocol-operation/utils/multisend-encoder.ts create mode 100644 packages/machine/src/middleware/protocol-operation/utils/multisig-tx-op.ts create mode 100644 packages/machine/src/middleware/protocol-operation/utils/signature.ts delete mode 100644 packages/machine/src/middleware/state-transition/install-proposer.ts delete mode 100644 packages/machine/src/middleware/state-transition/setup-proposer.ts delete mode 100644 packages/machine/src/middleware/state-transition/uninstall-proposer.ts delete mode 100644 packages/machine/src/middleware/state-transition/update-proposer.ts delete mode 100644 packages/machine/src/mixins/apply.ts delete mode 100644 packages/machine/src/mixins/index.ts delete mode 100644 packages/machine/src/mixins/observable.ts create mode 100644 packages/machine/src/models/app-instance.ts create mode 100644 packages/machine/src/models/index.ts create mode 100644 packages/machine/src/models/state-channel.ts delete mode 100644 packages/machine/src/node.ts create mode 100644 packages/machine/src/protocol-types-tbd.ts delete mode 100644 packages/machine/test/integration/cf-operations.spec.ts delete mode 100644 packages/machine/test/integration/lifecycle/depositor.ts delete mode 100644 packages/machine/test/integration/lifecycle/setup-protocol.ts delete mode 100644 packages/machine/test/integration/lifecycle/three-player-lifecycle.spec.ts delete mode 100644 packages/machine/test/integration/lifecycle/tic-tac-toe-simulator.ts delete mode 100644 packages/machine/test/integration/lifecycle/two-player-lifecycle.spec.ts create mode 100644 packages/machine/test/integration/setup-then-set-state.spec.ts delete mode 100644 packages/machine/test/integration/test-commitment-store.ts delete mode 100644 packages/machine/test/integration/test-io-provider.ts delete mode 100644 packages/machine/test/integration/test-key-value-store.ts delete mode 100644 packages/machine/test/integration/test-response-sink.ts create mode 100644 packages/machine/test/mocks.ts delete mode 100644 packages/machine/test/unit/cf-operations.spec.ts delete mode 100644 packages/machine/test/unit/cf-operations/fixture.ts delete mode 100644 packages/machine/test/unit/cf-operations/install.spec.ts delete mode 100644 packages/machine/test/unit/cf-operations/set-state.spec.ts delete mode 100644 packages/machine/test/unit/cf-operations/setup.spec.ts create mode 100644 packages/machine/test/unit/ethereum/install.spec.ts create mode 100644 packages/machine/test/unit/ethereum/set-state.spec.ts create mode 100644 packages/machine/test/unit/ethereum/setup.spec.ts create mode 100644 packages/machine/test/unit/ethereum/uninstall.spec.ts create mode 100644 packages/machine/test/unit/instruction-executor.spec.ts create mode 100644 packages/machine/test/unit/models/app-instance/app-instance.spec.ts create mode 100644 packages/machine/test/unit/models/state-channel/install.spec.ts create mode 100644 packages/machine/test/unit/models/state-channel/set-state.spec.ts create mode 100644 packages/machine/test/unit/models/state-channel/setup-channel.spec.ts create mode 100644 packages/machine/test/unit/models/state-channel/state-channel.spec.ts create mode 100644 packages/machine/test/unit/models/state-channel/uninstall-app.spec.ts delete mode 100644 packages/machine/test/unit/state-transition.spec.ts delete mode 100644 packages/machine/test/utils/environment.ts create mode 100644 packages/machine/truffle-config.js delete mode 100644 packages/machine/tsconfig.test.json create mode 100644 packages/types/.gitignore create mode 100644 packages/types/package.json create mode 100644 packages/types/src/globals.d.ts create mode 100644 packages/types/src/index.ts create mode 100644 packages/types/tsconfig.json create mode 100644 packages/types/tslint.json delete mode 100644 packages/typescript-typings/types/counterfactual/index.d.ts create mode 100644 packages/typescript-typings/types/ethereum-waffle/index.d.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 9653e7db1..2829efbbb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,5 @@ version: 2 + jobs: build: docker: @@ -9,6 +10,7 @@ jobs: - restore_cache: key: dependency-cache-{{ checksum ".dependencies_checksum" }} - run: yarn + - run: yarn build - save_cache: key: dependency-cache-{{ checksum ".dependencies_checksum" }} paths: @@ -16,7 +18,7 @@ jobs: run-tests: docker: - - image: circleci/node:10-browsers + - image: circleci/node:10 steps: - checkout - run: cat packages/*/package.json | shasum > .dependencies_checksum @@ -24,11 +26,6 @@ jobs: key: dependency-cache-{{ checksum ".dependencies_checksum" }} - run: yarn # symlink packages' node_modules - run: yarn build - - run: - name: "Run ganache" - command: yarn ganache:ci - background: true - - run: cd packages/contracts/ && yarn migrate --network ganache - run: yarn test:ci-non-playground run-playground-tests: diff --git a/.soliumrc.json b/.soliumrc.json new file mode 100644 index 000000000..56ec72f7e --- /dev/null +++ b/.soliumrc.json @@ -0,0 +1,28 @@ +{ + "extends": "solium:all", + "plugins": ["security"], + "rules": { + "arg-overflow": ["error", 8], + "array-declarations": 1, + "camelcase": 1, + "deprecated-suicide": 1, + "imports-on-top": 1, + "indentation": ["error", 2], + "lbrace": 0, + "max-len": ["error", 80], + "mixedcase": 0, + "no-empty-blocks": 1, + "no-experimental": 0, + "no-unused-vars": 1, + "operator-whitespace": 1, + "pragma-on-top": 1, + "quotes": 1, + "security/enforce-explicit-visibility": ["error"], + "security/no-block-members": ["warning"], + "security/no-inline-assembly": 0, + "security/no-low-level-calls": 0, + "uppercase": 1, + "variable-declarations": 1, + "whitespace": 1 + } +} diff --git a/README.md b/README.md index 0786112e3..580dcc36b 100644 --- a/README.md +++ b/README.md @@ -71,20 +71,7 @@ yarn lint:fix ### Tests -Presently for some of the tests to work, you need to have a `ganache-cli` instance running in the background. To do this, run using: - -```shell -yarn ganache -``` - -You also need to migrate the contracts in the contracts package to generate a `networks` file which the `machine` package directly consume (for now). - -```shell -cd packages/contracts -yarn migrate --network ganache -``` - -Finally, to run all tests: +To run all tests: ```shell yarn test diff --git a/build.sh b/build.sh index 5ddbcc952..62d4599bf 100644 --- a/build.sh +++ b/build.sh @@ -6,11 +6,11 @@ set -e -packages="contracts common-types cf.js machine node node-provider playground-server playground dapp-high-roller" +packages="types apps contracts common-types cf.js machine node node-provider playground-server playground dapp-high-roller" for package in $packages; do - echo ">>> Building package: $package" - cd packages/$package + echo "⚙️ Building package: ${package}" + cd packages/${package} yarn build cd - done diff --git a/package.json b/package.json index f6199a28e..97de0d408 100644 --- a/package.json +++ b/package.json @@ -14,36 +14,20 @@ }, "scripts": { "build": "sh build.sh", - "clean": "lerna run clean --parallel --no-bail", + "clean": "git clean -Xdf --exclude=\"!.env\"", + "clean:dry": "git clean -Xdn --exclude=\"!.env\"", "test": "lerna run --stream --concurrency 1 test", - "test:ci-non-playground": "lerna run --ignore @counterfactual/playground --ignore @counterfactual/dapp-high-roller --stream test", - "ganache": "ganache-cli --networkId ${npm_package_config_ganacheNetworkID} --verbose --gasLimit ${npm_package_config_ganacheGasLimit} --gasPrice ${npm_package_config_ganacheGasPrice} --port ${npm_package_config_ganachePort} --deterministic --account=\"${npm_package_config_unlockedAccount0},${npm_package_config_etherBalance}\" --account=\"${npm_package_config_unlockedAccount1},${npm_package_config_etherBalance}\" --account=\"${npm_package_config_unlockedAccount2},${npm_package_config_etherBalance}\" --account=\"${npm_package_config_unlockedAccount3},${npm_package_config_etherBalance}\" &> /dev/null &", - "ganache:windows": "ganache-cli --networkId %npm_package_config_ganacheNetworkID% --verbose --gasLimit %npm_package_config_ganacheGasLimit% --gasPrice %npm_package_config_ganacheGasPrice% --port %npm_package_config_ganachePort% --deterministic --account=\"%npm_package_config_unlockedAccount0%,%npm_package_config_etherBalance%\" --account=\"%npm_package_config_unlockedAccount1%,%npm_package_config_etherBalance%\" --account=\"%npm_package_config_unlockedAccount2%,%npm_package_config_etherBalance%\" --account=\"%npm_package_config_unlockedAccount3%,%npm_package_config_etherBalance%\"", - "ganache:ci": "ganache-cli --networkId ${npm_package_config_ganacheNetworkID} --gasLimit ${npm_package_config_ganacheGasLimit} --gasPrice ${npm_package_config_ganacheGasPrice} --port ${npm_package_config_ganachePort} --deterministic --account=\"${npm_package_config_unlockedAccount0},${npm_package_config_etherBalance}\" --account=\"${npm_package_config_unlockedAccount1},${npm_package_config_etherBalance}\" --account=\"${npm_package_config_unlockedAccount2},${npm_package_config_etherBalance}\" --account=\"${npm_package_config_unlockedAccount3},${npm_package_config_etherBalance}\"", - "ganache:stop": "ps aux | grep ganache-cli | grep -v grep | awk '{print $2}' | xargs kill -9", + "test:ci-non-playground": "lerna run --ignore @counterfactual/playground --ignore @counterfactual/dapp-high-roller --stream --concurrency 1 test", "lint": "lerna run lint --parallel --no-bail", "lint:fix": "lerna run lint:fix --parallel --no-bail", "dev:playground": "lerna run start --parallel --scope=**/playground --scope=**/dapp-high-roller" }, - "config": { - "ganacheNetworkID": 7777777, - "ganachePort": 9545, - "ganacheGasLimit": "0xfffffffffff", - "ganacheGasPrice": "0x01", - "unlockedAccount0": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257a", - "unlockedAccount1": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257b", - "unlockedAccount2": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257c", - "unlockedAccount3": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d", - "etherBalance": "1000000000000000000000000" - }, "keywords": [ "ethereum", "counterfactual", - "state channels", - "off chain" + "state channels" ], "devDependencies": { - "ganache-cli": "6.2.5", "lerna": "^3.4.3", "prettier": "1.15.3", "tslint": "^5.11.0", diff --git a/packages/apps/.gitignore b/packages/apps/.gitignore new file mode 100644 index 000000000..378eac25d --- /dev/null +++ b/packages/apps/.gitignore @@ -0,0 +1 @@ +build diff --git a/packages/apps/.solcover.js b/packages/apps/.solcover.js new file mode 100644 index 000000000..ce0c5e560 --- /dev/null +++ b/packages/apps/.solcover.js @@ -0,0 +1,11 @@ +module.exports = { + testCommand: 'truffle test --network coverage lib/**/*.spec.js', + skipFiles: [ + "external/Proxy.sol", + "external/ProxyFactory.sol", + "lib/StaticCall.sol", + "delegateTargets/MultiSend.sol", + "Registry.sol", + "test/loadContracts.sol" + ] +}; diff --git a/packages/apps/.soliumignore b/packages/apps/.soliumignore new file mode 100644 index 000000000..ad7472515 --- /dev/null +++ b/packages/apps/.soliumignore @@ -0,0 +1,2 @@ +node_modules +coverageEnv diff --git a/packages/apps/.soliumrc.json b/packages/apps/.soliumrc.json new file mode 120000 index 000000000..3c6b43ed7 --- /dev/null +++ b/packages/apps/.soliumrc.json @@ -0,0 +1 @@ +../../.soliumrc.json \ No newline at end of file diff --git a/packages/contracts/contracts/appDefinitions/CommitRevealApp.sol b/packages/apps/contracts/CommitRevealApp.sol similarity index 97% rename from packages/contracts/contracts/appDefinitions/CommitRevealApp.sol rename to packages/apps/contracts/CommitRevealApp.sol index 3ee5ddf92..cc410d6c4 100644 --- a/packages/contracts/contracts/appDefinitions/CommitRevealApp.sol +++ b/packages/apps/contracts/CommitRevealApp.sol @@ -1,7 +1,7 @@ pragma solidity 0.4.25; pragma experimental "ABIEncoderV2"; -import "../lib/Transfer.sol"; +import "@counterfactual/contracts/contracts/libs/Transfer.sol"; contract CommitRevealApp { diff --git a/packages/contracts/contracts/appDefinitions/CountingApp.sol b/packages/apps/contracts/CountingApp.sol similarity index 96% rename from packages/contracts/contracts/appDefinitions/CountingApp.sol rename to packages/apps/contracts/CountingApp.sol index 479843d86..1710b0e77 100644 --- a/packages/contracts/contracts/appDefinitions/CountingApp.sol +++ b/packages/apps/contracts/CountingApp.sol @@ -1,7 +1,7 @@ pragma solidity 0.4.25; pragma experimental "ABIEncoderV2"; -import "../lib/Transfer.sol"; +import "@counterfactual/contracts/contracts/libs/Transfer.sol"; contract CountingApp { diff --git a/packages/contracts/contracts/appDefinitions/Nim.sol b/packages/apps/contracts/NimApp.sol similarity index 95% rename from packages/contracts/contracts/appDefinitions/Nim.sol rename to packages/apps/contracts/NimApp.sol index fb3b9684d..953213d46 100644 --- a/packages/contracts/contracts/appDefinitions/Nim.sol +++ b/packages/apps/contracts/NimApp.sol @@ -1,14 +1,14 @@ pragma solidity 0.4.25; pragma experimental "ABIEncoderV2"; -import "../lib/Transfer.sol"; +import "@counterfactual/contracts/contracts/libs/Transfer.sol"; /* Normal-form Nim https://en.wikipedia.org/wiki/Nim */ -contract Nim { +contract NimApp { struct Action { uint256 pileIdx; diff --git a/packages/contracts/contracts/appDefinitions/PaymentApp.sol b/packages/apps/contracts/PaymentApp.sol similarity index 91% rename from packages/contracts/contracts/appDefinitions/PaymentApp.sol rename to packages/apps/contracts/PaymentApp.sol index 9cf85dece..4c6454305 100644 --- a/packages/contracts/contracts/appDefinitions/PaymentApp.sol +++ b/packages/apps/contracts/PaymentApp.sol @@ -1,7 +1,7 @@ pragma solidity 0.4.25; pragma experimental "ABIEncoderV2"; -import "../lib/Transfer.sol"; +import "@counterfactual/contracts/contracts/libs/Transfer.sol"; contract PaymentApp { diff --git a/packages/contracts/contracts/appDefinitions/TicTacToe.sol b/packages/apps/contracts/TicTacToeApp.sol similarity index 98% rename from packages/contracts/contracts/appDefinitions/TicTacToe.sol rename to packages/apps/contracts/TicTacToeApp.sol index edd701b66..2da830e98 100644 --- a/packages/contracts/contracts/appDefinitions/TicTacToe.sol +++ b/packages/apps/contracts/TicTacToeApp.sol @@ -1,10 +1,10 @@ pragma solidity 0.4.25; pragma experimental "ABIEncoderV2"; -import "../lib/Transfer.sol"; +import "@counterfactual/contracts/contracts/libs/Transfer.sol"; -contract TicTacToe { +contract TicTacToeApp { enum ActionType { PLAY, diff --git a/packages/apps/package.json b/packages/apps/package.json new file mode 100644 index 000000000..eeb3067a3 --- /dev/null +++ b/packages/apps/package.json @@ -0,0 +1,34 @@ +{ + "name": "@counterfactual/apps", + "version": "0.0.1", + "description": "Collection of various apps built on Counterfactual", + "repository": "github.com/counterfactual/monorepo", + "license": "MIT", + "private": true, + "files": [ + "build" + ], + "scripts": { + "build": "waffle waffle.json", + "test": "ts-mocha test/*", + "lint:fix": "yarn lint:ts:fix && yarn lint:sol:fix", + "lint": "yarn lint:ts && yarn lint:sol", + "lint:sol:fix": "solium -d contracts/ --fix", + "lint:sol": "solium -d .", + "lint:ts:fix": "tslint -c tslint.json -p . --fix", + "lint:ts": "tslint -c tslint.json -p ." + }, + "devDependencies": { + "@counterfactual/contracts": "0.0.3", + "@counterfactual/typescript-typings": "0.0.1", + "@types/chai": "^4.1.7", + "@types/mocha": "^5.2.5", + "chai": "^4.2.0", + "ethereum-waffle": "1.2.0", + "ethers": "4.0.20", + "mocha": "^4.1.0", + "solium": "1.1.8", + "ts-mocha": "^2.0.0", + "tslint": "^5.11.0" + } +} diff --git a/packages/apps/test/nim.spec.ts b/packages/apps/test/nim.spec.ts new file mode 100644 index 000000000..abe9a0704 --- /dev/null +++ b/packages/apps/test/nim.spec.ts @@ -0,0 +1,117 @@ +import chai from "chai"; +import * as waffle from "ethereum-waffle"; +import { Contract } from "ethers"; +import { AddressZero } from "ethers/constants"; +import { BigNumber, defaultAbiCoder } from "ethers/utils"; + +import NimApp from "../build/NimApp.json"; + +chai.use(waffle.solidity); + +const { expect } = chai; + +type NimAppState = { + players: string[]; + turnNum: BigNumber; + pileHeights: BigNumber[]; +}; + +function decodeAppState(encodedAppState: string): NimAppState { + return defaultAbiCoder.decode( + ["tuple(address[2] players, uint256 turnNum, uint256[3] pileHeights)"], + encodedAppState + )[0]; +} + +describe("Nim", () => { + let nim: Contract; + + before(async () => { + const provider = waffle.createMockProvider(); + const wallet = (await waffle.getWallets(provider))[0]; + nim = await waffle.deployContract(wallet, NimApp); + }); + + describe("applyAction", () => { + it("can take from a pile", async () => { + const preState = { + players: [AddressZero, AddressZero], + turnNum: 0, + pileHeights: [6, 5, 12] + }; + + const action = { + pileIdx: 0, + takeAmnt: 5 + }; + + const ret = await nim.functions.applyAction(preState, action); + + const postState = decodeAppState(ret); + + expect(postState.pileHeights[0]).to.eq(1); + expect(postState.pileHeights[1]).to.eq(5); + expect(postState.pileHeights[2]).to.eq(12); + expect(postState.turnNum).to.eq(1); + }); + + it("can take to produce an empty pile", async () => { + const preState = { + players: [AddressZero, AddressZero], + turnNum: 0, + pileHeights: [6, 5, 12] + }; + + const action = { + pileIdx: 0, + takeAmnt: 6 + }; + + const ret = await nim.functions.applyAction(preState, action); + + const postState = decodeAppState(ret); + + expect(postState.pileHeights[0]).to.eq(0); + expect(postState.pileHeights[1]).to.eq(5); + expect(postState.pileHeights[2]).to.eq(12); + expect(postState.turnNum).to.eq(1); + }); + + it("should fail for taking too much", async () => { + const preState = { + players: [AddressZero, AddressZero], + turnNum: 0, + pileHeights: [6, 5, 12] + }; + + const action = { + pileIdx: 0, + takeAmnt: 7 + }; + + await expect( + nim.functions.applyAction(preState, action) + ).to.be.revertedWith("invalid pileIdx"); + }); + }); + + describe("isFinal", () => { + it("empty state is final", async () => { + const preState = { + players: [AddressZero, AddressZero], + turnNum: 49, + pileHeights: [0, 0, 0] + }; + expect(await nim.functions.isStateTerminal(preState)).to.eq(true); + }); + + it("nonempty state is not final", async () => { + const preState = { + players: [AddressZero, AddressZero], + turnNum: 49, + pileHeights: [0, 1, 0] + }; + expect(await nim.functions.isStateTerminal(preState)).to.eq(false); + }); + }); +}); diff --git a/packages/apps/test/payment.spec.ts b/packages/apps/test/payment.spec.ts new file mode 100644 index 000000000..ef5c7b7ae --- /dev/null +++ b/packages/apps/test/payment.spec.ts @@ -0,0 +1,47 @@ +import chai from "chai"; +import * as waffle from "ethereum-waffle"; +import { Contract } from "ethers"; +import { AddressZero, WeiPerEther } from "ethers/constants"; + +import PaymentApp from "../build/PaymentApp.json"; + +chai.use(waffle.solidity); + +const { expect } = chai; + +const [A, B] = [ + "0xb37e49bFC97A948617bF3B63BC6942BB15285715", + "0xaeF082d339D227646DB914f0cA9fF02c8544F30b" +]; + +describe("PaymentApp", () => { + let pc: Contract; + + before(async () => { + const provider = waffle.createMockProvider(); + const wallet = (await waffle.getWallets(provider))[0]; + pc = await waffle.deployContract(wallet, PaymentApp); + }); + + it("should resolve to payments", async () => { + const ret = await pc.functions.resolve( + { + alice: A, + bob: B, + aliceBalance: WeiPerEther, + bobBalance: WeiPerEther + }, + { + assetType: 0, + limit: WeiPerEther.mul(2), + token: AddressZero + } + ); + expect(ret.assetType).to.eq(0); + expect(ret.token).to.eq(AddressZero); + expect(ret.to[0]).to.eq(A); + expect(ret.to[1]).to.eq(B); + expect(ret.value[0]).to.eq(WeiPerEther); + expect(ret.value[1]).to.eq(WeiPerEther); + }); +}); diff --git a/packages/contracts/test/integration/tic-tac-toe.spec.ts b/packages/apps/test/tictactoe.spec.ts similarity index 52% rename from packages/contracts/test/integration/tic-tac-toe.spec.ts rename to packages/apps/test/tictactoe.spec.ts index 873d23464..116aadadd 100644 --- a/packages/contracts/test/integration/tic-tac-toe.spec.ts +++ b/packages/apps/test/tictactoe.spec.ts @@ -1,30 +1,44 @@ -import { ethers } from "ethers"; +import chai from "chai"; +import * as waffle from "ethereum-waffle"; +import { Contract } from "ethers"; +import { AddressZero } from "ethers/constants"; +import { defaultAbiCoder } from "ethers/utils"; -import { AbstractContract, buildArtifacts, expect } from "../../utils"; -import * as Utils from "../../utils/misc"; +import TicTacToeApp from "../build/TicTacToeApp.json"; -const web3 = (global as any).web3; -const { unlockedAccount } = Utils.setupTestEnv(web3); +chai.use(waffle.solidity); -contract("TicTacToe", (accounts: string[]) => { - let game: ethers.Contract; +const { expect } = chai; - const stateEncoding = - "tuple(address[2] players, uint256 turnNum, uint256 winner, uint256[3][3] board)"; +type TicTacToeAppState = { + players: string[]; + turnNum: number; + winner: number; + board: number[][]; +}; + +function decodeAppState(encodedAppState: string): TicTacToeAppState { + return defaultAbiCoder.decode( + [ + "tuple(address[2] players, uint256 turnNum, uint256 winner, uint256[3][3] board)" + ], + encodedAppState + )[0]; +} + +describe("TicTacToeApp", () => { + let tictactoe: Contract; - // @ts-ignore before(async () => { - const staticCall = buildArtifacts.StaticCall; - const ticTacToe = await AbstractContract.fromArtifactName("TicTacToe", { - StaticCall: staticCall - }); - game = await ticTacToe.deploy(unlockedAccount); + const provider = waffle.createMockProvider(); + const wallet = (await waffle.getWallets(provider))[0]; + tictactoe = await waffle.deployContract(wallet, TicTacToeApp); }); describe("applyAction", () => { it("can place into an empty board", async () => { const preState = { - players: [ethers.constants.AddressZero, ethers.constants.AddressZero], + players: [AddressZero, AddressZero], turnNum: 0, winner: 0, board: [[0, 0, 0], [0, 0, 0], [0, 0, 0]] @@ -40,20 +54,17 @@ contract("TicTacToe", (accounts: string[]) => { } }; - const ret = await game.functions.applyAction(preState, action); + const ret = await tictactoe.functions.applyAction(preState, action); - const state = ethers.utils.defaultAbiCoder.decode( - [stateEncoding], - ret - )[0]; + const state = decodeAppState(ret); - expect(state.board[0][0]).to.be.eql(new ethers.utils.BigNumber(1)); - expect(state.turnNum).to.be.eql(new ethers.utils.BigNumber(1)); + expect(state.board[0][0]).to.eq(1); + expect(state.turnNum).to.eq(1); }); it("can place into an empty square", async () => { const preState = { - players: [ethers.constants.AddressZero, ethers.constants.AddressZero], + players: [AddressZero, AddressZero], turnNum: 1, winner: 0, board: [[1, 0, 0], [0, 0, 0], [0, 0, 0]] @@ -69,20 +80,17 @@ contract("TicTacToe", (accounts: string[]) => { } }; - const ret = await game.functions.applyAction(preState, action); + const ret = await tictactoe.functions.applyAction(preState, action); - const state = ethers.utils.defaultAbiCoder.decode( - [stateEncoding], - ret - )[0]; + const state = decodeAppState(ret); - expect(state.board[1][1]).to.be.eql(new ethers.utils.BigNumber(2)); - expect(state.turnNum).to.be.eql(new ethers.utils.BigNumber(2)); + expect(state.board[1][1]).to.eq(2); + expect(state.turnNum).to.eq(2); }); it("cannot placeinto an occupied square", async () => { const preState = { - players: [ethers.constants.AddressZero, ethers.constants.AddressZero], + players: [AddressZero, AddressZero], turnNum: 0, winner: 0, board: [[1, 0, 0], [0, 0, 0], [0, 0, 0]] @@ -98,12 +106,14 @@ contract("TicTacToe", (accounts: string[]) => { } }; - await Utils.assertRejects(game.functions.applyAction(preState, action)); + await expect( + tictactoe.functions.applyAction(preState, action) + ).to.be.revertedWith("playMove: square is not empty"); }); it("can draw from a full board", async () => { const preState = { - players: [ethers.constants.AddressZero, ethers.constants.AddressZero], + players: [AddressZero, AddressZero], turnNum: 0, winner: 0, board: [[1, 2, 1], [1, 2, 2], [2, 1, 2]] @@ -119,19 +129,16 @@ contract("TicTacToe", (accounts: string[]) => { } }; - const ret = await game.functions.applyAction(preState, action); + const ret = await tictactoe.functions.applyAction(preState, action); - const state = ethers.utils.defaultAbiCoder.decode( - [stateEncoding], - ret - )[0]; + const state = decodeAppState(ret); - expect(state.winner).to.be.eql(new ethers.utils.BigNumber(3)); // DRAWN + expect(state.winner).to.eq(3); // DRAWN }); it("cannot draw from a non-full board", async () => { const preState = { - players: [ethers.constants.AddressZero, ethers.constants.AddressZero], + players: [AddressZero, AddressZero], turnNum: 0, winner: 0, board: [[1, 2, 1], [1, 0, 2], [2, 1, 2]] @@ -147,12 +154,14 @@ contract("TicTacToe", (accounts: string[]) => { } }; - await Utils.assertRejects(game.functions.applyAction(preState, action)); + await expect( + tictactoe.functions.applyAction(preState, action) + ).to.be.revertedWith("assertBoardIsFull: square is empty"); }); it("can play_and_draw from an almost full board", async () => { const preState = { - players: [ethers.constants.AddressZero, ethers.constants.AddressZero], + players: [AddressZero, AddressZero], turnNum: 0, winner: 0, board: [[0, 2, 1], [1, 2, 2], [2, 1, 2]] @@ -168,19 +177,16 @@ contract("TicTacToe", (accounts: string[]) => { } }; - const ret = await game.functions.applyAction(preState, action); + const ret = await tictactoe.functions.applyAction(preState, action); - const state = ethers.utils.defaultAbiCoder.decode( - [stateEncoding], - ret - )[0]; + const state = decodeAppState(ret); - expect(state.winner).to.be.eql(new ethers.utils.BigNumber(3)); // DRAWN + expect(state.winner).to.eq(3); // DRAWN }); it("can notplay_and_draw from a sparse board", async () => { const preState = { - players: [ethers.constants.AddressZero, ethers.constants.AddressZero], + players: [AddressZero, AddressZero], turnNum: 0, winner: 0, board: [[0, 2, 1], [1, 2, 2], [2, 0, 0]] @@ -196,12 +202,14 @@ contract("TicTacToe", (accounts: string[]) => { } }; - await Utils.assertRejects(game.functions.applyAction(preState, action)); + await expect( + tictactoe.functions.applyAction(preState, action) + ).to.be.revertedWith("assertBoardIsFull: square is empty"); }); it("can play_and_win from a winning position", async () => { const preState = { - players: [ethers.constants.AddressZero, ethers.constants.AddressZero], + players: [AddressZero, AddressZero], turnNum: 0, winner: 0, board: [[1, 1, 0], [0, 0, 0], [0, 0, 0]] @@ -217,19 +225,16 @@ contract("TicTacToe", (accounts: string[]) => { } }; - const ret = await game.functions.applyAction(preState, action); + const ret = await tictactoe.functions.applyAction(preState, action); - const state = ethers.utils.defaultAbiCoder.decode( - [stateEncoding], - ret - )[0]; + const state = decodeAppState(ret); - expect(state.winner).to.be.eql(new ethers.utils.BigNumber(1)); // WON + expect(state.winner).to.eq(1); // WON }); it("cannot play_and_win from a non winning position", async () => { const preState = { - players: [ethers.constants.AddressZero, ethers.constants.AddressZero], + players: [AddressZero, AddressZero], turnNum: 0, winner: 0, board: [[1, 0, 0], [0, 0, 0], [0, 0, 0]] @@ -245,7 +250,9 @@ contract("TicTacToe", (accounts: string[]) => { } }; - await Utils.assertRejects(game.functions.applyAction(preState, action)); + await expect( + tictactoe.functions.applyAction(preState, action) + ).to.be.revertedWith("Win Claim not valid"); }); }); }); diff --git a/packages/apps/tsconfig.json b/packages/apps/tsconfig.json new file mode 100644 index 000000000..84540293d --- /dev/null +++ b/packages/apps/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["test"] +} diff --git a/packages/apps/tslint.json b/packages/apps/tslint.json new file mode 100644 index 000000000..5c9e26102 --- /dev/null +++ b/packages/apps/tslint.json @@ -0,0 +1,6 @@ +{ + "extends": ["../../tslint.json"], + "linterOptions": { + "exclude": ["build/*.json"] + } +} diff --git a/packages/apps/waffle.json b/packages/apps/waffle.json new file mode 100644 index 000000000..836f4e34a --- /dev/null +++ b/packages/apps/waffle.json @@ -0,0 +1,3 @@ +{ + "npmPath": "../../node_modules" +} diff --git a/packages/cf.js/package.json b/packages/cf.js/package.json index ed7047386..c0e593f58 100644 --- a/packages/cf.js/package.json +++ b/packages/cf.js/package.json @@ -12,12 +12,11 @@ "license": "MIT", "private": true, "scripts": { - "clean": "rm -rf .rpt2_cache dist", "build": "tsc -b && rollup -c", "lint": "tslint -c tslint.json -p tsconfig.json", - "lint:fix": "tslint -c tslint.json -p tsconfig.json --fix", - "test": "jest --runInBand --detectOpenHandles --bail", - "test-debug-ide": "node $NODE_DEBUG_OPTION ./node_modules/.bin/jest --runInBand" + "lint:fix": "yarn lint --fix", + "test": "jest --detectOpenHandles", + "test-debug-ide": "node $NODE_DEBUG_OPTION ./node_modules/.bin/jest" }, "devDependencies": { "jest": "23.6.0", @@ -27,10 +26,10 @@ "typescript": "^3.1.2" }, "dependencies": { - "@counterfactual/contracts": "0.0.2", + "@counterfactual/contracts": "0.0.3", "@counterfactual/common-types": "0.0.1", "cuid": "^2.1.4", - "ethers": "^4.0.17", + "ethers": "4.0.20", "eventemitter3": "^3.1.0" }, "jest": { diff --git a/packages/cf.js/rollup.config.js b/packages/cf.js/rollup.config.js index b892d8381..c45191c6c 100644 --- a/packages/cf.js/rollup.config.js +++ b/packages/cf.js/rollup.config.js @@ -8,14 +8,17 @@ export default { output: [ { file: pkg.main, - format: "cjs" + format: "cjs", + exports: "named", }, { file: pkg.iife, name: "window.cf", format: "iife", + exports: "named", globals: { - "ethers/utils": "ethers.utils" + "ethers/utils": "ethers.utils", + "ethers/constants": "ethers.constants" } } ], @@ -24,8 +27,7 @@ export default { json({ include: [ // FIXME: these shouldn't be required - "../contracts/build/contracts/ETHBalanceRefundApp.json", - "../contracts/build/contracts/AppInstance.json" + "../contracts/build/contracts/ETHBalanceRefundApp.json" ] }) ] diff --git a/packages/cf.js/src/index.ts b/packages/cf.js/src/index.ts index c77b6f0bc..12fcd185b 100644 --- a/packages/cf.js/src/index.ts +++ b/packages/cf.js/src/index.ts @@ -1,8 +1,17 @@ +import { AppFactory } from "./app-factory"; import * as legacy from "./legacy"; +import { Provider } from "./provider"; import * as types from "./types"; import * as utils from "./utils"; -export { legacy, types, utils }; +const cf = { + AppFactory, + Provider, + legacy, + types, + utils +}; -export { Provider } from "./provider"; -export { AppFactory } from "./app-factory"; +export { AppFactory, Provider, legacy, types, utils }; + +export default cf; diff --git a/packages/cf.js/src/legacy/app-instance.ts b/packages/cf.js/src/legacy/app-instance.ts index 09a57e48b..2a2b7241f 100644 --- a/packages/cf.js/src/legacy/app-instance.ts +++ b/packages/cf.js/src/legacy/app-instance.ts @@ -1,4 +1,4 @@ -import { ethers } from "ethers"; +import { formatParamType, Interface, ParamType } from "ethers/utils"; import { Terms } from "./app"; import { AbiEncodings, AppDefinition } from "./types"; @@ -13,25 +13,23 @@ export class AppInstance { // TODO: temp hack necessary until ethers support https://github.com/ethers-io/ethers.js/issues/325 static generateAbiEncodings( - abi: string | (string | ethers.utils.ParamType)[] + abi: string | (string | ParamType)[] ): AbiEncodings { - const iface = new ethers.utils.Interface(abi); + const iface = new Interface(abi); const appFunctionNames = Object.keys(iface.functions).filter(fn => { return fn.indexOf("(") === -1; }); const appActions = appFunctionNames.map(fn => { const inputs = iface.functions[fn].inputs; const tuples = inputs.map(input => { - return ethers.utils.formatParamType(input); + return formatParamType(input); }); return `${fn}(${tuples.join(",")})`; }); return { - appStateEncoding: ethers.utils.formatParamType( - iface.functions.resolve.inputs[0] - ), + appStateEncoding: formatParamType(iface.functions.resolve.inputs[0]), appActionEncoding: JSON.stringify([appActions.join(",")]) }; } diff --git a/packages/cf.js/src/legacy/app/index.ts b/packages/cf.js/src/legacy/app/index.ts index 60558723e..96f388661 100644 --- a/packages/cf.js/src/legacy/app/index.ts +++ b/packages/cf.js/src/legacy/app/index.ts @@ -1,14 +1,11 @@ -import AppInstanceJson from "@counterfactual/contracts/build/contracts/AppInstance.json"; -import { ethers } from "ethers"; +import { HashZero } from "ethers/constants"; +import { BigNumber, defaultAbiCoder, keccak256 } from "ethers/utils"; import * as abi from "../../utils/abi"; import { StateChannelInfo } from "../channel"; -import { NetworkContext } from "../network"; import { Address, Bytes, Bytes4, H256, PeerBalance } from "../utils"; import { Nonce } from "../utils/nonce"; -const { keccak256 } = ethers.utils; - /** * Maps 1-1 with AppInstance.sol (with the addition of the uniqueId, which * is used to calculate the cf address). @@ -17,31 +14,28 @@ const { keccak256 } = ethers.utils; */ export class AppInstance { constructor( - readonly ctx: NetworkContext, readonly owner: Address, readonly signingKeys: Address[], readonly cfApp: AppInterface, readonly terms: Terms, - readonly timeout: number, - readonly uniqueId: number + readonly timeout: number ) {} public cfAddress(): H256 { - // FIXME: shouldn't have to require abi and bytecode here - const initcode = new ethers.utils.Interface( - AppInstanceJson.abi - ).deployFunction.encode(this.ctx.linkedBytecode(AppInstanceJson.bytecode), [ - this.owner, - this.signingKeys, - this.cfApp.hash(), - this.terms.hash(), - this.timeout - ]); - return keccak256( - abi.encodePacked( - ["bytes1", "bytes", "uint256"], - ["0x19", initcode, this.uniqueId] + defaultAbiCoder.encode( + [ + "tuple(address owner,address[] signingKeys,bytes32 appInterfaceHash,bytes32 termsHash,uint256 defaultTimeout)" + ], + [ + { + owner: this.owner, + signingKeys: this.signingKeys, + appInterfaceHash: this.cfApp.hash(), + termsHash: this.terms.hash(), + defaultTimeout: this.timeout + } + ] ) ); } @@ -73,7 +67,7 @@ export class AppInterface { console.error( "WARNING: Can't compute hash for AppInterface because its address is 0x0" ); - return ethers.constants.HashZero; + return HashZero; } return keccak256( abi.encode( @@ -97,7 +91,7 @@ export class AppInterface { export class Terms { constructor( readonly assetType: number, - readonly limit: ethers.utils.BigNumber, + readonly limit: BigNumber, readonly token: Address ) {} @@ -124,8 +118,8 @@ export interface UpdateData { } export interface UninstallOptions { - peerABalance: ethers.utils.BigNumber; - peerBBalance: ethers.utils.BigNumber; + peerABalance: BigNumber; + peerBBalance: BigNumber; } export interface InstallData { @@ -144,8 +138,8 @@ export interface InstallOptions { stateEncoding: string; abiEncoding: string; state: object; - peerABalance: ethers.utils.BigNumber; - peerBBalance: ethers.utils.BigNumber; + peerABalance: BigNumber; + peerBBalance: BigNumber; } export interface AppInstanceInfo { diff --git a/packages/cf.js/src/legacy/channel.ts b/packages/cf.js/src/legacy/channel.ts index c527a1ad4..75176dc41 100644 --- a/packages/cf.js/src/legacy/channel.ts +++ b/packages/cf.js/src/legacy/channel.ts @@ -7,14 +7,6 @@ export interface StateChannelInfo { multisigAddress: Address; appInstances: AppInstanceInfos; freeBalance: FreeBalance; - - // TODO: Move this out of the datastructure - // https://github.com/counterfactual/monorepo/issues/127 - /** - * @returns the addresses of the owners of this state channel sorted - * in alphabetical order. - */ - owners(): string[]; } // a mapping from multisig address to a StateChannelInfo struct containing diff --git a/packages/cf.js/src/legacy/eth-balance-refund-app.ts b/packages/cf.js/src/legacy/eth-balance-refund-app.ts index 09ac070e3..c01630db0 100644 --- a/packages/cf.js/src/legacy/eth-balance-refund-app.ts +++ b/packages/cf.js/src/legacy/eth-balance-refund-app.ts @@ -1,5 +1,5 @@ -import * as ETHBalanceRefundAppContract from "@counterfactual/contracts/build/contracts/ETHBalanceRefundApp.json"; -import { ethers } from "ethers"; +import BuildArtifact from "@counterfactual/contracts/build/contracts/ETHBalanceRefundApp.json"; +import { AddressZero, Zero } from "ethers/constants"; import { Terms } from "./app"; import { AppInstance } from "./app-instance"; @@ -8,14 +8,8 @@ import { AppDefinition } from "./types"; export class ETHBalanceRefundApp extends AppInstance { constructor(appAddress: string, signingKeys: string[]) { const timeout = 100; - const terms = new Terms( - 0, - ethers.utils.bigNumberify("0"), - ethers.constants.AddressZero - ); - const abiEncodings = AppInstance.generateAbiEncodings( - ETHBalanceRefundAppContract.abi - ); + const terms = new Terms(0, Zero, AddressZero); + const abiEncodings = AppInstance.generateAbiEncodings(BuildArtifact.abi); const appDefinition: AppDefinition = { address: appAddress, diff --git a/packages/cf.js/src/legacy/index.ts b/packages/cf.js/src/legacy/index.ts index e152cc0e0..dc69a959a 100644 --- a/packages/cf.js/src/legacy/index.ts +++ b/packages/cf.js/src/legacy/index.ts @@ -1,8 +1,7 @@ import * as app from "./app"; import * as channel from "./channel"; import { NotificationType } from "./mixins/observable"; -import * as network from "./network"; import * as node from "./node"; import * as utils from "./utils"; -export { app, channel, network, node, NotificationType, utils }; +export { app, channel, node, NotificationType, utils }; diff --git a/packages/cf.js/src/legacy/network.ts b/packages/cf.js/src/legacy/network.ts deleted file mode 100644 index 3b904b78c..000000000 --- a/packages/cf.js/src/legacy/network.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { ethers } from "ethers"; - -import { Address } from "./utils"; - -/** - * A network context is a set of addresses at which global contracts are - * deployed. A global contract provides functionality in such a way that - * all channels can use the same global contract, hence they only need to be - * deployed once. Examples of non-global contracts are the Multisig and - * the AppInstance contracts. - * - * @function linkedBytecode Given bytecode in solc's pre-linkage format (where - * global contract addresses are replaced with placeholders of the form - * `________`; see the description of "placeholder" at - * https://solidity.readthedocs.io/en/v0.4.24/contracts.html#libraries), return - * the linked bytecode after linking with the list of addresses. - */ -export class NetworkContext { - // FIXME: This is just bad practice :S - // https://github.com/counterfactual/monorepo/issues/143 - private contractToVar = { - Registry: "registryAddr", - PaymentApp: "paymentAppAddr", - ConditionalTransaction: "conditionalTransactionAddr", - MultiSend: "multiSendAddr", - NonceRegistry: "nonceRegistryAddr", - Signatures: "signaturesAddr", - StaticCall: "staticCallAddr", - ETHBalanceRefundApp: "ethBalanceRefundAppAddr" - }; - - constructor( - readonly registryAddr: Address, - readonly paymentAppAddr: Address, - readonly conditionalTransactionAddr: Address, - readonly multiSendAddr: Address, - readonly nonceRegistryAddr: Address, - readonly signaturesAddr: Address, - readonly staticCallAddr: Address, - readonly ethBalanceRefundAppAddr: Address - ) {} - - public linkedBytecode(unlinkedBytecode: string): string { - let bytecode = unlinkedBytecode; - for (const contractName in this.contractToVar) { - const regex = new RegExp(`__${contractName}_+`, "g"); - const address = this[this.contractToVar[contractName]].substr(2); - bytecode = bytecode.replace(regex, address); - } - return bytecode; - } -} - -export const EMPTY_NETWORK_CONTEXT = new NetworkContext( - ethers.constants.AddressZero, - ethers.constants.AddressZero, - ethers.constants.AddressZero, - ethers.constants.AddressZero, - ethers.constants.AddressZero, - ethers.constants.AddressZero, - ethers.constants.AddressZero, - ethers.constants.AddressZero -); diff --git a/packages/cf.js/src/legacy/node/index.ts b/packages/cf.js/src/legacy/node/index.ts index fb8a0eb9a..9e397bb8d 100644 --- a/packages/cf.js/src/legacy/node/index.ts +++ b/packages/cf.js/src/legacy/node/index.ts @@ -1,4 +1,4 @@ -import { ethers } from "ethers"; +import { Signature } from "ethers/utils"; // FIXME: move operation action names away from client action names // https://github.com/counterfactual/monorepo/issues/144 @@ -47,5 +47,5 @@ export interface ClientActionMessage { toAddress: string; fromAddress: string; seq: number; - signature?: ethers.utils.Signature; + signature?: Signature; } diff --git a/packages/cf.js/src/legacy/types.ts b/packages/cf.js/src/legacy/types.ts index 1c9ee5224..c9705241f 100644 --- a/packages/cf.js/src/legacy/types.ts +++ b/packages/cf.js/src/legacy/types.ts @@ -1,4 +1,4 @@ -import { ethers } from "ethers"; +import { BigNumber } from "ethers/utils"; export interface AppDefinition { address: string; @@ -12,7 +12,7 @@ export interface AbiEncodings { } export interface Deposits { - [s: string]: ethers.utils.BigNumber; + [s: string]: BigNumber; } export type DeserializationCondition = { diff --git a/packages/cf.js/src/legacy/utils/free-balance.ts b/packages/cf.js/src/legacy/utils/free-balance.ts index 2b1c4cc05..ad663a619 100644 --- a/packages/cf.js/src/legacy/utils/free-balance.ts +++ b/packages/cf.js/src/legacy/utils/free-balance.ts @@ -1,7 +1,7 @@ -import { ethers } from "ethers"; +import { AddressZero } from "ethers/constants"; +import { BigNumber, Interface, parseEther } from "ethers/utils"; import { AppInterface, Terms } from "../app"; -import { NetworkContext } from "../network"; import { Address } from "."; import { Nonce } from "./nonce"; @@ -16,15 +16,14 @@ export class FreeBalance { // https://github.com/counterfactual/monorepo/issues/118 return new Terms( 0, // 0 means ETH - ethers.utils.parseEther("0.001"), // FIXME: un-hardcode (https://github.com/counterfactual/monorepo/issues/117) - ethers.constants.AddressZero + parseEther("0.001"), // FIXME: un-hardcode (https://github.com/counterfactual/monorepo/issues/117) + AddressZero ); } - public static contractInterface(ctx: NetworkContext): AppInterface { - const address = ctx.paymentAppAddr; + public static contractInterface(address: string): AppInterface { const applyAction = "0x00000000"; // not used - const resolver = new ethers.utils.Interface([ + const resolver = new Interface([ // TODO: Put this somewhere eh // https://github.com/counterfactual/monorepo/issues/134 "resolve(tuple(address,address,uint256,uint256),tuple(uint8,uint256,address))" @@ -43,16 +42,16 @@ export class FreeBalance { constructor( readonly alice: Address, // first person in free balance object - readonly aliceBalance: ethers.utils.BigNumber, + readonly aliceBalance: BigNumber, readonly bob: Address, // second person in free balance object - readonly bobBalance: ethers.utils.BigNumber, + readonly bobBalance: BigNumber, readonly uniqueId: number, readonly localNonce: number, readonly timeout: number, readonly dependencyNonce: Nonce ) {} - public balanceOfAddress(address: Address): ethers.utils.BigNumber { + public balanceOfAddress(address: Address): BigNumber { if (address === this.alice) return this.aliceBalance; if (address === this.bob) return this.bobBalance; throw Error(`address ${address} not in free balance`); diff --git a/packages/cf.js/src/legacy/utils/index.ts b/packages/cf.js/src/legacy/utils/index.ts index 8c1d94b38..be537401c 100644 --- a/packages/cf.js/src/legacy/utils/index.ts +++ b/packages/cf.js/src/legacy/utils/index.ts @@ -1,5 +1,3 @@ -import { ethers } from "ethers"; - import { FreeBalance } from "./free-balance"; import { Nonce } from "./nonce"; import { CanonicalPeerBalance, PeerBalance } from "./peer-balance"; @@ -17,14 +15,3 @@ export type Bytes4 = string; // fixed-size byte arrays export type Bytes32 = string; export type Address = string; // ethereum address (i.e. rightmost 20 bytes of keccak256 of ECDSA pubkey) export type H256 = string; // a bytes32 which is the output of the keccak256 hash function - -export async function mineOneBlock(provider: ethers.providers.JsonRpcProvider) { - return provider.send("evm_mine", []); -} - -export async function mineBlocks( - n: number, - provider: ethers.providers.JsonRpcProvider -) { - Array.from({ length: n }).forEach(async () => await mineOneBlock(provider)); -} diff --git a/packages/cf.js/src/legacy/utils/nonce.ts b/packages/cf.js/src/legacy/utils/nonce.ts index 74051a529..b0c833c99 100644 --- a/packages/cf.js/src/legacy/utils/nonce.ts +++ b/packages/cf.js/src/legacy/utils/nonce.ts @@ -1,11 +1,9 @@ -import { ethers } from "ethers"; +import { keccak256 } from "ethers/utils"; import { abi } from "../../utils"; import { Bytes32 } from "."; -const { keccak256 } = ethers.utils; - export class Nonce { public isSet: boolean; public salt: Bytes32; diff --git a/packages/cf.js/src/legacy/utils/peer-balance.ts b/packages/cf.js/src/legacy/utils/peer-balance.ts index 0ce932df1..cf222e2a3 100644 --- a/packages/cf.js/src/legacy/utils/peer-balance.ts +++ b/packages/cf.js/src/legacy/utils/peer-balance.ts @@ -1,6 +1,6 @@ -import { ethers } from "ethers"; +import { BigNumber, bigNumberify } from "ethers/utils"; -import { Address } from "."; +import { Address } from "./index"; export class PeerBalance { /** @@ -8,9 +8,9 @@ export class PeerBalance { */ public static balances( address1: Address, - balance1: ethers.utils.BigNumber, + balance1: BigNumber, address2: Address, - balance2: ethers.utils.BigNumber + balance2: BigNumber ): CanonicalPeerBalance { if (address2.localeCompare(address1) < 0) { return new CanonicalPeerBalance( @@ -61,13 +61,10 @@ export class PeerBalance { ) ]; } - public balance: ethers.utils.BigNumber; + public balance: BigNumber; - constructor( - readonly address: Address, - balance: number | ethers.utils.BigNumber - ) { - this.balance = ethers.utils.bigNumberify(balance.toString()); + constructor(readonly address: Address, balance: number | BigNumber) { + this.balance = bigNumberify(balance.toString()); } } diff --git a/packages/cf.js/src/legacy/utils/serializer.ts b/packages/cf.js/src/legacy/utils/serializer.ts index 04e463a9e..34bfcc990 100644 --- a/packages/cf.js/src/legacy/utils/serializer.ts +++ b/packages/cf.js/src/legacy/utils/serializer.ts @@ -1,4 +1,4 @@ -import { ethers } from "ethers"; +import { bigNumberify } from "ethers/utils"; import { DeserializationCase } from "../types"; @@ -24,7 +24,7 @@ const isSignature = data => const deserializeArray = data => data.map(value => deserialize(value)); -const deserializeBigNumber = data => ethers.utils.bigNumberify(data._hex); +const deserializeBigNumber = data => bigNumberify(data._hex); const identity = data => data; diff --git a/packages/cf.js/src/utils/abi.ts b/packages/cf.js/src/utils/abi.ts index e4ec5d6e1..8b4b4faf2 100644 --- a/packages/cf.js/src/utils/abi.ts +++ b/packages/cf.js/src/utils/abi.ts @@ -1,13 +1,13 @@ -import { ethers } from "ethers"; +import { Arrayish, defaultAbiCoder, solidityPack } from "ethers/utils"; export function encode(types: string[], values: any[]) { - return ethers.utils.defaultAbiCoder.encode(types, values); + return defaultAbiCoder.encode(types, values); } -export function decode(types: string[], data: ethers.utils.Arrayish) { - return ethers.utils.defaultAbiCoder.decode(types, data); +export function decode(types: string[], data: Arrayish) { + return defaultAbiCoder.decode(types, data); } export function encodePacked(types: string[], values: any[]) { - return ethers.utils.solidityPack(types, values); + return solidityPack(types, values); } diff --git a/packages/cf.js/src/utils/signature.ts b/packages/cf.js/src/utils/signature.ts index e6e631449..abd069b15 100644 --- a/packages/cf.js/src/utils/signature.ts +++ b/packages/cf.js/src/utils/signature.ts @@ -1,11 +1,14 @@ import { Bytes32 } from "@counterfactual/common-types"; -import { ethers } from "ethers"; +import { + BigNumber, + joinSignature, + recoverAddress, + Signature +} from "ethers/utils"; -export function signaturesToBytes( - ...signatures: ethers.utils.Signature[] -): string { +export function signaturesToBytes(...signatures: Signature[]): string { const signaturesHexString = signatures - .map(ethers.utils.joinSignature) + .map(joinSignature) .map(s => s.substr(2)) .join(""); return `0x${signaturesHexString}`; @@ -13,13 +16,13 @@ export function signaturesToBytes( export function signaturesToSortedBytes( digest: Bytes32, - ...signatures: ethers.utils.Signature[] + ...signatures: Signature[] ): string { const sigs = signatures.slice(); sigs.sort((sigA, sigB) => { - const addrA = ethers.utils.recoverAddress(digest, signaturesToBytes(sigA)); - const addrB = ethers.utils.recoverAddress(digest, signaturesToBytes(sigB)); - return new ethers.utils.BigNumber(addrA).lt(addrB) ? -1 : 1; + const addrA = recoverAddress(digest, signaturesToBytes(sigA)); + const addrB = recoverAddress(digest, signaturesToBytes(sigB)); + return new BigNumber(addrA).lt(addrB) ? -1 : 1; }); return signaturesToBytes(...sigs); } diff --git a/packages/cf.js/test/app-factory.spec.ts b/packages/cf.js/test/app-factory.spec.ts index 9a4e5bfdf..afbbe2e5a 100644 --- a/packages/cf.js/test/app-factory.spec.ts +++ b/packages/cf.js/test/app-factory.spec.ts @@ -1,5 +1,5 @@ import { AssetType, Node } from "@counterfactual/common-types"; -import { ethers } from "ethers"; +import { parseEther } from "ethers/utils"; import { AppFactory } from "../src/app-factory"; import { Provider } from "../src/provider"; @@ -37,7 +37,7 @@ describe("CF.js AppFactory", () => { expect(request.type).toBe(Node.MethodName.PROPOSE_INSTALL); const params = request.params as Node.ProposeInstallParams; expect(params.initialState).toBe(testState); - expect(params.myDeposit).toEqual(ethers.utils.parseEther("0.5")); + expect(params.myDeposit).toEqual(parseEther("0.5")); nodeProvider.simulateMessageFromNode({ type: Node.MethodName.PROPOSE_INSTALL, requestId: request.requestId, @@ -51,8 +51,8 @@ describe("CF.js AppFactory", () => { asset: { assetType: AssetType.ETH }, - peerDeposit: ethers.utils.parseEther("0.5"), - myDeposit: ethers.utils.parseEther("0.5"), + peerDeposit: parseEther("0.5"), + myDeposit: parseEther("0.5"), timeout: "100", initialState: testState }); @@ -66,8 +66,8 @@ describe("CF.js AppFactory", () => { asset: { assetType: AssetType.ETH }, - peerDeposit: ethers.utils.parseEther("0.5"), - myDeposit: ethers.utils.parseEther("0.5"), + peerDeposit: parseEther("0.5"), + myDeposit: parseEther("0.5"), timeout: "100", initialState: "4000" }); @@ -86,7 +86,7 @@ describe("CF.js AppFactory", () => { asset: { assetType: AssetType.ETH }, - peerDeposit: ethers.utils.parseEther("0.5"), + peerDeposit: parseEther("0.5"), myDeposit: "$%GARBAGE$%", timeout: "100", initialState: "4000" diff --git a/packages/cf.js/test/legacy/app-instance.spec.ts b/packages/cf.js/test/legacy/app-instance.spec.ts index db2bdb42c..e3e433dcd 100644 --- a/packages/cf.js/test/legacy/app-instance.spec.ts +++ b/packages/cf.js/test/legacy/app-instance.spec.ts @@ -1,11 +1,11 @@ -import * as ETHBalanceRefundAppContract from "@counterfactual/contracts/build/contracts/ETHBalanceRefundApp.json"; +import ETHBalanceRefundApp from "@counterfactual/contracts/build/contracts/ETHBalanceRefundApp.json"; import { AppInstance } from "../../src/legacy/app-instance"; describe("AppInstance", async () => { it("generateAbiEncodings correctly generates a appStateEncoding and appActionEncoding", () => { const abiEncodings = AppInstance.generateAbiEncodings( - ETHBalanceRefundAppContract.abi + ETHBalanceRefundApp.abi ); expect(abiEncodings.appStateEncoding).toEqual( diff --git a/packages/cf.js/test/utils/signature.spec.ts b/packages/cf.js/test/utils/signature.spec.ts index 8700fb19d..c4e981bc2 100644 --- a/packages/cf.js/test/utils/signature.spec.ts +++ b/packages/cf.js/test/utils/signature.spec.ts @@ -1,14 +1,14 @@ -import { ethers } from "ethers"; +import { keccak256, SigningKey, toUtf8Bytes } from "ethers/utils"; import { signaturesToBytes, signaturesToSortedBytes } from "../../src/utils"; const privateKey = "0x0123456789012345678901234567890123456789012345678901234567890123"; -const signingKey = new ethers.utils.SigningKey(privateKey); +const signingKey = new SigningKey(privateKey); const signaturesAndDigests = ["sig1", "sig3", "sig2"].map(message => { - const bytes = ethers.utils.toUtf8Bytes(message); - const digest = ethers.utils.keccak256(bytes); + const bytes = toUtf8Bytes(message); + const digest = keccak256(bytes); const signature = signingKey.signDigest(digest); return { diff --git a/packages/common-types/package.json b/packages/common-types/package.json index ae8066622..761e8bbf3 100644 --- a/packages/common-types/package.json +++ b/packages/common-types/package.json @@ -8,7 +8,6 @@ "license": "MIT", "private": true, "scripts": { - "clean": "rm -rf dist", "build": "tsc -b && rollup -c", "lint": "tslint -c tslint.json -p tsconfig.json", "lint:fix": "tslint -c tslint.json -p tsconfig.json --fix" @@ -18,7 +17,7 @@ "typescript": "^3.1.2" }, "dependencies": { - "ethers": "^4.0.17", + "ethers": "4.0.20", "rollup-plugin-typescript2": "^0.18.0" } } diff --git a/packages/common-types/src/node-protocol.ts b/packages/common-types/src/node-protocol.ts index 7fbc025e2..8e21e5c5a 100644 --- a/packages/common-types/src/node-protocol.ts +++ b/packages/common-types/src/node-protocol.ts @@ -13,6 +13,17 @@ export interface INodeProvider { } export namespace Node { + export type NetworkContext = { + // Protocol + ConditionalTransaction: Address; + MultiSend: Address; + NonceRegistry: Address; + AppRegistry: Address; + // App-specific + PaymentApp: Address; + ETHBalanceRefund: Address; + }; + export enum ErrorType { ERROR = "error" } diff --git a/packages/contracts/.gitignore b/packages/contracts/.gitignore index f8de17b72..3147f1481 100644 --- a/packages/contracts/.gitignore +++ b/packages/contracts/.gitignore @@ -40,3 +40,7 @@ networks/*.json !networks/61717561.json .env + +test/**/*.js +test/**/*js.map +test/**/*.d.ts diff --git a/packages/contracts/.solcover.js b/packages/contracts/.solcover.js index ce0c5e560..7948db695 100644 --- a/packages/contracts/.solcover.js +++ b/packages/contracts/.solcover.js @@ -1,11 +1,10 @@ module.exports = { - testCommand: 'truffle test --network coverage lib/**/*.spec.js', + testCommand: 'truffle test --network coverage lib/*.spec.js', skipFiles: [ - "external/Proxy.sol", - "external/ProxyFactory.sol", - "lib/StaticCall.sol", - "delegateTargets/MultiSend.sol", - "Registry.sol", - "test/loadContracts.sol" + "proxies/Proxy.sol", + "proxies/ProxyFactory.sol", + "libs/LibStaticCall.sol", + "MultiSend.sol", + "ContractRegistry.sol" ] }; diff --git a/packages/contracts/.soliumrc.json b/packages/contracts/.soliumrc.json deleted file mode 100644 index a7717d67a..000000000 --- a/packages/contracts/.soliumrc.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "extends": "solium:all", - "plugins": ["security"], - "rules": { - "arg-overflow": ["error", 8], - "array-declarations": 1, - "camelcase": 1, - "deprecated-suicide": 1, - "imports-on-top": 1, - "indentation": ["error", 2], - "lbrace": 1, - "max-len": ["error", 80], - "mixedcase": 0, - "no-empty-blocks": 1, - "no-experimental": 0, - "no-unused-vars": 1, - "operator-whitespace": 1, - "pragma-on-top": 1, - "quotes": 1, - "security/enforce-explicit-visibility": ["error"], - "security/no-block-members": ["warning"], - "security/no-inline-assembly": 0, - "security/no-low-level-calls": 0, - "uppercase": 1, - "variable-declarations": 1, - "whitespace": 1 - } -} diff --git a/packages/contracts/.soliumrc.json b/packages/contracts/.soliumrc.json new file mode 120000 index 000000000..3c6b43ed7 --- /dev/null +++ b/packages/contracts/.soliumrc.json @@ -0,0 +1 @@ +../../.soliumrc.json \ No newline at end of file diff --git a/packages/contracts/README.md b/packages/contracts/README.md index bd159870f..78b4d9cf3 100644 --- a/packages/contracts/README.md +++ b/packages/contracts/README.md @@ -12,9 +12,9 @@ To install the dependencies: yarn ``` -### Building the package +### Compiling Solidity -To build the machine package: +To compile the Solidity source code into bytecode and ABI, run: ```shell yarn build @@ -22,20 +22,6 @@ yarn build ### Tests -To ensure correctness of the Solidity contracts, the tests run against `ganache-cli` instance running in the background. To do this, run: - -```shell -cd ../../ -yarn ganache -cd packages/contracts -``` - -You also need to migrate the contracts in the contracts package to generate a `networks` file which the tests consume: - -```shell -yarn migrate --network ganache -``` - To run all tests: ```shell @@ -45,11 +31,9 @@ yarn test To run only specific tests: ```shell -yarn run tsc -b -yarn run truffle test --network ganache dist/test/ +yarn test [test/.spec.ts ...] ``` - # Migrations The `networks` folder contains the migration files for the different Ethereum networks the contracts have been migrated to. The ID of the respective networks are used as file names. diff --git a/packages/contracts/contracts/AppInstance.sol b/packages/contracts/contracts/AppInstance.sol deleted file mode 100644 index b8ae0725a..000000000 --- a/packages/contracts/contracts/AppInstance.sol +++ /dev/null @@ -1,551 +0,0 @@ -pragma solidity ^0.4.25; -pragma experimental "ABIEncoderV2"; - -import "./lib/Signatures.sol"; -import "./lib/StaticCall.sol"; -import "./lib/Transfer.sol"; - - -/// @title AppInstance - A contract that defines a state channel application -/// @author Liam Horne - -/// @notice Supports the adjudication and timeout guarantees required by state channel -/// applications to be secure in a gas and storage-optimized manner. Resolves to a -/// `Transfer.Transaction` when the channel is closed. -contract AppInstance { - - using StaticCall for address; - using Signatures for bytes; - - event DisputeStarted( - address sender, - uint256 disputeCounter, - bytes32 appStateHash, - uint256 nonce, - uint256 finalizesAt - ); - - event DisputeProgressed( - address sender, - bytes appState, - bytes action, - bytes toState, - uint256 disputeNonce, - uint256 finalizesAt - ); - - event DisputeFinalized( - address sender, - bytes finalState - ); - - event DisputeCancelled( - address sender - ); - - enum Status { - ON, - DISPUTE, - OFF - } - - struct Auth { - address owner; - address[] signingKeys; - } - - struct App { - address addr; - bytes4 applyAction; - bytes4 resolve; - bytes4 getTurnTaker; - bytes4 isStateTerminal; - } - - struct State { - Status status; - bytes32 appStateHash; - address latestSubmitter; - uint256 nonce; - uint256 disputeNonce; - uint256 finalizesAt; - uint256 disputeCounter; - } - - Auth public auth; - State public state; - Transfer.Transaction public resolution; - - bytes32 private appHash; - bytes32 private termsHash; - uint256 private defaultTimeout; - - modifier onlyWhenChannelOpen() { - require( - !isStateTerminal(state), - "State has already been settled" - ); - _; - } - - modifier onlyWhenChannelDispute() { - require( - state.status == Status.DISPUTE, - "State is not being disputed" - ); - _; - } - - modifier onlyWhenChannelClosed() { - require(isStateTerminal(state), "State is still unsettled"); - _; - } - - /// @notice Contract constructor - /// @param owner An address which is allowed to set state. Typically an address off a multisig wallet. - /// @param signingKeys An array of addresses which can set state with unanimous consent - /// @param app Hash of the application's interface - /// @param terms Hash of a `Transfer.Terms` object commiting to the terms of the app - /// @param timeout The default timeout (in blocks) in case of dispute - constructor( - address owner, - address[] signingKeys, - bytes32 app, - bytes32 terms, - uint256 timeout - ) public { - auth.owner = owner; - auth.signingKeys = signingKeys; - termsHash = terms; - appHash = app; - defaultTimeout = timeout; - } - - /// @notice A getter function for the owner of the state channel - /// @return The address of the `owner` - function getOwner() external view returns (address) { - return auth.owner; - } - - /// @notice A getter function for the signing keys of the state channel - /// @return The addresses of the `signingKeys` - function getSigningKeys() external view returns (address[]) { - return auth.signingKeys; - } - - /// @notice A getter function for the latest agreed nonce of the state channel - /// @return The uint value of the latest agreed nonce - function latestNonce() external view returns (uint256) { - return state.nonce; - } - - /// @notice A helper method to determine whether or not the channel is closed - /// @return A boolean representing whether or not the state channel is closed - function isClosed() external view returns (bool) { - return isStateTerminal(state); - } - - /// @notice A getter function for the resolution if one is set - /// @return A `Transfer.Transaction` object representing the resolution of the channel - function getResolution() - public - view - onlyWhenChannelClosed - returns (Transfer.Transaction) - { - return resolution; - } - - /// @notice Set the application state to a given value. - /// This value must have been signed off by all parties to the channel, that is, - /// this must be called with the correct msg.sender or signatures must be provided. - /// @param appStateHash The hash of the agreed upon state. Typically this is the latest signed state. - /// @param nonce The nonce of the agreed upon state - /// @param timeout A dynamic timeout value representing the timeout for this state - /// @param signatures A sorted bytes string of concatenated signatures of each signingKey - /// @dev This function is only callable when the state channel is in an ON state. - function setState( - bytes32 appStateHash, - uint256 nonce, - uint256 timeout, - bytes signatures - ) - public - onlyWhenChannelOpen - { - if (msg.sender != auth.owner) { - bytes32 h = computeStateHash(appStateHash, nonce, timeout); - require( - signatures.verifySignatures(h, auth.signingKeys), - "Invalid signatures" - ); - } - - if (timeout > 0) { - require( - nonce > state.nonce, - "Tried to set state with non-new state" - ); - state.status = Status.DISPUTE; - } else { - require( - nonce >= state.nonce, - "Tried to finalize state with stale state" - ); - state.status = Status.OFF; - } - - state.appStateHash = appStateHash; - state.nonce = nonce; - state.disputeNonce = 0; - state.finalizesAt = block.number + timeout; - state.disputeCounter += 1; - state.latestSubmitter = msg.sender; - } - - /// @notice Create a dispute regarding the latest signed state and immediately after, - /// performs a unilateral action to update it. - /// @param app An `App` struct specifying the application logic - /// @param appState The ABI encoded application state - /// @param nonce The nonce of the agreed upon state - /// @param timeout A dynamic timeout value representing the timeout for this state - /// @param action The ABI encoded action the submitter wishes to take - /// @param appStateSignatures A sorted bytes string of concatenated signatures on the - /// `appState` state, signed by all `signingKeys` - /// @param actionSignature A bytes string of a single signature by the address of the - /// signing key for which it is their turn to take the submitted `action` - /// @param claimFinal A boolean representing a claim by the caller that the action - /// progresses the state of the application to a terminal / finalized state - /// @dev Note this function is only callable when the state channel is in an ON state - function createDispute( - App app, - bytes appState, - uint256 nonce, - uint256 timeout, - bytes action, - bytes appStateSignatures, - bytes actionSignature, - bool claimFinal - ) - public - onlyWhenChannelOpen - { - require( - nonce > state.nonce, - "Tried to create dispute with outdated state" - ); - - bytes32 appStateHash = keccak256(appState); - require( - appStateSignatures.verifySignatures( - computeStateHash(appStateHash, nonce, timeout), - auth.signingKeys - ), - "Invalid signatures" - ); - - address turnTaker = getAppTurnTaker(app, appState); - - bytes32 actionHash = computeActionHash( - turnTaker, - appStateHash, - action, - nonce, - state.disputeNonce - ); - require( - turnTaker == actionSignature.recoverKey(actionHash, 0), - "Action must have been signed by correct turn taker" - ); - - emit DisputeStarted( - msg.sender, - state.disputeCounter + 1, - appStateHash, - nonce, - block.number + timeout - ); - - bytes memory newAppState = executeAppApplyAction(app, appState, action); - - state.appStateHash = keccak256(newAppState); - state.nonce = nonce; - state.disputeNonce = 0; - state.disputeCounter += 1; - state.latestSubmitter = msg.sender; - - if (claimFinal) { - require( - isAppStateTerminal(app, newAppState), - "Attempted to claimFinal on a non-terminal state" - ); - state.finalizesAt = block.number; - state.status = Status.OFF; - - emit DisputeFinalized(msg.sender, newAppState); - } else { - state.finalizesAt = block.number + timeout; - state.status = Status.DISPUTE; - - emit DisputeProgressed( - msg.sender, - appState, - action, - newAppState, - state.disputeNonce, - block.number + timeout - ); - } - } - - /// @notice Respond to a dispute with a valid action - /// @param app An `App` struct specifying the application logic - /// @param appState The ABI encoded latest signed application state - /// @param action The ABI encoded action the submitter wishes to take - /// @param actionSignature A bytes string of a single signature by the address of the - /// signing key for which it is their turn to take the submitted `action` - /// @param claimFinal If set, the caller claims that the action progresses the state - /// to a terminal / finalized state - /// @dev This function is only callable when the state channel is in a DISPUTE state - function progressDispute( - App app, - bytes appState, - bytes action, - bytes actionSignature, - bool claimFinal - ) - public - onlyWhenChannelDispute - { - require( - keccak256(appState) == state.appStateHash, - "Invalid state submitted" - ); - - require( - keccak256(abi.encode(app)) == appHash, - "Tried to resolve dispute with non-agreed upon app" - ); - - address turnTaker = getAppTurnTaker(app, appState); - - require( - turnTaker == actionSignature.recoverKey(keccak256(action), 0), - "Action must have been signed by correct turn taker" - ); - - bytes memory newAppState = executeAppApplyAction(app, appState, action); - - state.appStateHash = keccak256(newAppState); - state.disputeNonce += 1; - state.latestSubmitter = msg.sender; - - if (claimFinal) { - require( - isAppStateTerminal(app, newAppState), - "Attempted to claimFinal on a non-terminal state" - ); - state.finalizesAt = block.number; - state.status = Status.OFF; - - emit DisputeFinalized(msg.sender, newAppState); - } else { - state.status = Status.DISPUTE; - state.finalizesAt = block.number + defaultTimeout; - - emit DisputeProgressed( - msg.sender, - appState, - action, - newAppState, - state.disputeNonce, - block.number + defaultTimeout - ); - } - } - - /// @notice Unanimously agree to cancel a dispute - /// @param signatures Signatures by all signing keys of the currently latest disputed - /// state; an indication of agreement of this state and valid to cancel a dispute - /// @dev Note this function is only callable when the state channel is in a DISPUTE state - function cancelDispute(bytes signatures) - public - onlyWhenChannelDispute - { - bytes32 stateHash = computeStateHash( - state.appStateHash, - state.nonce, - defaultTimeout - ); - - require( - signatures.verifySignatures(stateHash, auth.signingKeys), - "Invalid signatures" - ); - - state.disputeNonce = 0; - state.finalizesAt = 0; - state.status = Status.ON; - state.latestSubmitter = msg.sender; - - emit DisputeCancelled(msg.sender); - } - - /// @notice Fetch and store the resolution of a state channel application - /// @param app An `App` struct including all information relevant to interface with an app - /// @param finalState The ABI encoded version of the finalized application state - /// @param terms The ABI encoded version of the already agreed upon terms - /// @dev Note this function is only callable when the state channel is in an OFF state - function setResolution(App app, bytes finalState, bytes terms) - public - onlyWhenChannelClosed - { - require( - keccak256(finalState) == state.appStateHash, - "Tried to set resolution with incorrect final state" - ); - - require( - keccak256(terms) == termsHash, - "Tried to set resolution with non-agreed upon terms" - ); - - require( - keccak256(abi.encode(app)) == appHash, - "Tried to set resolution with non-agreed upon app" - ); - - resolution = getAppResolution(app, finalState, terms); - } - - /// @notice A helper method to check if the state of the channel is final by - /// doing a check on the submitted state and comparing to the current block number - /// @param s A state wrapper struct including the status and finalization time - /// @return A boolean indicating if the state is final or not - function isStateTerminal(State s) public view returns (bool) { - if (s.status == Status.ON) { - return false; - } else if (s.status == Status.DISPUTE) { - return block.number >= s.finalizesAt; - } else if (s.status == Status.OFF) { - return true; - } - } - - /// @notice Compute a unique hash for a state of this state channel and application - /// @param stateHash The hash of a state to be signed - /// @param nonce The nonce corresponding to the version of the state - /// @param timeout A dynamic timeout value representing the timeout for this state - /// @return A bytes32 hash of the arguments encoded with the signing keys for the channel - function computeStateHash(bytes32 stateHash, uint256 nonce, uint256 timeout) - internal - view - returns (bytes32) - { - return keccak256( - abi.encodePacked( - byte(0x19), - auth.signingKeys, - nonce, - timeout, - stateHash - ) - ); - } - - /// @notice Compute a unique hash for an action used in this channel application - /// @param turnTaker The address of the user taking the action - /// @param previousState The hash of a state this action is being taken on - /// @param action The ABI encoded version of the action being taken - /// @param setStateNonce The nonce of the state this action is being taken on - /// @param disputeNonce A nonce corresponding to how many actions have been taken on the - /// state since a new state has been unanimously agreed upon by all signing keys. - /// @return A bytes32 hash of the arguments - function computeActionHash( - address turnTaker, - bytes32 previousState, - bytes action, - uint256 setStateNonce, - uint256 disputeNonce - ) - internal - pure - returns (bytes32) - { - return keccak256( - abi.encodePacked( - byte(0x19), - turnTaker, - previousState, - action, - setStateNonce, - disputeNonce - ) - ); - } - - /// @notice A helper method to check if the state of an application is terminal or not - /// @param app An `App` struct including all information relevant to interface with an app - /// @param appState The ABI encoded version of some application state - /// @return A boolean indicating if the application state is terminal or not - function isAppStateTerminal(App app, bytes appState) - private - view - returns (bool) - { - return app.addr.staticcall_as_bool( - abi.encodePacked(app.isStateTerminal, appState) - ); - } - - /// @notice A helper method to get the turn taker for an app - /// @param app An `App` struct including all information relevant to interface with an app - /// @param appState The ABI encoded version of some application state - /// @return An address representing the turn taker in the `signingKeys` - function getAppTurnTaker(App app, bytes appState) - private - view - returns (address) - { - uint256 idx = app.addr.staticcall_as_uint256( - abi.encodePacked(app.getTurnTaker, appState) - ); - - require( - auth.signingKeys[idx] != address(0), - "Application returned invalid turn taker index" - ); - - return auth.signingKeys[idx]; - } - - /// @notice Execute the application's applyAction function to compute new state - /// @param app An `App` struct including all information relevant to interface with an app - /// @param appState The ABI encoded version of some application state - /// @param action The ABI encoded version of some application action - /// @return A bytes array of the ABI encoded newly computed application state - function executeAppApplyAction(App app, bytes appState, bytes action) - private - view - returns (bytes) - { - return app.addr.staticcall_as_bytes( - abi.encodePacked(app.applyAction, appState, action) - ); - } - - /// @notice Execute the application's resolve function to compute a resolution - /// @param app An `App` struct including all information relevant to interface with an app - /// @param appState The ABI encoded version of some application state - /// @param terms The ABI encoded version of the transfer terms - /// @return A `Transfer.Transaction` struct with all encoded information of the resolution - function getAppResolution(App app, bytes appState, bytes terms) - private - view - returns (Transfer.Transaction) - { - return app.addr.staticcall_as_TransferDetails( - abi.encodePacked(app.resolve, appState, terms) - ); - } - -} diff --git a/packages/contracts/contracts/AppRegistry.sol b/packages/contracts/contracts/AppRegistry.sol new file mode 100644 index 000000000..a4477967f --- /dev/null +++ b/packages/contracts/contracts/AppRegistry.sol @@ -0,0 +1,20 @@ +pragma solidity ^0.4.25; +pragma experimental "ABIEncoderV2"; + +import "./mixins/MixinAppRegistryCore.sol"; +import "./mixins/MixinCancelChallenge.sol"; +import "./mixins/MixinSetResolution.sol"; +import "./mixins/MixinSetState.sol"; +import "./mixins/MixinSetStateWithAction.sol"; + + +/// @dev Base contract implementing all logic needed for full-featured App registry +contract AppRegistry is + MixinAppRegistryCore, + MixinSetState, + MixinSetStateWithAction, + MixinCancelChallenge, + MixinSetResolution +{ + constructor () public {} +} diff --git a/packages/contracts/contracts/ConditionalTransaction.sol b/packages/contracts/contracts/ConditionalTransaction.sol new file mode 100644 index 000000000..15f880a19 --- /dev/null +++ b/packages/contracts/contracts/ConditionalTransaction.sol @@ -0,0 +1,32 @@ +pragma solidity 0.4.25; +pragma experimental "ABIEncoderV2"; + +import "./libs/Transfer.sol"; +import "./libs/LibCondition.sol"; + +import "./ContractRegistry.sol"; +import "./NonceRegistry.sol"; + + +/// @title ConditionalTransaction - A conditional transfer contract +/// @author Liam Horne - +/// @author Mitchell Van Der Hoeff - +/// @notice Supports a complex transfer of funds contingent on some condition. +contract ConditionalTransaction is LibCondition { + + using Transfer for Transfer.Transaction; + + function executeSimpleConditionalTransaction( + Condition condition, + Transfer.Transaction memory txn + ) + public + { + require( + isSatisfied(condition), + "Condition was not satisfied for conditional transaction" + ); + txn.execute(); + } + +} diff --git a/packages/contracts/contracts/Registry.sol b/packages/contracts/contracts/ContractRegistry.sol similarity index 94% rename from packages/contracts/contracts/Registry.sol rename to packages/contracts/contracts/ContractRegistry.sol index ce8756ba4..fc745d311 100644 --- a/packages/contracts/contracts/Registry.sol +++ b/packages/contracts/contracts/ContractRegistry.sol @@ -3,11 +3,11 @@ pragma solidity 0.4.25; import "openzeppelin-eth/contracts/utils/Address.sol"; -/// @title Registry - A global Ethereum deterministic address translator +/// @title ContractRegistry - A global Ethereum deterministic address translator /// @author Liam Horne - /// @notice Supports deployment of contract initcode with the ability to deterministically reference the address before the actual contract is deployed regardless of msg.sender or transaction nonce /// @dev Will be obsolete for most use cases when CREATE2 is added to the EVM https://github.com/ethereum/EIPs/pull/1014 -contract Registry { +contract ContractRegistry { using Address for address; @@ -62,7 +62,7 @@ contract Registry { /// @param ptr A counterfactual address /// @param data The data being sent in the call to the counterfactual contract function proxyCall(address registry, bytes32 ptr, bytes data) public { - address to = Registry(registry).resolver(ptr); + address to = ContractRegistry(registry).resolver(ptr); require(to != address(0), "Resolved to a 0x0 address"); require(to.isContract(), "Tried to call function on a non-contract"); require(to.call(data), "Registry.proxyCall failed."); diff --git a/packages/contracts/contracts/delegateTargets/ETHVirtualAppAgreement.sol b/packages/contracts/contracts/ETHVirtualAppAgreement.sol similarity index 87% rename from packages/contracts/contracts/delegateTargets/ETHVirtualAppAgreement.sol rename to packages/contracts/contracts/ETHVirtualAppAgreement.sol index 513148a32..212409607 100644 --- a/packages/contracts/contracts/delegateTargets/ETHVirtualAppAgreement.sol +++ b/packages/contracts/contracts/ETHVirtualAppAgreement.sol @@ -1,9 +1,8 @@ pragma solidity 0.4.25; pragma experimental "ABIEncoderV2"; -import "../lib/Transfer.sol"; -import "../Registry.sol"; -import "../AppInstance.sol"; +import "./libs/Transfer.sol"; +import "./AppRegistry.sol"; /// @title ETHVirtualAppAgreement @@ -24,10 +23,10 @@ contract ETHVirtualAppAgreement { // todo(ldct): is it possible to make address(registry) a constant specified // at link time? struct Agreement { - Registry registry; + AppRegistry registry; Transfer.Terms terms; uint256 expiry; - bytes32 target; + bytes32 appInstanceId; uint256 capitalProvided; address[2] beneficiaries; } @@ -37,36 +36,34 @@ contract ETHVirtualAppAgreement { agreement.expiry <= block.number, "agreement lockup time has not elapsed" ); - - address target = agreement.registry.resolver(agreement.target); - - Transfer.Transaction memory resolution = AppInstance(target) - .getResolution(); + + Transfer.Transaction memory resolution = agreement.registry + .getResolution(agreement.appInstanceId); require( agreement.terms.assetType == resolution.assetType, "returned incompatible resolution" ); - + require( agreement.terms.token == resolution.token, "returned incompatible resolution" ); - + require( agreement.capitalProvided > resolution.value[0], "returned incompatible resolution" ); uint256[] memory amount = new uint256[](2); - + amount[0] = resolution.value[0]; amount[1] = agreement.capitalProvided - amount[0]; bytes[] memory data = new bytes[](2); address[] memory beneficiaries = new address[](2); - + beneficiaries[0] = agreement.beneficiaries[0]; beneficiaries[1] = agreement.beneficiaries[1]; diff --git a/packages/contracts/contracts/delegateTargets/MultiSend.sol b/packages/contracts/contracts/MultiSend.sol similarity index 100% rename from packages/contracts/contracts/delegateTargets/MultiSend.sol rename to packages/contracts/contracts/MultiSend.sol diff --git a/packages/contracts/contracts/delegateTargets/ConditionalTransaction.sol b/packages/contracts/contracts/StateChannelTransaction.sol similarity index 51% rename from packages/contracts/contracts/delegateTargets/ConditionalTransaction.sol rename to packages/contracts/contracts/StateChannelTransaction.sol index e69fa75d7..4cfc9d916 100644 --- a/packages/contracts/contracts/delegateTargets/ConditionalTransaction.sol +++ b/packages/contracts/contracts/StateChannelTransaction.sol @@ -1,55 +1,46 @@ pragma solidity 0.4.25; pragma experimental "ABIEncoderV2"; -import "../lib/Transfer.sol"; -import "../Conditional.sol"; -import "../Registry.sol"; -import "../NonceRegistry.sol"; -import "../AppInstance.sol"; +import "./libs/Transfer.sol"; +import "./libs/LibCondition.sol"; + +import "./ContractRegistry.sol"; +import "./NonceRegistry.sol"; +import "./AppRegistry.sol"; /// @title ConditionalTransaction - A conditional transfer contract /// @author Liam Horne - /// @author Mitchell Van Der Hoeff - /// @notice Supports a complex transfer of funds contingent on some condition. -contract ConditionalTransaction is Conditional { +contract StateChannelTransaction is LibCondition { using Transfer for Transfer.Transaction; - function executeSimpleConditionalTransaction( - Condition condition, - Transfer.Transaction memory txn - ) - public - { - require( - Conditional.isSatisfied(condition), - "Condition was not satisfied for conditional transaction" - ); - txn.execute(); - } - /// @notice Execute a fund transfer for a state channel app in a finalized state /// @param uninstallKey The key in the nonce registry - /// @param appCfAddress Counterfactual address of the app contract + /// @param appInstanceId AppInstanceId to be resolved /// @param terms The pre-agreed upon terms of the funds transfer function executeAppConditionalTransaction( - address registry, - address nonceRegistry, + AppRegistry appRegistry, + NonceRegistry nonceRegistry, bytes32 uninstallKey, - bytes32 appCfAddress, + bytes32 appInstanceId, Transfer.Terms terms ) public { require( - !NonceRegistry(nonceRegistry).isFinalized(uninstallKey, 1), + !nonceRegistry.isFinalized(uninstallKey, 1), "App has been uninstalled" ); - address appAddr = Registry(registry).resolver(appCfAddress); - AppInstance app = AppInstance(appAddr); - Transfer.Transaction memory txn = app.getResolution(); + require( + appRegistry.isStateFinalized(appInstanceId), + "App is not finalized yet" + ); + + Transfer.Transaction memory txn = appRegistry.getResolution(appInstanceId); require( Transfer.meetsTerms(txn, terms), @@ -59,4 +50,5 @@ contract ConditionalTransaction is Conditional { txn.execute(); } + } diff --git a/packages/contracts/contracts/appDefinitions/ETHBalanceRefundApp.sol b/packages/contracts/contracts/default-apps/ETHBalanceRefundApp.sol similarity index 95% rename from packages/contracts/contracts/appDefinitions/ETHBalanceRefundApp.sol rename to packages/contracts/contracts/default-apps/ETHBalanceRefundApp.sol index cc2b4daf5..6feebe878 100644 --- a/packages/contracts/contracts/appDefinitions/ETHBalanceRefundApp.sol +++ b/packages/contracts/contracts/default-apps/ETHBalanceRefundApp.sol @@ -1,7 +1,7 @@ pragma solidity 0.4.25; pragma experimental "ABIEncoderV2"; -import "../lib/Transfer.sol"; +import "../libs/Transfer.sol"; contract ETHBalanceRefundApp { diff --git a/packages/contracts/contracts/default-apps/ETHBucket.sol b/packages/contracts/contracts/default-apps/ETHBucket.sol new file mode 100644 index 000000000..f5d0a82ae --- /dev/null +++ b/packages/contracts/contracts/default-apps/ETHBucket.sol @@ -0,0 +1,39 @@ +pragma solidity 0.4.25; +pragma experimental "ABIEncoderV2"; + +import "../libs/Transfer.sol"; + + +contract ETHBucket { + + struct AppState { + address alice; + address bob; + uint256 aliceBalance; + uint256 bobBalance; + } + + function resolve(AppState state, Transfer.Terms terms) + public + pure + returns (Transfer.Transaction) + { + uint256[] memory amounts = new uint256[](2); + amounts[0] = state.aliceBalance; + amounts[1] = state.bobBalance; + + address[] memory to = new address[](2); + to[0] = state.alice; + to[1] = state.bob; + bytes[] memory data = new bytes[](2); + + return Transfer.Transaction( + terms.assetType, + terms.token, + to, + amounts, + data + ); + } + +} diff --git a/packages/contracts/contracts/Conditional.sol b/packages/contracts/contracts/libs/LibCondition.sol similarity index 82% rename from packages/contracts/contracts/Conditional.sol rename to packages/contracts/contracts/libs/LibCondition.sol index 736df1416..b10d2f21b 100644 --- a/packages/contracts/contracts/Conditional.sol +++ b/packages/contracts/contracts/libs/LibCondition.sol @@ -1,17 +1,17 @@ pragma solidity 0.4.25; pragma experimental "ABIEncoderV2"; -import "./lib/StaticCall.sol"; +import "./LibStaticCall.sol"; -/// @title Conditional - A wrapper to verify if abstract conditions are true +/// @title LibCondition - A wrapper to verify if abstract conditions are true /// @author Liam Horne - /// @notice This contracts purpose is to make it easy for to define and verify the /// resolution of an abstract blockchain condition. A condition may be represented as /// a successful function call or as an equality check on a function calls return value. -contract Conditional { +contract LibCondition { - using StaticCall for address; + using LibStaticCall for address; struct Condition { address to; @@ -39,24 +39,23 @@ contract Conditional { /// @notice Checks if the defined function call in a Condition fails or not. /// @param condition A `Condition` struct /// @return A boolean indicating whether or not the function call passed or failed. - function assertNotFails(Condition condition) + function assertNotFails(Condition memory condition) private view returns (bool) { - condition.to.staticcall_as_bytes( + return condition.to.staticcall_no_error( abi.encodePacked( condition.selector, condition.parameters ) ); - return true; // TODO: Need better way of cheaply checking } /// @notice Verifies the expected return value of a Condition /// @param condition A `Condition` struct /// @return A boolean verifying if the function call returned the expected result - function assertReturnsExpectedResult(Condition condition) + function assertReturnsExpectedResult(Condition memory condition) private view returns (bool) diff --git a/packages/contracts/contracts/lib/Signatures.sol b/packages/contracts/contracts/libs/LibSignature.sol similarity index 91% rename from packages/contracts/contracts/lib/Signatures.sol rename to packages/contracts/contracts/libs/LibSignature.sol index 71ca9fe30..020f508ec 100644 --- a/packages/contracts/contracts/lib/Signatures.sol +++ b/packages/contracts/contracts/libs/LibSignature.sol @@ -1,21 +1,21 @@ pragma solidity 0.4.25; -/// @title Signatures - A library wrapper around signature verification +/// @title LibSignature - A library wrapper around signature verification /// @author Liam Horne - /// @author Ricardo Guilherme Schmidt (Status Research & Development GmbH) /// @author Richard Meissner - /// @notice This contracts purpose is to make it easy to do signature verification of /// string concatenated signatures in a bytes array. It is heavily based off the /// SignatureValidator contract from Gnosis: https://git.io/fNzRJ -library Signatures { +contract LibSignature { /// @dev Recovers address who signed the message /// @param messageSignature message `txHash` signature /// @param txHash operation ethereum signed message hash /// @param pos which signature to read function recoverKey( - bytes memory messageSignature, + bytes messageSignature, bytes32 txHash, uint256 pos ) @@ -35,7 +35,7 @@ library Signatures { /// @param txHash operation ethereum signed message hash /// @param signers addresses of all signers in order function verifySignatures( - bytes memory signatures, + bytes signatures, bytes32 txHash, address[] signers ) @@ -57,7 +57,7 @@ library Signatures { /// @dev divides bytes signature into `uint8 v, bytes32 r, bytes32 s` /// @param pos which signature to read /// @param signatures concatenated rsv signatures - function signatureSplit(bytes memory signatures, uint256 pos) + function signatureSplit(bytes signatures, uint256 pos) public pure returns (uint8 v, bytes32 r, bytes32 s) diff --git a/packages/contracts/contracts/libs/LibStateChannelApp.sol b/packages/contracts/contracts/libs/LibStateChannelApp.sol new file mode 100644 index 000000000..6cd22ce8a --- /dev/null +++ b/packages/contracts/contracts/libs/LibStateChannelApp.sol @@ -0,0 +1,54 @@ +pragma solidity ^0.4.25; + + +/// @title LibStateChannelApp +/// @author Liam Horne - +/// @notice Contains the structures and enums needed for the AppRegistry +contract LibStateChannelApp { + + // The mode that the App is currently in from POV of the blockchain + enum AppStatus { + ON, + DISPUTE, + OFF + } + + // An interface to the App used to make function calls to it + struct AppInterface { + address addr; + bytes4 getTurnTaker; + bytes4 applyAction; + bytes4 resolve; + bytes4 isStateTerminal; + } + + // A minimal structure that uniquely identifies a single instance of an App + struct AppIdentity { + address owner; + address[] signingKeys; + bytes32 appInterfaceHash; + bytes32 termsHash; + uint256 defaultTimeout; + } + + // A structure representing the state of an AppInstance from POV of the blockchain + struct AppChallenge { + AppStatus status; + address latestSubmitter; + bytes32 appStateHash; + uint256 disputeCounter; + uint256 disputeNonce; + uint256 finalizesAt; + uint256 nonce; + } + + // A minimal structure representing a state update that can be signed + struct AppStateProof { + bytes32 id; + bytes32 appStateHash; + uint256 nonce; + uint256 timeout; + } + + +} diff --git a/packages/contracts/contracts/lib/StaticCall.sol b/packages/contracts/contracts/libs/LibStaticCall.sol similarity index 90% rename from packages/contracts/contracts/lib/StaticCall.sol rename to packages/contracts/contracts/libs/LibStaticCall.sol index 253f41466..65f506e34 100644 --- a/packages/contracts/contracts/lib/StaticCall.sol +++ b/packages/contracts/contracts/libs/LibStaticCall.sol @@ -2,14 +2,15 @@ pragma solidity 0.4.25; pragma experimental "ABIEncoderV2"; import "openzeppelin-eth/contracts/utils/Address.sol"; -import "../lib/Transfer.sol"; +import "../libs/Transfer.sol"; -/// @title StaticCall - A library wrapper around the STATICALL opcode +/// @title LibStaticCall - A library wrapper around the STATICALL opcode /// @author Liam Horne - -/// @notice This contracts purpose is to make it easy for contracts to make static function -/// calls to contracts with unknown ABIs without exposing assembly code in the contract. -library StaticCall { +/// @notice This contract's purpose is to make it easy for contracts to make +/// static function calls to contracts with unknown ABIs without exposing +/// assembly code in the contract +library LibStaticCall { using Address for address; @@ -24,7 +25,7 @@ library StaticCall { { require(to.isContract(), "StaticCall to address is not a contract."); assembly { - let success := staticcall(gas, to, add(data, 0x20), mload(data), 0, 0) + success := staticcall(gas, to, add(data, 0x20), mload(data), 0, 0) } } diff --git a/packages/contracts/contracts/lib/Transfer.sol b/packages/contracts/contracts/libs/Transfer.sol similarity index 100% rename from packages/contracts/contracts/lib/Transfer.sol rename to packages/contracts/contracts/libs/Transfer.sol diff --git a/packages/contracts/contracts/mixins/MAppCaller.sol b/packages/contracts/contracts/mixins/MAppCaller.sol new file mode 100644 index 000000000..c8fdcc36e --- /dev/null +++ b/packages/contracts/contracts/mixins/MAppCaller.sol @@ -0,0 +1,86 @@ +pragma solidity ^0.4.25; +pragma experimental "ABIEncoderV2"; + +import "../libs/LibSignature.sol"; +import "../libs/LibStateChannelApp.sol"; +import "../libs/LibStaticCall.sol"; +import "../libs/Transfer.sol"; + + +/// @title MAppCaller +/// @author Liam Horne - +/// @notice A mixin for the AppRegistry to make staticcalls to Apps +contract MAppCaller is LibSignature, LibStateChannelApp { + + using LibStaticCall for address; + + /// @notice A helper method to check if the state of an application is terminal or not + /// @param appInterface An `AppInterface` struct + /// @param appState The ABI encoded version of some application state + /// @return A boolean indicating if the application state is terminal or not + function isStateTerminal(AppInterface appInterface, bytes appState) + internal + view + returns (bool) + { + return appInterface.addr.staticcall_as_bool( + abi.encodePacked(appInterface.isStateTerminal, appState) + ); + } + + /// @notice A helper method to get the turn taker for an app + /// @param appInterface An `AppInterface` struct + /// @param appState The ABI encoded version of some application state + /// @return An address representing the turn taker in the `signingKeys` + function getTurnTaker( + AppInterface appInterface, + address[] signingKeys, + bytes appState + ) + internal + view + returns (address) + { + uint256 idx = appInterface.addr.staticcall_as_uint256( + abi.encodePacked(appInterface.getTurnTaker, appState) + ); + + require( + signingKeys[idx] != address(0), + "Application returned invalid turn taker index" + ); + + return signingKeys[idx]; + } + + /// @notice Execute the application's applyAction function to compute new state + /// @param appInterface An `AppInterface` struct + /// @param appState The ABI encoded version of some application state + /// @param action The ABI encoded version of some application action + /// @return A bytes array of the ABI encoded newly computed application state + function applyAction(AppInterface appInterface, bytes appState, bytes action) + internal + view + returns (bytes) + { + return appInterface.addr.staticcall_as_bytes( + abi.encodePacked(appInterface.applyAction, appState, action) + ); + } + + /// @notice Execute the application's resolve function to compute a resolution + /// @param appInterface An `AppInterface` struct + /// @param appState The ABI encoded version of some application state + /// @param terms The ABI encoded version of the transfer terms + /// @return A `Transfer.Transaction` struct with all encoded information of the resolution + function resolve(AppInterface appInterface, bytes appState, bytes terms) + internal + view + returns (Transfer.Transaction) + { + return appInterface.addr.staticcall_as_TransferDetails( + abi.encodePacked(appInterface.resolve, appState, terms) + ); + } + +} diff --git a/packages/contracts/contracts/mixins/MAppRegistryCore.sol b/packages/contracts/contracts/mixins/MAppRegistryCore.sol new file mode 100644 index 000000000..ce30a7913 --- /dev/null +++ b/packages/contracts/contracts/mixins/MAppRegistryCore.sol @@ -0,0 +1,114 @@ +pragma solidity 0.4.25; +pragma experimental "ABIEncoderV2"; + +import "../libs/LibStateChannelApp.sol"; +import "../libs/Transfer.sol"; + + +contract MAppRegistryCore { + + using Transfer for Transfer.Transaction; + + // A mapping of AppInstanceIds to AppChallenge structs which represents + // the current on-chain status of some particular applications state. + mapping (bytes32 => LibStateChannelApp.AppChallenge) public appStates; + + // A mapping of AppInstanceIds to Transaction structs which represents + // the *final* resolution of a particular application + mapping (bytes32 => Transfer.Transaction) public appResolutions; + + // TODO: docs + modifier doAppInterfaceCheck( + LibStateChannelApp.AppInterface appInterface, + bytes32 appInterfaceHash + ) { + // TODO: This is inefficient from a gas point of view since we could just include + // the hash of appInterface in the call to computeAppIdentityHash. Cleanup in the fututre. + require( + keccak256(abi.encode(appInterface)) == appInterfaceHash, + "Call to AppRegistry included mismatched appInterface and appInterfaceHash" + ); + _; + } + + modifier doTermsCheck( + bytes terms, + bytes32 termsHash + ) { + require( + keccak256(terms) == termsHash, + "Call to AppRegistry included mismatched terms and termsHash" + ); + _; + } + + /// @notice Compute a unique hash for a single instance of an App + /// @param appIdentity An `AppIdentity` struct that encodes all unqiue info for an App + /// @return A bytes32 hash of the AppIdentity + function computeAppIdentityHash(LibStateChannelApp.AppIdentity appIdentity) + internal + pure + returns (bytes32) + { + return keccak256(abi.encode(appIdentity)); + } + + /// @notice Compute a unique hash for a state of this state channel and application + /// @param id The unique hash of an `AppIdentity` + /// @param appStateHash The hash of a state to be signed + /// @param nonce The nonce corresponding to the version of the state + /// @param timeout A dynamic timeout value representing the timeout for this state + /// @return A bytes32 hash of the arguments encoded with the signing keys for the channel + function computeStateHash( + bytes32 id, + bytes32 appStateHash, + uint256 nonce, + uint256 timeout + ) + internal + pure + returns (bytes32) + { + return keccak256( + abi.encodePacked( + byte(0x19), + id, + nonce, + timeout, + appStateHash + ) + ); + } + + /// @notice Compute a unique hash for an action used in this channel application + /// @param turnTaker The address of the user taking the action + /// @param previousState The hash of a state this action is being taken on + /// @param action The ABI encoded version of the action being taken + /// @param setStateNonce The nonce of the state this action is being taken on + /// @param disputeNonce A nonce corresponding to how many actions have been taken on the + /// state since a new state has been unanimously agreed by signing keys. + /// @return A bytes32 hash of the arguments + function computeActionHash( + address turnTaker, + bytes32 previousState, + bytes action, + uint256 setStateNonce, + uint256 disputeNonce + ) + internal + pure + returns (bytes32) + { + return keccak256( + abi.encodePacked( + byte(0x19), + turnTaker, + previousState, + action, + setStateNonce, + disputeNonce + ) + ); + } + +} diff --git a/packages/contracts/contracts/mixins/MixinAppRegistryCore.sol b/packages/contracts/contracts/mixins/MixinAppRegistryCore.sol new file mode 100644 index 000000000..fcac42036 --- /dev/null +++ b/packages/contracts/contracts/mixins/MixinAppRegistryCore.sol @@ -0,0 +1,54 @@ +pragma solidity ^0.4.25; +pragma experimental "ABIEncoderV2"; + +import "../libs/LibStateChannelApp.sol"; +import "../libs/Transfer.sol"; + +import "./MAppRegistryCore.sol"; + + +/// @title MixinAppRegistryCore +/// @author Liam Horne - +/// @notice Core functionality and utilities for the AppRegistry +contract MixinAppRegistryCore is MAppRegistryCore { + + /// @notice A getter function for the current AppChallenge state + /// @param id The unique hash of an `AppIdentity` + /// @return A `AppChallenge` object representing the state of the on-chain challenge + function getAppChallenge(bytes32 id) + external + view + returns (LibStateChannelApp.AppChallenge) + { + return appStates[id]; + } + + /// @notice Checks whether or not some application's state is OFF or timed out + /// @param id The unique hash of an `AppIdentity` + /// @return A boolean indicator + function isStateFinalized(bytes32 id) + external + view + returns (bool) + { + return ( + appStates[id].status == LibStateChannelApp.AppStatus.OFF || + ( + appStates[id].status == LibStateChannelApp.AppStatus.DISPUTE && + appStates[id].finalizesAt <= block.number + ) + ); + } + + /// @notice A getter function for the resolution if one is set + /// @param id The unique hash of an `AppIdentity` + /// @return A `Transfer.Transaction` object representing the resolution of the channel + function getResolution(bytes32 id) + external + view + returns (Transfer.Transaction) + { + return appResolutions[id]; + } + +} diff --git a/packages/contracts/contracts/mixins/MixinCancelChallenge.sol b/packages/contracts/contracts/mixins/MixinCancelChallenge.sol new file mode 100644 index 000000000..38b1d9783 --- /dev/null +++ b/packages/contracts/contracts/mixins/MixinCancelChallenge.sol @@ -0,0 +1,59 @@ +pragma solidity 0.4.25; +pragma experimental "ABIEncoderV2"; + +import "../libs/LibSignature.sol"; +import "../libs/LibStateChannelApp.sol"; +import "../libs/LibStaticCall.sol"; + +import "./MAppRegistryCore.sol"; + + +contract MixinCancelChallenge is + LibSignature, + LibStateChannelApp, + MAppRegistryCore +{ + + /// @notice Unanimously agree to cancel a dispute + // TODO: Docs + /// @param signatures Signatures by all signing keys of the currently latest disputed + /// state; an indication of agreement of this state and valid to cancel a dispute + /// @dev Note this function is only callable when the state channel is in a DISPUTE state + function cancelChallenge( + AppIdentity appIdentity, + bytes signatures + ) + // TODO: Uncomment when ABIEncoderV2 supports `external` + // ref: https://github.com/ethereum/solidity/issues/3199 + // external + public + { + bytes32 id = computeAppIdentityHash(appIdentity); + + AppChallenge storage challenge = appStates[id]; + + require( + challenge.status == AppStatus.DISPUTE && challenge.finalizesAt >= block.number, + "cancelChallenge called on app not in DISPUTE state" + ); + + bytes32 stateHash = computeStateHash( + id, + challenge.appStateHash, + challenge.nonce, + appIdentity.defaultTimeout + ); + + if (msg.sender != appIdentity.owner) { + require( + verifySignatures(signatures, stateHash, appIdentity.signingKeys), + "Invalid signatures" + ); + } + + challenge.disputeNonce = 0; + challenge.finalizesAt = 0; + challenge.status = AppStatus.ON; + challenge.latestSubmitter = msg.sender; + } +} diff --git a/packages/contracts/contracts/mixins/MixinSetResolution.sol b/packages/contracts/contracts/mixins/MixinSetResolution.sol new file mode 100644 index 000000000..66d40e89c --- /dev/null +++ b/packages/contracts/contracts/mixins/MixinSetResolution.sol @@ -0,0 +1,49 @@ +pragma solidity 0.4.25; +pragma experimental "ABIEncoderV2"; + +import "../libs/LibStateChannelApp.sol"; + +import "./MAppRegistryCore.sol"; +import "./MAppCaller.sol"; + + +contract MixinSetResolution is + LibStateChannelApp, + MAppRegistryCore, + MAppCaller +{ + + /// @notice Fetch and store the resolution of a state channel application + // TODO: docs + /// @param finalState The ABI encoded version of the finalized application state + /// @param terms The ABI encoded version of the already agreed upon terms + /// @dev Note this function is only callable when the state channel is in an OFF state + function setResolution( + AppIdentity appIdentity, + AppInterface appInterface, + bytes finalState, + bytes terms + ) + public + doAppInterfaceCheck(appInterface, appIdentity.appInterfaceHash) + doTermsCheck(terms, appIdentity.termsHash) + { + bytes32 id = computeAppIdentityHash(appIdentity); + + AppChallenge storage app = appStates[id]; + + require( + app.status == AppStatus.OFF || + (app.status == AppStatus.DISPUTE && block.number > app.finalizesAt), + "setResolution called on an app either still ON or in DISPUTE" + ); + + require( + keccak256(finalState) == app.appStateHash, + "setResolution called with incorrect witness data of finalState" + ); + + appResolutions[id] = MAppCaller.resolve(appInterface, finalState, terms); + } + +} diff --git a/packages/contracts/contracts/mixins/MixinSetState.sol b/packages/contracts/contracts/mixins/MixinSetState.sol new file mode 100644 index 000000000..48a506880 --- /dev/null +++ b/packages/contracts/contracts/mixins/MixinSetState.sol @@ -0,0 +1,98 @@ +pragma solidity 0.4.25; +pragma experimental "ABIEncoderV2"; + +import "../libs/LibStateChannelApp.sol"; +import "../libs/LibSignature.sol"; +import "../libs/LibStaticCall.sol"; + +import "./MAppRegistryCore.sol"; +import "./MAppCaller.sol"; + + +contract MixinSetState is + LibSignature, + LibStateChannelApp, + MAppRegistryCore, + MAppCaller +{ + + struct SignedStateHashUpdate { + bytes32 stateHash; + uint256 nonce; + uint256 timeout; + bytes signatures; + } + + /// @notice Set the application state to a given value. + /// This value must have been signed off by all parties to the channel, that is, + /// this must be called with the correct msg.sender (the state deposit holder) + /// or signatures must be provided. + /// @param appIdentity an AppIdentity struct with all information encoded within + /// it to represent which particular app is having state submitted + /// @param req An object containing the update to be applied to the + /// applications state including the signatures of the users needed + /// @dev This function is only callable when the state channel is in an ON state. + function setState( + AppIdentity appIdentity, + SignedStateHashUpdate req + ) + public + { + bytes32 id = computeAppIdentityHash(appIdentity); + + AppChallenge storage challenge = appStates[id]; + + require( + challenge.status == AppStatus.ON, + "setState was called on an app that is either in DISPUTE or OFF" + ); + + if (msg.sender != appIdentity.owner) { + require( + correctKeysSignedTheStateUpdate( + id, + appIdentity.signingKeys, + req + ), + "Call to setState included incorrectly signed state update" + ); + } + + require( + req.nonce > challenge.nonce, + "Tried to call setState with an outdated nonce version" + ); + + challenge.status = req.timeout > 0 ? AppStatus.DISPUTE : AppStatus.OFF; + challenge.appStateHash = req.stateHash; + challenge.nonce = req.nonce; + challenge.finalizesAt = block.number + req.timeout; + challenge.disputeNonce = 0; + challenge.disputeCounter += 1; + challenge.latestSubmitter = msg.sender; + } + + function correctKeysSignedTheStateUpdate( + bytes32 id, + address[] signingKeys, + SignedStateHashUpdate req + ) + private + pure + returns (bool) + { + bytes32 digest = computeStateHash( + id, + req.stateHash, + req.nonce, + req.timeout + ); + + return verifySignatures( + req.signatures, + digest, + signingKeys + ); + } + +} diff --git a/packages/contracts/contracts/mixins/MixinSetStateWithAction.sol b/packages/contracts/contracts/mixins/MixinSetStateWithAction.sol new file mode 100644 index 000000000..8983a81c7 --- /dev/null +++ b/packages/contracts/contracts/mixins/MixinSetStateWithAction.sol @@ -0,0 +1,153 @@ +pragma solidity 0.4.25; +pragma experimental "ABIEncoderV2"; + +import "../libs/LibStateChannelApp.sol"; +import "../libs/LibSignature.sol"; +import "../libs/LibStaticCall.sol"; + +import "./MAppRegistryCore.sol"; +import "./MAppCaller.sol"; + + +contract MixinSetStateWithAction is + LibSignature, + LibStateChannelApp, + MAppRegistryCore, + MAppCaller +{ + + struct SignedStateUpdate { + // NOTE: We include the full bytes of the state update, + // not just the hash of it as in MixinSetState. + bytes encodedState; + uint256 nonce; + uint256 timeout; + bytes signatures; + } + + struct SignedAction { + bytes encodedAction; + bytes signature; + bool checkForTerminal; + } + + /// @notice Create a dispute regarding the latest signed state and immediately after, + /// performs a unilateral action to update it. + // @param app An `App` struct specifying the application logic + // TODO: Docs + /// @dev Note this function is only callable when the state channel is in an ON state + function setStateWithAction( + AppIdentity appIdentity, + AppInterface appInterface, + SignedStateUpdate req, + SignedAction action + ) + public + doAppInterfaceCheck(appInterface, appIdentity.appInterfaceHash) + { + bytes32 id = computeAppIdentityHash(appIdentity); + + AppChallenge storage challenge = appStates[id]; + + require( + correctKeysSignedTheStateUpdate(id, appIdentity.signingKeys, req), + "Call to setStateWithAction included incorrectly signed state update" + ); + + require( + challenge.status == AppStatus.ON, + "setStateWithAction was called on an app that is either in DISPUTE or OFF" + ); + + require( + req.nonce > challenge.nonce, + "setStateWithAction was called with outdated state" + ); + + require( + correctKeySignedTheAction( + appInterface, + appIdentity.signingKeys, + challenge.disputeNonce, + req, + action + ), + "setStateWithAction called with action signed by incorrect turn taker" + ); + + bytes memory newState = MAppCaller.applyAction( + appInterface, + req.encodedState, + action.encodedAction + ); + + if (action.checkForTerminal) { + require( + MAppCaller.isStateTerminal(appInterface, newState), + "Attempted to claim non-terminal state was terminal in setStateWithAction" + ); + challenge.finalizesAt = block.number; + challenge.status = AppStatus.OFF; + } else { + challenge.finalizesAt = block.number + req.timeout; + challenge.status = AppStatus.DISPUTE; + } + + challenge.appStateHash = keccak256(newState); + challenge.nonce = req.nonce; + challenge.disputeNonce = 0; + challenge.disputeCounter += 1; + challenge.latestSubmitter = msg.sender; + } + + function correctKeysSignedTheStateUpdate( + bytes32 id, + address[] signingKeys, + SignedStateUpdate req + ) + private + pure + returns (bool) + { + bytes32 digest = computeStateHash( + id, + keccak256(req.encodedState), + req.nonce, + req.timeout + ); + return verifySignatures(req.signatures, digest, signingKeys); + } + + function correctKeySignedTheAction( + AppInterface appInterface, + address[] signingKeys, + uint256 disputeNonce, + SignedStateUpdate req, + SignedAction action + ) + private + view + returns (bool) + { + address turnTaker = MAppCaller.getTurnTaker( + appInterface, + signingKeys, + req.encodedState + ); + + address signer = recoverKey( + action.signature, + computeActionHash( + turnTaker, + keccak256(req.encodedState), + action.encodedAction, + req.nonce, + disputeNonce + ), + 0 + ); + + return turnTaker == signer; + } + +} diff --git a/packages/contracts/contracts/external/Proxy.sol b/packages/contracts/contracts/proxies/Proxy.sol similarity index 91% rename from packages/contracts/contracts/external/Proxy.sol rename to packages/contracts/contracts/proxies/Proxy.sol index 62b0f4068..f18367b5d 100644 --- a/packages/contracts/contracts/external/Proxy.sol +++ b/packages/contracts/contracts/proxies/Proxy.sol @@ -1,8 +1,7 @@ pragma solidity 0.4.25; -/// @title Proxy - Generic proxy contract allows to execute all transactions applying -/// the code of a master contract. +/// @title Proxy - Generic proxy contract allows to execute all transactions applying the code of a master contract. /// @author Stefan George - contract Proxy { @@ -14,7 +13,7 @@ contract Proxy { constructor(address _masterCopy) public { - require(_masterCopy != 0, "Proxy masterCopy address was 0x0"); + require(_masterCopy != 0, "Invalid master copy address provided"); masterCopy = _masterCopy; } diff --git a/packages/contracts/contracts/external/ProxyFactory.sol b/packages/contracts/contracts/proxies/ProxyFactory.sol similarity index 100% rename from packages/contracts/contracts/external/ProxyFactory.sol rename to packages/contracts/contracts/proxies/ProxyFactory.sol diff --git a/packages/contracts/contracts/MinimumViableMultisig.sol b/packages/contracts/contracts/state-deposit-holders/MinimumViableMultisig.sol similarity index 96% rename from packages/contracts/contracts/MinimumViableMultisig.sol rename to packages/contracts/contracts/state-deposit-holders/MinimumViableMultisig.sol index 023219958..137983b06 100644 --- a/packages/contracts/contracts/MinimumViableMultisig.sol +++ b/packages/contracts/contracts/state-deposit-holders/MinimumViableMultisig.sol @@ -1,6 +1,6 @@ pragma solidity 0.4.25; -import "./lib/Signatures.sol"; +import "../libs/LibSignature.sol"; /// @title MinimumViableMultisig - A multisig wallet supporting the minimum @@ -11,9 +11,9 @@ import "./lib/Signatures.sol"; /// (b) Requires n-of-n unanimous consent /// (c) Does not use on-chain address for signature verification /// (d) Uses hash-based instead of nonce-based replay protection -contract MinimumViableMultisig { +contract MinimumViableMultisig is LibSignature { - using Signatures for bytes; + address masterCopy; mapping(bytes32 => bool) isExecuted; @@ -57,7 +57,7 @@ contract MinimumViableMultisig { bytes32 transactionHash = getTransactionHash(to, value, data, operation); require( - signatures.verifySignatures(transactionHash, _owners), + verifySignatures(signatures, transactionHash, _owners), "Invalid signatures submitted to execTransaction" ); diff --git a/packages/contracts/contracts/fixtures/DelegateProxy.sol b/packages/contracts/contracts/test-fixtures/DelegateProxy.sol similarity index 100% rename from packages/contracts/contracts/fixtures/DelegateProxy.sol rename to packages/contracts/contracts/test-fixtures/DelegateProxy.sol diff --git a/packages/contracts/contracts/fixtures/DolphinCoin.sol b/packages/contracts/contracts/test-fixtures/DolphinCoin.sol similarity index 100% rename from packages/contracts/contracts/fixtures/DolphinCoin.sol rename to packages/contracts/contracts/test-fixtures/DolphinCoin.sol diff --git a/packages/contracts/contracts/fixtures/Echo.sol b/packages/contracts/contracts/test-fixtures/Echo.sol similarity index 100% rename from packages/contracts/contracts/fixtures/Echo.sol rename to packages/contracts/contracts/test-fixtures/Echo.sol diff --git a/packages/contracts/contracts/fixtures/ExampleCondition.sol b/packages/contracts/contracts/test-fixtures/ExampleCondition.sol similarity index 71% rename from packages/contracts/contracts/fixtures/ExampleCondition.sol rename to packages/contracts/contracts/test-fixtures/ExampleCondition.sol index 7de86993e..fc579b80f 100644 --- a/packages/contracts/contracts/fixtures/ExampleCondition.sol +++ b/packages/contracts/contracts/test-fixtures/ExampleCondition.sol @@ -1,8 +1,6 @@ pragma solidity 0.4.25; pragma experimental "ABIEncoderV2"; -import "../lib/Transfer.sol"; - contract ExampleCondition { @@ -13,15 +11,15 @@ contract ExampleCondition { function isSatisfiedNoParam() public pure - returns (bytes) + returns (bytes memory) { return abi.encode(true); } - function isSatisfiedParam(State state) + function isSatisfiedParam(State memory state) public pure - returns (bytes) + returns (bytes memory) { return abi.encode(state.ret); } diff --git a/packages/contracts/contracts/fixtures/ExampleTransfer.sol b/packages/contracts/contracts/test-fixtures/ExampleTransfer.sol similarity index 87% rename from packages/contracts/contracts/fixtures/ExampleTransfer.sol rename to packages/contracts/contracts/test-fixtures/ExampleTransfer.sol index 16f36098d..d3a6ab64b 100644 --- a/packages/contracts/contracts/fixtures/ExampleTransfer.sol +++ b/packages/contracts/contracts/test-fixtures/ExampleTransfer.sol @@ -1,7 +1,7 @@ pragma solidity 0.4.25; pragma experimental "ABIEncoderV2"; -import "../lib/Transfer.sol"; +import "../libs/Transfer.sol"; contract ExampleTransfer { diff --git a/packages/contracts/contracts/fixtures/ResolveToPay5ETHApp.sol b/packages/contracts/contracts/test-fixtures/ResolveToPay5WeiApp.sol similarity index 83% rename from packages/contracts/contracts/fixtures/ResolveToPay5ETHApp.sol rename to packages/contracts/contracts/test-fixtures/ResolveToPay5WeiApp.sol index e4e950859..453355614 100644 --- a/packages/contracts/contracts/fixtures/ResolveToPay5ETHApp.sol +++ b/packages/contracts/contracts/test-fixtures/ResolveToPay5WeiApp.sol @@ -1,14 +1,14 @@ pragma solidity 0.4.25; pragma experimental "ABIEncoderV2"; -import "../lib/Transfer.sol"; +import "../libs/Transfer.sol"; -/// @title ResolveToPay5ETHApp +/// @title ResolveToPay5WeiApp /// @notice This contract is a test fixture meant to emulate an AppInstance /// contract. An AppInstance has a getResolution() function that returns a /// `Transfer.Transaction` object when the channel is closed. -contract ResolveToPay5ETHApp { +contract ResolveToPay5WeiApp { function getResolution() public @@ -16,7 +16,7 @@ contract ResolveToPay5ETHApp { returns (Transfer.Transaction) { uint256[] memory amounts = new uint256[](1); - amounts[0] = 5 ether; + amounts[0] = 5 wei; address[] memory to = new address[](1); to[0] = address(0); diff --git a/packages/contracts/contracts/fixtures/TestCaller.sol b/packages/contracts/contracts/test-fixtures/TestCaller.sol similarity index 62% rename from packages/contracts/contracts/fixtures/TestCaller.sol rename to packages/contracts/contracts/test-fixtures/TestCaller.sol index 64c1fd933..f37182890 100644 --- a/packages/contracts/contracts/fixtures/TestCaller.sol +++ b/packages/contracts/contracts/test-fixtures/TestCaller.sol @@ -1,34 +1,35 @@ pragma solidity 0.4.25; -import "../lib/StaticCall.sol"; +import "../libs/LibStaticCall.sol"; contract TestCaller { - using StaticCall for address; + + using LibStaticCall for address; function execStaticCall( address to, bytes4 selector, - bytes calldata + bytes params ) public view returns (bytes) { - bytes memory data = abi.encodePacked(selector, calldata); + bytes memory data = abi.encodePacked(selector, params); return to.staticcall_as_bytes(data); } function execStaticCallBool( address to, bytes4 selector, - bytes calldata + bytes params ) public view returns (bool) { - bytes memory data = abi.encodePacked(selector, calldata); + bytes memory data = abi.encodePacked(selector, params); return to.staticcall_as_bool(data); } diff --git a/packages/contracts/index.ts b/packages/contracts/index.ts deleted file mode 100644 index 178cd64f8..000000000 --- a/packages/contracts/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./utils"; diff --git a/packages/contracts/migrations/1_initial_migration.js b/packages/contracts/migrations/1_initial_migration.js index 8ec07c0e2..0db1f156f 100644 --- a/packages/contracts/migrations/1_initial_migration.js +++ b/packages/contracts/migrations/1_initial_migration.js @@ -3,9 +3,10 @@ const tdr = require("truffle-deploy-registry"); const Migrations = artifacts.require("./Migrations.sol"); module.exports = function(deployer, network) { - deployer.deploy(Migrations).then(migrationsInstance => { - if (!tdr.isDryRunNetworkName(network)) { - return tdr.appendInstance(migrationsInstance); - } - }); + + const addToNetworkFile = (obj) => + tdr.isDryRunNetworkName(network) || tdr.appendInstance(obj); + + deployer.deploy(Migrations).then(addToNetworkFile); + }; diff --git a/packages/contracts/migrations/2_deploy_libraries.js b/packages/contracts/migrations/2_deploy_libraries.js index 63524f083..fa4127200 100644 --- a/packages/contracts/migrations/2_deploy_libraries.js +++ b/packages/contracts/migrations/2_deploy_libraries.js @@ -1,9 +1,9 @@ const tdr = require("truffle-deploy-registry"); -const Conditional = artifacts.require("Conditional"); -const ConditionalTransaction = artifacts.require("ConditionalTransaction"); -const StaticCall = artifacts.require("StaticCall"); +const StateChannelTransaction = artifacts.require("StateChannelTransaction"); +const LibStaticCall = artifacts.require("LibStaticCall"); const Transfer = artifacts.require("Transfer"); +const AppRegistry = artifacts.require("AppRegistry"); const VirtualAppAgreement = artifacts.require("ETHVirtualAppAgreement"); /// Deploy the libraries Transfer and StaticCall and link their dependents @@ -16,12 +16,14 @@ module.exports = (deployer, network) => { deployer.then(async () => { const transfer = await deployer.deploy(Transfer); + await deployer.link(Transfer, StateChannelTransaction); + await deployer.link(Transfer, AppRegistry); await deployer.link(Transfer, VirtualAppAgreement); - await deployer.link(Transfer, ConditionalTransaction); - const staticCall = await deployer.deploy(StaticCall); - await deployer.link(StaticCall, ConditionalTransaction); - await deployer.link(StaticCall, Conditional); + const staticCall = await deployer.deploy(LibStaticCall); + await deployer.link(LibStaticCall, AppRegistry); + await deployer.link(LibStaticCall, VirtualAppAgreement); + await deployer.link(LibStaticCall, StateChannelTransaction); if (!tdr.isDryRunNetworkName(network)) { await tdr.appendInstance(transfer); diff --git a/packages/contracts/migrations/3_deploy_contracts.js b/packages/contracts/migrations/3_deploy_contracts.js index e8347479e..b9c9caa20 100644 --- a/packages/contracts/migrations/3_deploy_contracts.js +++ b/packages/contracts/migrations/3_deploy_contracts.js @@ -1,42 +1,34 @@ const tdr = require("truffle-deploy-registry"); -const Conditional = artifacts.require("Conditional"); -const ConditionalTransaction = artifacts.require("ConditionalTransaction"); +const AppRegistry = artifacts.require("AppRegistry"); +const ContractRegistry = artifacts.require("ContractRegistry"); +const ETHBucket = artifacts.require("ETHBucket"); +const MinimumViableMultisig = artifacts.require("MinimumViableMultisig"); const MultiSend = artifacts.require("MultiSend"); const NonceRegistry = artifacts.require("NonceRegistry"); -const PaymentApp = artifacts.require("PaymentApp"); const ProxyFactory = artifacts.require("ProxyFactory"); -const Registry = artifacts.require("Registry"); -const Signatures = artifacts.require("Signatures"); -const VirtualAppAgreement = artifacts.require("ETHVirtualAppAgreement"); -const ETHBalanceRefundApp = artifacts.require("ETHBalanceRefundApp"); +const StateChannelTransaction = artifacts.require("StateChannelTransaction"); +const ETHVirtualAppAgreement = artifacts.require("ETHVirtualAppAgreement"); const ARTIFACTS = [ - ConditionalTransaction, - VirtualAppAgreement, + AppRegistry, + ContractRegistry, + ETHBucket, + MinimumViableMultisig, MultiSend, NonceRegistry, - PaymentApp, - ETHBalanceRefundApp, ProxyFactory, - Registry, - Signatures, - // FIXME: This doesn't need to be deployed, but eth-gas-reporter breaks - // if it isn't deployed. - Conditional + StateChannelTransaction, + ETHVirtualAppAgreement ]; module.exports = (deployer, network) => { deployer.then(async () => { - for (const artifact of ARTIFACTS) { const instance = await deployer.deploy(artifact); - if (!tdr.isDryRunNetworkName(network)) { await tdr.appendInstance(instance); } } - }); - }; diff --git a/packages/contracts/package.json b/packages/contracts/package.json index d952a798c..c10668765 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@counterfactual/contracts", - "version": "0.0.2", + "version": "0.0.3", "description": "Smart contracts for Counterfactual", "license": "MIT", "files": [ @@ -9,23 +9,17 @@ "networks" ], "scripts": { - "build": "truffle compile --all && tsc -b", - "migrate": "truffle migrate --reset", - "test": "tsc -b && truffle test --network ganache $npm_package_config_testFiles", - "test:windows": "tsc -b && truffle test --network ganache %npm_package_config_testFiles%", - "clean": "rm -rf dist build networks/7777777.json", + "build": "truffle compile --all", + "migrate": "truffle migrate", + "test": "./scripts/test.sh", "lint:fix": "yarn lint:ts:fix && yarn lint:sol:fix", "lint": "yarn lint:ts && yarn lint:sol", - "lint:sol:fix": "solium -d contracts/ --fix", + "lint:sol:fix": "yarn lint:sol --fix", "lint:sol": "solium -d .", - "lint:ts:fix": "tslint -c tslint.json -p . --fix", + "lint:ts:fix": "yarn lint:ts --fix", "lint:ts": "tslint -c tslint.json -p .", "test:coverage": "solidity-coverage" }, - "config": { - "testFiles": "dist/test/**/*.spec.js", - "unlockedAccount": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257a" - }, "keywords": [ "ethereum", "counterfactual", @@ -33,17 +27,15 @@ "solidity" ], "devDependencies": { + "@counterfactual/types": "0.0.1", "@counterfactual/typescript-typings": "0.0.1", - "@types/chai-as-promised": "^7.1.0", - "@types/chai-string": "^1.4.1", + "@types/chai": "^4.1.7", "@types/node": "^10.9.3", "chai": "^4.2.0", - "chai-as-promised": "^7.1.1", - "chai-bignumber": "^3.0.0", - "chai-string": "^1.5.0", "dotenv": "^6.1.0", "eth-gas-reporter": "0.1.12", - "ethers": "^4.0.17", + "ethereum-waffle": "1.2.0", + "ethers": "4.0.20", "ganache-cli": "6.2.5", "openzeppelin-eth": "^2.0.2", "shx": "^0.3.2", @@ -57,13 +49,5 @@ "tslint": "5.11.0", "typescript": "^3.1.2", "zos-lib": "^2.0.0" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/counterfactual/contracts.git" - }, - "bugs": { - "url": "https://github.com/counterfactual/contracts/issues" - }, - "homepage": "https://github.com/counterfactual/contracts#readme" + } } diff --git a/packages/contracts/scripts/test.sh b/packages/contracts/scripts/test.sh new file mode 100755 index 000000000..b76250cc5 --- /dev/null +++ b/packages/contracts/scripts/test.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -e + +function clean { + kill ${PID_FOR_GANACHE_CLI} +} + +trap clean INT TERM EXIT + +ganache-cli \ + --defaultBalanceEther 10000 \ + --gasLimit 0xfffffffffff \ + --gasPrice 0x01 \ + --networkId 7777777 \ + --quiet \ + &> /dev/null \ + & + +PID_FOR_GANACHE_CLI=$! + +yarn run tsc -p . + +yarn run truffle test --network ganache $1 diff --git a/packages/contracts/test/app-registry-new.spec.ts b/packages/contracts/test/app-registry-new.spec.ts new file mode 100644 index 000000000..e60868329 --- /dev/null +++ b/packages/contracts/test/app-registry-new.spec.ts @@ -0,0 +1,82 @@ +import { expect } from "chai"; +import { Contract, ContractFactory } from "ethers"; +import { AddressZero, HashZero } from "ethers/constants"; +import { JsonRpcSigner, Web3Provider } from "ethers/providers"; +import { hexlify, randomBytes } from "ethers/utils"; + +import { ALICE, BOB } from "./constants"; +import { AppInstance, AppInterface, AssetType, Terms } from "./utils"; + +const provider = new Web3Provider((global as any).web3.currentProvider); + +contract("AppRegistry - Counterparty is Unresponsive", (accounts: string[]) => { + let unlockedAccount: JsonRpcSigner; + let appRegistry: Contract; + + before(async () => { + unlockedAccount = await provider.getSigner(accounts[0]); + + const artifact = artifacts.require("AppRegistry"); + artifact.link(artifacts.require("LibStaticCall")); + artifact.link(artifacts.require("Transfer")); + + appRegistry = await await new ContractFactory( + artifact.abi, + artifact.binary, + unlockedAccount + ).deploy({ gasLimit: 6e9 }); + + await appRegistry.deployed(); + }); + + it("is possible to call setState to put state on-chain", async () => { + // Test AppInterface + const appInterface = new AppInterface( + AddressZero, + hexlify(randomBytes(4)), + hexlify(randomBytes(4)), + hexlify(randomBytes(4)), + hexlify(randomBytes(4)) + ); + + // Test Terms + const terms = new Terms(AssetType.ETH, 0, AddressZero); + + // Setup AppInstance + const appInstance = new AppInstance( + accounts[0], + [ALICE.address, BOB.address], + appInterface, + terms, + 10 + ); + + // Tell the AppRegistry to start timer + const stateHash = hexlify(randomBytes(32)); + await appRegistry.functions.setState(appInstance.appIdentity, { + stateHash, + nonce: 1, + timeout: 10, + signatures: HashZero + }); + + // Verify the correct data was put on-chain + const { + status, + latestSubmitter, + appStateHash, + disputeCounter, + disputeNonce, + finalizesAt, + nonce + } = await appRegistry.functions.appStates(appInstance.id); + + expect(status).to.be.eq(1); + expect(latestSubmitter).to.be.eq(await unlockedAccount.getAddress()); + expect(appStateHash).to.be.eq(stateHash); + expect(disputeCounter).to.be.eq(1); + expect(disputeNonce).to.be.eq(0); + expect(finalizesAt).to.be.eq((await provider.getBlockNumber()) + 10); + expect(nonce).to.be.eq(1); + }); +}); diff --git a/packages/contracts/test/app-registry.spec.ts b/packages/contracts/test/app-registry.spec.ts new file mode 100644 index 000000000..6ea6c9ca9 --- /dev/null +++ b/packages/contracts/test/app-registry.spec.ts @@ -0,0 +1,221 @@ +import { Contract, ContractFactory } from "ethers"; +import { AddressZero, HashZero } from "ethers/constants"; +import { JsonRpcSigner, Web3Provider } from "ethers/providers"; +import { hexlify, randomBytes } from "ethers/utils"; + +import { ALICE, BOB } from "./constants"; +import { + AppInstance, + AppInterface, + AssetType, + computeStateHash, + expect, + Terms +} from "./utils"; + +// HELPER DATA +const ONCHAIN_CHALLENGE_TIMEOUT = 30; + +contract("AppRegistry", (accounts: string[]) => { + let provider: Web3Provider; + let wallet: JsonRpcSigner; + let appRegistry: Contract; + + let setStateAsOwner: (nonce: number, appState?: string) => Promise; + let setStateWithSignatures: ( + nonce: number, + appState?: string + ) => Promise; + let cancelChallenge: () => Promise; + let sendSignedFinalizationToChain: () => Promise; + let latestState: () => Promise; + let latestNonce: () => Promise; + let isStateFinalized: () => Promise; + + before(async () => { + provider = new Web3Provider((global as any).web3.currentProvider); + + wallet = await provider.getSigner(accounts[0]); + + const artifact = artifacts.require("AppRegistry"); + artifact.link(artifacts.require("LibStaticCall")); + artifact.link(artifacts.require("Transfer")); + + appRegistry = await new ContractFactory( + artifact.abi, + artifact.binary, + wallet + ).deploy({ gasLimit: 6e9 }); + + await appRegistry.deployed(); + }); + + beforeEach(async () => { + const appInstance = new AppInstance( + accounts[0], + [ALICE.address, BOB.address], + new AppInterface( + hexlify(randomBytes(20)), + hexlify(randomBytes(4)), + hexlify(randomBytes(4)), + hexlify(randomBytes(4)), + hexlify(randomBytes(4)) + ), + new Terms(AssetType.ETH, 0, AddressZero), + 10 + ); + + latestState = async () => + (await appRegistry.functions.getAppChallenge(appInstance.id)) + .appStateHash; + + latestNonce = async () => + (await appRegistry.functions.getAppChallenge(appInstance.id)).nonce; + + isStateFinalized = async () => + await appRegistry.functions.isStateFinalized(appInstance.id); + + setStateAsOwner = (nonce: number, appState?: string) => + appRegistry.functions.setState(appInstance.appIdentity, { + nonce, + stateHash: appState || HashZero, + timeout: ONCHAIN_CHALLENGE_TIMEOUT, + signatures: HashZero + }); + + cancelChallenge = () => + appRegistry.functions.cancelChallenge(appInstance.appIdentity, HashZero); + + setStateWithSignatures = async (nonce: number, appState?: string) => + appRegistry.functions.setState(appInstance.appIdentity, { + nonce, + stateHash: appState || HashZero, + timeout: ONCHAIN_CHALLENGE_TIMEOUT, + signatures: await wallet.signMessage( + computeStateHash( + appInstance.id, + appState || HashZero, + nonce, + ONCHAIN_CHALLENGE_TIMEOUT + ) + ) + }); + + sendSignedFinalizationToChain = async () => + appRegistry.functions.setState(appInstance.appIdentity, { + nonce: (await latestNonce()) + 1, + stateHash: await latestState(), + timeout: 0, + signatures: await wallet.signMessage( + computeStateHash( + appInstance.id, + await latestState(), + await latestNonce(), + 0 + ) + ) + }); + }); + + describe("updating app state", async () => { + describe("with owner", async () => { + it("should work with higher nonce", async () => { + expect(await latestNonce()).to.eq(0); + await setStateAsOwner(1); + expect(await latestNonce()).to.eq(1); + }); + + it("should work many times", async () => { + expect(await latestNonce()).to.eq(0); + await setStateAsOwner(1); + expect(await latestNonce()).to.eq(1); + await cancelChallenge(); + await setStateAsOwner(2); + expect(await latestNonce()).to.eq(2); + await cancelChallenge(); + await setStateAsOwner(3); + expect(await latestNonce()).to.eq(3); + }); + + it("should work with much higher nonce", async () => { + expect(await latestNonce()).to.eq(0); + await setStateAsOwner(1000); + expect(await latestNonce()).to.eq(1000); + }); + + it("shouldn't work with an equal nonce", async () => { + await expect(setStateAsOwner(0)).to.be.reverted; + expect(await latestNonce()).to.eq(0); + }); + + it("shouldn't work with an lower nonce", async () => { + await setStateAsOwner(1); + await expect(setStateAsOwner(0)).to.be.reverted; + expect(await latestNonce()).to.eq(1); + }); + }); + + describe("with signing keys", async () => { + it("should work with higher nonce", async () => { + expect(await latestNonce()).to.eq(0); + await setStateWithSignatures(1); + expect(await latestNonce()).to.eq(1); + }); + + it("should work many times", async () => { + expect(await latestNonce()).to.eq(0); + await setStateWithSignatures(1); + expect(await latestNonce()).to.eq(1); + await cancelChallenge(); + await setStateWithSignatures(2); + expect(await latestNonce()).to.eq(2); + await cancelChallenge(); + await setStateWithSignatures(3); + expect(await latestNonce()).to.eq(3); + }); + + it("should work with much higher nonce", async () => { + expect(await latestNonce()).to.eq(0); + await setStateWithSignatures(1000); + expect(await latestNonce()).to.eq(1000); + }); + + it("shouldn't work with an equal nonce", async () => { + await expect(setStateWithSignatures(0)).to.be.reverted; + expect(await latestNonce()).to.eq(0); + }); + + it("shouldn't work with a lower nonce", async () => { + await setStateWithSignatures(1); + await expect(setStateWithSignatures(0)).to.be.reverted; + expect(await latestNonce()).to.eq(1); + }); + }); + }); + + describe("finalizing app state", async () => { + it("should work with keys", async () => { + expect(await isStateFinalized()).to.be.false; + await sendSignedFinalizationToChain(); + expect(await isStateFinalized()).to.be.true; + }); + }); + + describe("waiting for timeout", async () => { + it("should block updates after the timeout", async () => { + expect(await isStateFinalized()).to.be.false; + + await setStateAsOwner(1); + + for (const _ of Array(ONCHAIN_CHALLENGE_TIMEOUT + 1)) { + await provider.send("evm_mine", []); + } + + expect(await isStateFinalized()).to.be.true; + + await expect(setStateAsOwner(2)).to.be.reverted; + + await expect(setStateWithSignatures(0)).to.be.reverted; + }); + }); +}); diff --git a/packages/contracts/test/conditional-transaction.spec.ts b/packages/contracts/test/conditional-transaction.spec.ts new file mode 100644 index 000000000..1493e4d97 --- /dev/null +++ b/packages/contracts/test/conditional-transaction.spec.ts @@ -0,0 +1,141 @@ +import { Contract, ContractFactory } from "ethers"; +import { AddressZero, HashZero, WeiPerEther, Zero } from "ethers/constants"; +import { JsonRpcSigner, Web3Provider } from "ethers/providers"; +import { + defaultAbiCoder, + hexlify, + randomBytes, + solidityKeccak256 +} from "ethers/utils"; + +import { expect } from "./utils"; + +const provider = new Web3Provider((global as any).web3.currentProvider); + +contract("ConditionalTransaction", (accounts: string[]) => { + let unlockedAccount: JsonRpcSigner; + + let exampleCondition: Contract; + let delegateProxy: Contract; + let conditionalTransaction: Contract; + + before(async () => { + unlockedAccount = await provider.getSigner(accounts[0]); + + exampleCondition = await new ContractFactory( + artifacts.require("ExampleCondition").abi, + artifacts.require("ExampleCondition").bytecode, + unlockedAccount + ).deploy({ gasLimit: 6e9 }); + + delegateProxy = await new ContractFactory( + artifacts.require("DelegateProxy").abi, + artifacts.require("DelegateProxy").bytecode, + unlockedAccount + ).deploy({ gasLimit: 6e9 }); + + // Contract is already deployed when using Truffle Tests (re-uses migrations) + const artifact = artifacts.require("ConditionalTransaction"); + artifact.link(artifacts.require("Transfer")); + artifact.link(artifacts.require("LibStaticCall")); + conditionalTransaction = await new ContractFactory( + artifact.abi, + artifact.binary, + unlockedAccount + ).deploy({ gasLimit: 6e9 }); + + await exampleCondition.deployed(); + await delegateProxy.deployed(); + await conditionalTransaction.deployed(); + }); + + describe("Pre-commit to transfer details", () => { + const makeCondition = (expectedValue, onlyCheckForSuccess) => ({ + onlyCheckForSuccess, + expectedValueHash: solidityKeccak256(["bytes"], [expectedValue]), + parameters: HashZero, + selector: exampleCondition.interface.functions.isSatisfiedNoParam.sighash, + to: exampleCondition.address + }); + + const makeConditionParam = (expectedValue, parameters) => ({ + parameters, + expectedValueHash: solidityKeccak256(["bytes"], [expectedValue]), + onlyCheckForSuccess: false, + selector: exampleCondition.interface.functions.isSatisfiedParam.sighash, + to: exampleCondition.address + }); + + const trueParam = defaultAbiCoder.encode(["tuple(bool)"], [[true]]); + + const falseParam = defaultAbiCoder.encode(["tuple(bool)"], [[false]]); + + beforeEach(async () => { + await unlockedAccount.sendTransaction({ + to: delegateProxy.address, + value: WeiPerEther + }); + }); + + it("transfers the funds conditionally if true", async () => { + const randomTarget = hexlify(randomBytes(20)); + const tx = conditionalTransaction.interface.functions.executeSimpleConditionalTransaction.encode( + [ + makeCondition(HashZero, true), + { + value: [WeiPerEther], + assetType: 0, + to: [randomTarget], + token: AddressZero, + data: [] + } + ] + ); + + await delegateProxy.functions.delegate( + conditionalTransaction.address, + tx, + { + gasLimit: 600000 + } + ); + + const balTarget = await provider.getBalance(randomTarget); + expect(balTarget).to.eq(WeiPerEther); + + const emptyBalance = Zero; + const balDelegate = await provider.getBalance(delegateProxy.address); + expect(balDelegate).to.eq(emptyBalance); + }); + + it("does not transfer the funds conditionally if false", async () => { + const randomTarget = hexlify(randomBytes(20)); + const tx = conditionalTransaction.interface.functions.executeSimpleConditionalTransaction.encode( + [ + makeConditionParam(trueParam, falseParam), + { + value: [WeiPerEther], + assetType: 0, + to: [randomTarget], + token: AddressZero, + data: [] + } + ] + ); + + await expect( + delegateProxy.functions.delegate(conditionalTransaction.address, tx, { + gasLimit: 60000 + }) + // @ts-ignore + ).to.be.reverted; + + const emptyBalance = Zero; + const balTarget = await provider.getBalance(randomTarget); + expect(balTarget).to.eq(emptyBalance); + + const balDelegate = await provider.getBalance(delegateProxy.address); + expect(balDelegate).to.eq(WeiPerEther); + }); + }); +}); diff --git a/packages/contracts/test/constants.ts b/packages/contracts/test/constants.ts new file mode 100644 index 000000000..d9f5b7d9d --- /dev/null +++ b/packages/contracts/test/constants.ts @@ -0,0 +1,13 @@ +import { Wallet } from "ethers"; + +export const ALICE = + // 0xaeF082d339D227646DB914f0cA9fF02c8544F30b + new Wallet( + "0x3570f77380e22f8dc2274d8fd33e7830cc2d29cf76804e8c21f4f7a6cc571d27" + ); + +export const BOB = + // 0xb37e49bFC97A948617bF3B63BC6942BB15285715 + new Wallet( + "0x4ccac8b1e81fb18a98bbaf29b9bfe307885561f71b76bd4680d7aec9d0ddfcfd" + ); diff --git a/packages/contracts/test/contract-registry.spec.ts b/packages/contracts/test/contract-registry.spec.ts new file mode 100644 index 000000000..fdd45152e --- /dev/null +++ b/packages/contracts/test/contract-registry.spec.ts @@ -0,0 +1,146 @@ +import { Contract, ContractFactory } from "ethers"; +import { HashZero } from "ethers/constants"; +import { JsonRpcSigner, Web3Provider } from "ethers/providers"; +import { defaultAbiCoder, solidityKeccak256 } from "ethers/utils"; +import * as solc from "solc"; + +import { expect } from "./utils"; + +const provider = new Web3Provider((global as any).web3.currentProvider); + +const TEST_CONTRACT_SOLIDITY_CODE = ` + contract Test { + function sayHello() public pure returns (string) { + return "hi"; + } + }`; + +contract("ContractRegistry", accounts => { + let unlockedAccount: JsonRpcSigner; + + let contractRegistry: Contract; + let simpleContract: Contract; + + function cfaddress(initcode, i) { + return solidityKeccak256( + ["bytes1", "bytes", "uint256"], + ["0x19", initcode, i] + ); + } + + before(async () => { + unlockedAccount = await provider.getSigner(accounts[0]); + }); + + beforeEach(async () => { + contractRegistry = await new ContractFactory( + artifacts.require("ContractRegistry").abi, + artifacts.require("ContractRegistry").bytecode, + unlockedAccount + ).deploy({ gasLimit: 6e9 }); + + await contractRegistry.deployed(); + }); + + it("computes counterfactual addresses of bytes deployments", async () => { + expect(cfaddress(HashZero, 1)).to.eq( + await contractRegistry.functions.cfaddress(HashZero, 1) + ); + }); + + it("deploys a contract", done => { + const output = (solc as any).compile(TEST_CONTRACT_SOLIDITY_CODE, 0); + const iface = JSON.parse(output.contracts[":Test"].interface); + const bytecode = `0x${output.contracts[":Test"].bytecode}`; + + const filter = contractRegistry.filters.ContractCreated(null, null); + const callback = async (from, to, value, event) => { + const deployedAddress = value.args.deployedAddress; + expect(deployedAddress).to.eq( + await contractRegistry.resolver(cfaddress(bytecode, 2)) + ); + simpleContract = new Contract(deployedAddress, iface, unlockedAccount); + expect(await simpleContract.sayHello()).to.eq("hi"); + done(); + }; + const registryContract = contractRegistry.on(filter, callback); + registryContract.deploy(bytecode, 2); + }); + + it("deploys a contract using msg.sender", done => { + const output = (solc as any).compile(TEST_CONTRACT_SOLIDITY_CODE, 0); + const iface = JSON.parse(output.contracts[":Test"].interface); + const bytecode = `0x${output.contracts[":Test"].bytecode}`; + + const filter = contractRegistry.filters.ContractCreated(null, null); + const callback = async (from, to, value, event) => { + const deployedAddress = value.args.deployedAddress; + expect(deployedAddress).to.eq( + await contractRegistry.resolver(cfaddress(bytecode, 3)) + ); + + simpleContract = new Contract(deployedAddress, iface, unlockedAccount); + expect(await simpleContract.sayHello()).to.eq("hi"); + done(); + }; + const registryContract = contractRegistry.on(filter, callback); + registryContract.deploy(bytecode, 3); + }); + + it("deploys a Proxy contract contract through as owner", done => { + const output = (solc as any).compile(TEST_CONTRACT_SOLIDITY_CODE, 0); + const iface = JSON.parse(output.contracts[":Test"].interface); + const initcode = + artifacts.require("Proxy").bytecode + + defaultAbiCoder.encode(["address"], [simpleContract.address]).substr(2); + + const filter = contractRegistry.filters.ContractCreated(null, null); + const callback = async (from, to, value, event) => { + const deployedAddress = value.args.deployedAddress; + expect(deployedAddress).to.eq( + await contractRegistry.resolver(cfaddress(initcode, 3)) + ); + + const contract = new Contract(deployedAddress, iface, unlockedAccount); + expect(await contract.sayHello()).to.eq("hi"); + done(); + }; + + const registryContract = contractRegistry.on(filter, callback); + registryContract.deploy(initcode, 3); + }); + + it("deploys a contract and passes arguments", done => { + const source = ` + contract Test { + address whatToSay; + function Test(address _whatToSay) public { + whatToSay = _whatToSay; + } + function sayHello() public view returns (address) { + return whatToSay; + } + }`; + const output = (solc as any).compile(source, 0); + const iface = JSON.parse(output.contracts[":Test"].interface); + const bytecode = `0x${output.contracts[":Test"].bytecode}`; + + const initcode = + bytecode + defaultAbiCoder.encode(["address"], [accounts[0]]).substr(2); + + const filter = contractRegistry.filters.ContractCreated(null, null); + const callback = async (from, to, value, event) => { + const deployedAddress = value.args.deployedAddress; + expect(deployedAddress).to.eq( + await contractRegistry.resolver(cfaddress(initcode, 4)) + ); + + const contract = new Contract(deployedAddress, iface, unlockedAccount); + expect(await contract.sayHello()).to.eq(accounts[0]); + done(); + }; + + const registryContract = contractRegistry.on(filter, callback); + registryContract.deploy(initcode, 4); + }); +}); diff --git a/packages/contracts/test/eth-virtual-app-agreement.spec.ts b/packages/contracts/test/eth-virtual-app-agreement.spec.ts new file mode 100644 index 000000000..c34cd475b --- /dev/null +++ b/packages/contracts/test/eth-virtual-app-agreement.spec.ts @@ -0,0 +1,263 @@ +import { Contract, ContractFactory, Wallet } from "ethers"; +import { AddressZero, HashZero } from "ethers/constants"; +import { JsonRpcSigner, Web3Provider } from "ethers/providers"; +import { + BigNumber, + bigNumberify, + defaultAbiCoder, + keccak256 +} from "ethers/utils"; + +import { expect } from "./utils/index"; + +const provider = new Web3Provider((global as any).web3.currentProvider); + +contract("ETHVirtualAppAgreement", (accounts: string[]) => { + let unlockedAccount: JsonRpcSigner; + + let appRegistry: Contract; + let virtualAppAgreement: Contract; + let fixedResolutionApp: Contract; + let appInstanceId: string; + + /// Deploys a new DelegateProxy instance, funds it, and delegatecalls to + /// FixedResolutionApp with random beneficiaries + const delegatecallVirtualAppAgreement = async ( + virtualAppAgreement: Contract, + appRegistry: Contract, + resolutionAddr: string, + expiry: number, + capitalProvided: BigNumber, + assetType: number + ): Promise => { + const delegateProxy = await new ContractFactory( + artifacts.require("DelegateProxy").abi, + artifacts.require("DelegateProxy").binary, + unlockedAccount + ).deploy({ gasLimit: 6e9 }); + + await delegateProxy.deployed(); + + await unlockedAccount.sendTransaction({ + to: delegateProxy.address, + value: bigNumberify(100) + }); + + const beneficiaries = [ + Wallet.createRandom().address, + Wallet.createRandom().address + ]; + + const tx = virtualAppAgreement.interface.functions.delegateTarget.encode([ + { + beneficiaries, + expiry, + capitalProvided, + registry: appRegistry.address, + terms: { + assetType, + limit: 0, + token: AddressZero + }, + appInstanceId: resolutionAddr + } + ]); + + expect(await provider.getBalance(beneficiaries[0])).to.eq(0); + expect(await provider.getBalance(beneficiaries[1])).to.eq(0); + + await delegateProxy.functions.delegate(virtualAppAgreement.address, tx, { + gasLimit: 150000 + }); + + return beneficiaries; + }; + + before(async () => { + unlockedAccount = await provider.getSigner(accounts[0]); + + const artifact1 = artifacts.require("ETHVirtualAppAgreement"); + artifact1.link(artifacts.require("Transfer")); + + virtualAppAgreement = await new ContractFactory( + artifact1.abi, + artifact1.binary, + unlockedAccount + ).deploy({ gasLimit: 6e9 }); + + const artifact2 = artifacts.require("AppRegistry"); + artifact2.link(artifacts.require("LibStaticCall")); + artifact2.link(artifacts.require("Transfer")); + + appRegistry = await await new ContractFactory( + artifact2.abi, + artifact2.binary, + unlockedAccount + ).deploy({ gasLimit: 6e9 }); + + await appRegistry.deployed(); + + fixedResolutionApp = await new ContractFactory( + artifacts.require("ResolveToPay5WeiApp").abi, + artifacts.require("ResolveToPay5WeiApp").binary, + unlockedAccount + ).deploy({ gasLimit: 6e9 }); + + await appRegistry.deployed(); + await virtualAppAgreement.deployed(); + await fixedResolutionApp.deployed(); + + const appInterface = { + addr: fixedResolutionApp.address, + getTurnTaker: "0x00000000", + applyAction: "0x00000000", + resolve: fixedResolutionApp.interface.functions.getResolution.sighash, + isStateTerminal: "0x00000000" + }; + + const terms = { + assetType: 0, + limit: 0, + token: AddressZero + }; + + const encodedTerms = defaultAbiCoder.encode( + [ + `tuple( + uint8 assetType, + uint256 limit, + address token + )` + ], + [terms] + ); + + const appIdentity = { + owner: await unlockedAccount.getAddress(), + signingKeys: [], + appInterfaceHash: keccak256( + defaultAbiCoder.encode( + [ + `tuple( + address addr, + bytes4 getTurnTaker, + bytes4 applyAction, + bytes4 resolve, + bytes4 isStateTerminal + )` + ], + [appInterface] + ) + ), + termsHash: keccak256(encodedTerms), + defaultTimeout: 10 + }; + + appInstanceId = keccak256( + defaultAbiCoder.encode( + [ + `tuple( + address owner, + address[] signingKeys, + bytes32 appInterfaceHash, + bytes32 termsHash, + uint256 defaultTimeout + )` + ], + [appIdentity] + ) + ); + + await appRegistry.functions.setState(appIdentity, { + stateHash: keccak256(HashZero), + nonce: 1, + timeout: 0, + signatures: HashZero + }); + + // Can be called immediately without waiting for blocks to be mined + // because the timeout was set to 0 in the previous call to setState + await appRegistry.functions.setResolution( + appIdentity, + appInterface, + HashZero, + encodedTerms + ); + }); + + describe("ETHVirtualAppAgreement", () => { + // TODO: This test should work after having done setResolution above but hard not + // been finished yet. Leaving it here as skipped until we implement it. + it.skip("succeeds with a valid resolution and elapsed lockup period", async () => { + const beneficiaries = await delegatecallVirtualAppAgreement( + virtualAppAgreement, + appRegistry, + appInstanceId, + 0, + bigNumberify(10), + 0 + ); + expect(await provider.getBalance(beneficiaries[0])).to.eq( + bigNumberify(5) + ); + expect(await provider.getBalance(beneficiaries[1])).to.eq( + bigNumberify(5) + ); + }); + + it("fails with invalid resolution target", async () => { + await expect( + delegatecallVirtualAppAgreement( + virtualAppAgreement, + appRegistry, + HashZero, + 0, + bigNumberify(10), + 0 + ) + ).to.be.reverted; + }); + + it("fails if called before agreement expiry", async () => { + await expect( + delegatecallVirtualAppAgreement( + virtualAppAgreement, + appRegistry, + appInstanceId, + (await provider.getBlockNumber()) + 10, + bigNumberify(10), + 0 + ) + // TODO: Add revert reason (to.be.revertedWith(...)) + ).to.be.reverted; + }); + + it("fails if resolution value is larger than capital provided", async () => { + await expect( + delegatecallVirtualAppAgreement( + virtualAppAgreement, + appRegistry, + appInstanceId, + 0, + bigNumberify(2), + 0 + ) + // TODO: Add revert reason (to.be.revertedWith(...)) + ).to.be.reverted; + }); + + it("fails if resolution returns different token type", async () => { + await expect( + delegatecallVirtualAppAgreement( + virtualAppAgreement, + appRegistry, + appInstanceId, + 0, + bigNumberify(10), + 1 + ) + // TODO: Add revert reason (to.be.revertedWith(...)) + ).to.be.reverted; + }); + }); +}); diff --git a/packages/contracts/test/integration/commit-reveal.spec.ts b/packages/contracts/test/integration/commit-reveal.spec.ts deleted file mode 100644 index 4f1a91307..000000000 --- a/packages/contracts/test/integration/commit-reveal.spec.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "ethers"; - -import { - abiEncodingForStruct, - AbstractContract, - AppInstance, - AssetType, - buildArtifacts, - computeNonceRegistryKey, - Multisig, - TransferTerms -} from "../../utils"; -import * as Utils from "../../utils/misc"; - -const web3 = (global as any).web3; -const { provider, unlockedAccount } = Utils.setupTestEnv(web3); - -const { - Registry, - NonceRegistry, - ConditionalTransaction, - StaticCall -} = buildArtifacts; - -/// Returns the commit hash that can be used to commit to chosenNumber -/// using appSalt -function computeCommitHash(appSalt: string, chosenNumber: number) { - return ethers.utils.solidityKeccak256( - ["bytes32", "uint256"], - [appSalt, chosenNumber] - ); -} - -enum Stage { - SETTING_MAX, - CHOOSING, - GUESSING, - REVEALING, - DONE -} - -enum Player { - CHOOSING = 0, - GUESSING = 1 -} - -const { parseEther } = ethers.utils; -const commitRevealAppDefinition = AbstractContract.fromArtifactName( - "CommitRevealApp", - { - StaticCall - } -); - -const appStateEncoding = abiEncodingForStruct(` - address[2] playerAddrs; - uint256 stage; - uint256 maximum; - uint256 guessedNumber; - bytes32 commitHash; - uint256 winner; -`); - -async function createMultisig( - sender: ethers.Wallet, - initialFunding: ethers.utils.BigNumber, - owners: ethers.Wallet[] -): Promise { - const multisig = new Multisig(owners.map(w => w.address)); - await multisig.deploy(sender); - await sender.sendTransaction({ - to: multisig.address, - value: initialFunding - }); - return multisig; -} - -async function deployAppDefinition(): Promise { - return (await commitRevealAppDefinition).deploy(unlockedAccount); -} - -async function deployAppInstance( - multisig: Multisig, - appContract: ethers.Contract, - terms: TransferTerms -) { - const registry = await (await Registry).getDeployed(unlockedAccount); - const signers = multisig.owners; // TODO: generate new signing keys for each state channel - const appInstance = new AppInstance( - signers, - multisig, - appContract, - appStateEncoding, - terms - ); - await appInstance.deploy(unlockedAccount, registry); - if (!appInstance.contract) { - throw new Error("Deploy failed"); - } - return appInstance; -} - -async function executeStateChannelTransaction( - stateChannel: AppInstance, - multisig: Multisig, - uninstallNonceKey: string, - signers: ethers.Wallet[] -) { - if (!stateChannel.contract) { - throw new Error("Deploy failed"); - } - const conditionalTransaction = await (await ConditionalTransaction).getDeployed( - unlockedAccount - ); - const registry = await (await Registry).getDeployed(unlockedAccount); - const nonceRegistry = await (await NonceRegistry).getDeployed( - unlockedAccount - ); - - await multisig.execDelegatecall( - conditionalTransaction, - "executeAppConditionalTransaction", - [ - registry.address, - nonceRegistry.address, - uninstallNonceKey, - stateChannel.contract.cfAddress, - stateChannel.terms - ], - signers - ); -} - -describe("CommitReveal", async () => { - it("should pay out to the winner", async function() { - // @ts-ignore - this.timeout(4000); - - const [alice, bob] = Utils.generateEthWallets(2, provider); - - // 1. Deploy & fund multisig - const multisig = await createMultisig(unlockedAccount, parseEther("2"), [ - alice, - bob - ]); - - // 2. Deploy CommitRevealApp AppDefinition - const appContract = await deployAppDefinition(); - - // 3. Deploy StateChannel - const terms = { - assetType: AssetType.ETH, - limit: parseEther("2") - }; - const stateChannel = await deployAppInstance(multisig, appContract, terms); - - // 4. Call setState(claimFinal=true) on StateChannel with a final state - const numberSalt = - "0xdfdaa4d168f0be935a1e1d12b555995bc5ea67bd33fce1bc5be0a1e0a381fc94"; - - const chosenNumber = 5; - - const commitHash = computeCommitHash(numberSalt, chosenNumber); - - const appState = { - commitHash, - playerAddrs: [alice.address, bob.address], - stage: Stage.DONE, - maximum: 10, - guessedNumber: 1, - winner: Player.CHOOSING - }; - await stateChannel.setState(appState, [alice, bob]); - - // 5. Call setResolution() on StateChannel - await stateChannel.setResolution(appState); - - // 6. Compute channel nonce key - // TODO: @scalefree Document source of this string. - const channelNonceSalt = - "0x3004efe76b684aef3c1b29448e84d461ff211ddba19cdf75eb5e31eebbb6999b"; - - const channelNonceKey = computeNonceRegistryKey( - new ethers.utils.BigNumber(0), - multisig.address, - channelNonceSalt - ); - - // 7. Call executeStateChannelConditionalTransaction on ConditionalTransaction from multisig - await executeStateChannelTransaction( - stateChannel, - multisig, - channelNonceKey, - [alice, bob] - ); - - // 8. Verify balance of A and B - expect((await alice.getBalance()).toString()).to.eql( - parseEther("2").toString() - ); - expect((await bob.getBalance()).toString()).to.eql( - parseEther("0").toString() - ); - }); -}); diff --git a/packages/contracts/test/integration/counting-app.spec.ts b/packages/contracts/test/integration/counting-app.spec.ts deleted file mode 100644 index 747c15cee..000000000 --- a/packages/contracts/test/integration/counting-app.spec.ts +++ /dev/null @@ -1,311 +0,0 @@ -import { ethers } from "ethers"; - -import { AbstractContract, buildArtifacts, expect } from "../../utils"; -import * as Utils from "../../utils/misc"; - -const web3 = (global as any).web3; -const { provider, unlockedAccount } = Utils.setupTestEnv(web3); - -const [A, B] = [ - // 0xaeF082d339D227646DB914f0cA9fF02c8544F30b - new ethers.Wallet( - "0x3570f77380e22f8dc2274d8fd33e7830cc2d29cf76804e8c21f4f7a6cc571d27" - ), - // 0xb37e49bFC97A948617bF3B63BC6942BB15285715 - new ethers.Wallet( - "0x4ccac8b1e81fb18a98bbaf29b9bfe307885561f71b76bd4680d7aec9d0ddfcfd" - ) -]; - -const computeStateHash = (stateHash: string, nonce: number, timeout: number) => - ethers.utils.keccak256( - ethers.utils.solidityPack( - ["bytes1", "address[]", "uint256", "uint256", "bytes32"], - ["0x19", [A.address, B.address], nonce, timeout, stateHash] - ) - ); - -const computeActionHash = ( - turn: string, - prevState: string, - action: string, - setStateNonce: number, - disputeNonce: number -) => - ethers.utils.keccak256( - ethers.utils.solidityPack( - ["bytes1", "address", "bytes32", "bytes", "uint256", "uint256"], - ["0x19", turn, prevState, action, setStateNonce, disputeNonce] - ) - ); - -contract("CountingApp", (accounts: string[]) => { - let game: ethers.Contract; - let stateChannel: ethers.Contract; - - const exampleState = { - player1: A.address, - player2: B.address, - count: 0, - turnNum: 0 - }; - - enum AssetType { - ETH, - ERC20, - ANY - } - - const encode = (encoding: string, state: any) => - ethers.utils.defaultAbiCoder.encode([encoding], [state]); - - const latestNonce = async () => stateChannel.functions.latestNonce(); - - // TODO: Wait for this to work: - // ethers.utils.formatParamType(iface.functions.resolve.inputs[0]) - // github.com/ethers-io/ethers.js/blob/typescript/src.ts/utils/abi-coder.ts#L301 - const gameEncoding = - "tuple(address player1, address player2, uint256 count, uint256 turnNum)"; - - const appEncoding = - "tuple(address addr, bytes4 applyAction, bytes4 resolve, bytes4 getTurnTaker, bytes4 isStateTerminal)"; - - const termsEncoding = "tuple(uint8 assetType, uint256 limit, address token)"; - - const { keccak256 } = ethers.utils; - - const sendSignedFinalizationToChain = async (stateHash: string) => - stateChannel.functions.setState( - stateHash, - await latestNonce(), - 0, - Utils.signMessage( - computeStateHash( - stateHash || ethers.constants.HashZero, - await latestNonce(), - 0 - ), - unlockedAccount - ) - ); - - let app; - let terms; - beforeEach(async () => { - const networkID = await AbstractContract.getNetworkID(unlockedAccount); - const staticCall = AbstractContract.fromArtifactName("StaticCall"); - const appInstance = await buildArtifacts.AppInstance; - const countingApp = await AbstractContract.fromArtifactName("CountingApp", { - StaticCall: staticCall - }); - - game = await countingApp.deploy(unlockedAccount); - - app = { - addr: game.address, - resolve: game.interface.functions.resolve.sighash, - applyAction: game.interface.functions.applyAction.sighash, - getTurnTaker: game.interface.functions.getTurnTaker.sighash, - isStateTerminal: game.interface.functions.isStateTerminal.sighash - }; - - terms = { - assetType: AssetType.ETH, - limit: Utils.UNIT_ETH.mul(2), - token: ethers.constants.AddressZero - }; - - const contractFactory = new ethers.ContractFactory( - appInstance.abi, - await appInstance.generateLinkedBytecode(networkID), - unlockedAccount - ); - - stateChannel = await contractFactory.deploy( - accounts[0], - [A.address, B.address], - keccak256(encode(appEncoding, app)), - keccak256(encode(termsEncoding, terms)), - 10 - ); - }); - - it("should resolve to some balance", async () => { - const ret = await game.functions.resolve(exampleState, terms); - expect(ret.assetType).to.eql(AssetType.ETH); - expect(ret.token).to.be.equalIgnoreCase(ethers.constants.AddressZero); - expect(ret.to[0]).to.be.equalIgnoreCase(A.address); - expect(ret.to[1]).to.be.equalIgnoreCase(B.address); - expect(ret.value[0].toString()).to.be.eql(Utils.UNIT_ETH.mul(2).toString()); - expect(ret.value[1]).to.be.eql(new ethers.utils.BigNumber(0)); - }); - - describe("setting a resolution", async () => { - it("should fail before state is settled", async () => { - const finalState = encode(gameEncoding, exampleState); - await Utils.assertRejects( - stateChannel.functions.setResolution( - app, - finalState, - encode(termsEncoding, terms) - ) - ); - }); - it("should succeed after state is settled", async () => { - const finalState = encode(gameEncoding, exampleState); - await sendSignedFinalizationToChain(keccak256(finalState)); - await stateChannel.functions.setResolution( - app, - finalState, - encode(termsEncoding, terms) - ); - const ret = await stateChannel.functions.getResolution(); - expect(ret.assetType).to.be.eql(AssetType.ETH); - expect(ret.token).to.be.equalIgnoreCase(ethers.constants.AddressZero); - expect(ret.to[0]).to.be.equalIgnoreCase(A.address); - expect(ret.to[1]).to.be.equalIgnoreCase(B.address); - expect(ret.value[0].toString()).to.be.eql( - Utils.UNIT_ETH.mul(2).toString() - ); - expect(ret.value[1]).to.be.eql(new ethers.utils.BigNumber(0)); - }); - }); - - describe("handling a dispute", async () => { - enum ActionTypes { - INCREMENT, - DECREMENT - } - - enum Status { - ON, - DISPUTE, - OFF - } - - const actionEncoding = "tuple(uint8 actionType, uint256 byHowMuch)"; - - const state = encode(gameEncoding, exampleState); - - it("should update state based on applyAction", async () => { - const action = { - actionType: ActionTypes.INCREMENT, - byHowMuch: 1 - }; - - const h1 = computeStateHash(keccak256(state), 1, 10); - const h2 = computeActionHash( - A.address, - keccak256(state), - encode(actionEncoding, action), - 1, - 0 - ); - - await stateChannel.functions.createDispute( - app, - state, - 1, - 10, - encode(actionEncoding, action), - Utils.signMessage(h1, A, B), - Utils.signMessage(h2, A), - false - ); - - const onchain = await stateChannel.functions.state(); - - const expectedState = { ...exampleState, count: 1, turnNum: 1 }; - const expectedStateHash = keccak256(encode(gameEncoding, expectedState)); - const expectedFinalizeBlock = (await provider.getBlockNumber()) + 10; - - expect(onchain.status).to.be.eql(Status.DISPUTE); - expect(onchain.appStateHash).to.be.equalIgnoreCase(expectedStateHash); - expect(onchain.latestSubmitter).to.be.equalIgnoreCase(accounts[0]); - expect(onchain.nonce).to.be.eql(new ethers.utils.BigNumber(1)); - expect(onchain.disputeNonce).to.be.eql(new ethers.utils.BigNumber(0)); - expect(onchain.disputeCounter).to.be.eql(new ethers.utils.BigNumber(1)); - expect(onchain.finalizesAt).to.be.eql( - new ethers.utils.BigNumber(expectedFinalizeBlock) - ); - }); - - it("should update and finalize state based on applyAction", async () => { - const action = { - actionType: ActionTypes.INCREMENT, - byHowMuch: 2.0 - }; - - const h1 = computeStateHash(keccak256(state), 1, 10); - const h2 = computeActionHash( - A.address, - keccak256(state), - encode(actionEncoding, action), - 1, - 0 - ); - - await stateChannel.functions.createDispute( - app, - state, - 1, - 10, - encode(actionEncoding, action), - Utils.signMessage(h1, A, B), - Utils.signMessage(h2, A), - true - ); - - const channelState = await stateChannel.functions.state(); - - const expectedState = { ...exampleState, count: 2, turnNum: 1 }; - const expectedStateHash = keccak256(encode(gameEncoding, expectedState)); - const expectedFinalizeBlock = await provider.getBlockNumber(); - - expect(channelState.status).to.be.eql(Status.OFF); - expect(channelState.appStateHash).to.be.equalIgnoreCase( - expectedStateHash - ); - expect(channelState.latestSubmitter).to.be.equalIgnoreCase(accounts[0]); - expect(channelState.nonce).to.be.eql(new ethers.utils.BigNumber(1)); - expect(channelState.disputeNonce).to.be.eql( - new ethers.utils.BigNumber(0) - ); - expect(channelState.disputeCounter).to.be.eql( - new ethers.utils.BigNumber(1) - ); - expect(channelState.finalizesAt).to.be.eql( - new ethers.utils.BigNumber(expectedFinalizeBlock) - ); - }); - - it("should fail when trying to finalize a non-final state", async () => { - const action = { - actionType: ActionTypes.INCREMENT, - byHowMuch: 1.0 - }; - - const h1 = computeStateHash(keccak256(state), 1, 10); - const h2 = computeActionHash( - A.address, - keccak256(state), - encode(actionEncoding, action), - 1, - 0 - ); - - await Utils.assertRejects( - stateChannel.functions.createDispute( - app, - state, - 1, - 10, - encode(actionEncoding, action), - Utils.signMessage(h1, A, B), - Utils.signMessage(h2, A), - true - ) - ); - }); - }); -}); diff --git a/packages/contracts/test/integration/eth-virtual-app-agreement.spec.ts b/packages/contracts/test/integration/eth-virtual-app-agreement.spec.ts deleted file mode 100644 index 8b17b72d6..000000000 --- a/packages/contracts/test/integration/eth-virtual-app-agreement.spec.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { Contract, Wallet } from "ethers"; -import { AddressZero, HashZero } from "ethers/constants"; -import { BigNumber, parseEther } from "ethers/utils"; - -import { AbstractContract, buildArtifacts, expect } from "../../utils"; -import * as Utils from "../../utils/misc"; - -const web3 = (global as any).web3; -const { provider, unlockedAccount } = Utils.setupTestEnv(web3); - -/// Deploys a new DelegateProxy instance, funds it, and delegatecalls to -/// FixedResolutionApp with random beneficiaries -const delegatecallVirtualAppAgreement = async ( - virtualAppAgreement: Contract, - registry: Contract, - resolutionAddr: string, - expiry: number, - capitalProvided: BigNumber, - assetType: number -): Promise => { - const delegateProxy = await (await AbstractContract.fromArtifactName( - "DelegateProxy" - )).deploy(unlockedAccount); - - await unlockedAccount.sendTransaction({ - to: delegateProxy.address, - value: parseEther("100") - }); - - const beneficiaries = [ - Wallet.createRandom().address, - Wallet.createRandom().address - ]; - - const tx = virtualAppAgreement.interface.functions.delegateTarget.encode([ - { - beneficiaries, - expiry, - capitalProvided, - registry: registry.address, - terms: { - assetType, - limit: 0, - token: AddressZero - }, - target: resolutionAddr - } - ]); - - expect( - (await unlockedAccount.provider.getBalance(beneficiaries[0])).toString() - ).to.eq("0"); - expect( - (await unlockedAccount.provider.getBalance(beneficiaries[1])).toString() - ).to.eq("0"); - - await delegateProxy.functions.delegate(virtualAppAgreement.address, tx, { - gasLimit: 150000 - }); - return beneficiaries; -}; - -contract("Virtual App", (accounts: string[]) => { - let virtualAppAgreement: Contract; - let registry: Contract; - let fixedResolutionApp: Contract; - - // @ts-ignore - before(async () => { - virtualAppAgreement = await (await buildArtifacts.VirtualAppAgreement).deploy( - unlockedAccount - ); - registry = await (await buildArtifacts.Registry).deploy(unlockedAccount); - fixedResolutionApp = await (await AbstractContract.fromArtifactName( - "ResolveToPay5ETHApp", - { - Transfer: buildArtifacts.Transfer - } - )).deployViaRegistry(unlockedAccount, registry); - }); - - describe("ETHVirtualAppAgreement", () => { - it("succeeds with a valid resolution and elapsed lockup period", async () => { - const beneficiaries = await delegatecallVirtualAppAgreement( - virtualAppAgreement, - registry, - fixedResolutionApp.cfAddress, - 0, - parseEther("10"), - 0 - ); - expect( - (await unlockedAccount.provider.getBalance(beneficiaries[0])).toString() - ).to.eq(parseEther("5").toString()); - expect( - (await unlockedAccount.provider.getBalance(beneficiaries[1])).toString() - ).to.eq(parseEther("5").toString()); - }); - it("fails with invalid resolution target", async () => { - await Utils.assertRejects( - delegatecallVirtualAppAgreement( - virtualAppAgreement, - registry, - HashZero, - 0, - parseEther("10"), - 0 - ) - ); - }); - it("fails if called before agreement expiry", async () => { - await Utils.assertRejects( - delegatecallVirtualAppAgreement( - virtualAppAgreement, - registry, - fixedResolutionApp.cfAddress, - (await provider.getBlockNumber()) + 10, - parseEther("10"), - 0 - ) - ); - }); - it("fails if resolution value is larger than capital provided", async () => { - await Utils.assertRejects( - delegatecallVirtualAppAgreement( - virtualAppAgreement, - registry, - fixedResolutionApp.cfAddress, - 0, - parseEther("2"), - 0 - ) - ); - }); - it("fails if resolution returns different token type", async () => { - await Utils.assertRejects( - delegatecallVirtualAppAgreement( - virtualAppAgreement, - registry, - fixedResolutionApp.cfAddress, - 0, - parseEther("10"), - 1 - ) - ); - }); - }); -}); diff --git a/packages/contracts/test/integration/nim.spec.ts b/packages/contracts/test/integration/nim.spec.ts deleted file mode 100644 index dc38819f8..000000000 --- a/packages/contracts/test/integration/nim.spec.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { ethers } from "ethers"; - -import { AbstractContract, expect } from "../../utils"; -import * as Utils from "../../utils/misc"; - -const web3 = (global as any).web3; -const { unlockedAccount } = Utils.setupTestEnv(web3); - -contract("Nim", (accounts: string[]) => { - let game: ethers.Contract; - - const stateEncoding = - "tuple(address[2] players, uint256 turnNum, uint256[3] pileHeights)"; - - beforeEach(async () => { - const staticCall = AbstractContract.fromArtifactName("StaticCall"); - const nim = await AbstractContract.fromArtifactName("Nim", { - StaticCall: staticCall - }); - game = await nim.deploy(unlockedAccount); - }); - - describe("applyAction", () => { - it("can take from a pile", async () => { - const preState = { - players: [ethers.constants.AddressZero, ethers.constants.AddressZero], - turnNum: 0, - pileHeights: [6, 5, 12] - }; - - const action = { - pileIdx: 0, - takeAmnt: 5 - }; - - const ret = await game.functions.applyAction(preState, action); - - const postState = ethers.utils.defaultAbiCoder.decode( - [stateEncoding], - ret - )[0]; - - expect(postState.pileHeights[0]).to.be.eql(new ethers.utils.BigNumber(1)); - expect(postState.pileHeights[1]).to.be.eql(new ethers.utils.BigNumber(5)); - expect(postState.pileHeights[2]).to.be.eql( - new ethers.utils.BigNumber(12) - ); - expect(postState.turnNum).to.be.eql(new ethers.utils.BigNumber(1)); - }); - - it("can take to produce an empty pile", async () => { - const preState = { - players: [ethers.constants.AddressZero, ethers.constants.AddressZero], - turnNum: 0, - pileHeights: [6, 5, 12] - }; - - const action = { - pileIdx: 0, - takeAmnt: 6 - }; - - const ret = await game.functions.applyAction(preState, action); - - const postState = ethers.utils.defaultAbiCoder.decode( - [stateEncoding], - ret - )[0]; - - expect(postState.pileHeights[0]).to.be.eql(new ethers.utils.BigNumber(0)); - expect(postState.pileHeights[1]).to.be.eql(new ethers.utils.BigNumber(5)); - expect(postState.pileHeights[2]).to.be.eql( - new ethers.utils.BigNumber(12) - ); - expect(postState.turnNum).to.be.eql(new ethers.utils.BigNumber(1)); - }); - - it("should fail for taking too much", async () => { - const preState = { - players: [ethers.constants.AddressZero, ethers.constants.AddressZero], - turnNum: 0, - pileHeights: [6, 5, 12] - }; - - const action = { - pileIdx: 0, - takeAmnt: 7 - }; - - await Utils.assertRejects(game.functions.applyAction(preState, action)); - }); - }); - - describe("isFinal", () => { - it("empty state is final", async () => { - const preState = { - players: [ethers.constants.AddressZero, ethers.constants.AddressZero], - turnNum: 49, - pileHeights: [0, 0, 0] - }; - const ret = await game.functions.isStateTerminal(preState); - expect(ret).to.be.eql(true); - }); - - it("nonempty state is not final", async () => { - const preState = { - players: [ethers.constants.AddressZero, ethers.constants.AddressZero], - turnNum: 49, - pileHeights: [0, 1, 0] - }; - const ret = await game.functions.isStateTerminal(preState); - expect(ret).to.be.eql(false); - }); - }); -}); diff --git a/packages/contracts/test/integration/two-party-payments.spec.ts b/packages/contracts/test/integration/two-party-payments.spec.ts deleted file mode 100644 index 7687aaa9e..000000000 --- a/packages/contracts/test/integration/two-party-payments.spec.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { ethers } from "ethers"; - -import { AbstractContract, expect } from "../../utils"; -import * as Utils from "../../utils/misc"; - -const web3 = (global as any).web3; -const { unlockedAccount } = Utils.setupTestEnv(web3); - -const [A, B] = [ - // 0xb37e49bFC97A948617bF3B63BC6942BB15285715 - new ethers.Wallet( - "0x4ccac8b1e81fb18a98bbaf29b9bfe307885561f71b76bd4680d7aec9d0ddfcfd" - ), - // 0xaeF082d339D227646DB914f0cA9fF02c8544F30b - new ethers.Wallet( - "0x3570f77380e22f8dc2274d8fd33e7830cc2d29cf76804e8c21f4f7a6cc571d27" - ) -]; - -const getUpdateHash = (stateHash: string, nonce: number, timeout: number) => - ethers.utils.solidityKeccak256( - ["bytes1", "address[]", "uint256", "uint256", "bytes32"], - ["0x19", [A.address, B.address], nonce, timeout, stateHash] - ); - -contract("PaymentApp", (accounts: string[]) => { - let pc: ethers.Contract; - let testAppInstance: ethers.Contract; - - enum AssetType { - ETH, - ERC20, - ANY - } - - const exampleState = { - alice: A.address, - bob: B.address, - aliceBalance: Utils.UNIT_ETH, - bobBalance: Utils.UNIT_ETH - }; - - const encode = (encoding: string, state: any) => - ethers.utils.defaultAbiCoder.encode([encoding], [state]); - - const latestNonce = async () => testAppInstance.functions.latestNonce(); - - // TODO: Wait for this to work: - // ethers.utils.formatParamType(iface.functions.resolve.inputs[0]) - // github.com/ethers-io/ethers.js/blob/typescript/src.ts/utils/abi-coder.ts#L301 - const pcEncoding = - "tuple(address alice, address bob, uint256 aliceBalance, uint256 bobBalance)"; - - const appEncoding = - "tuple(address addr, bytes4 applyAction, bytes4 resolve, bytes4 getTurnTaker, bytes4 isStateTerminal)"; - - const termsEncoding = "tuple(uint8 assetType, uint256 limit, address token)"; - - const keccak256 = (bytes: string) => - ethers.utils.solidityKeccak256(["bytes"], [bytes]); - - const sendSignedFinalizationToChain = async (stateHash: string) => - testAppInstance.functions.setState( - stateHash, - await latestNonce(), - 0, - Utils.signMessage( - getUpdateHash( - stateHash || ethers.constants.HashZero, - await latestNonce(), - 0 - ), - unlockedAccount - ) - ); - - let app; - let terms; - beforeEach(async () => { - const paymentApp = await AbstractContract.fromArtifactName("PaymentApp"); - pc = await paymentApp.deploy(unlockedAccount); - - // Specifically for the AppInstance - const appInstance = artifacts.require("AppInstance"); - const staticCall = artifacts.require("StaticCall"); - const signatures = artifacts.require("Signatures"); - const transfer = artifacts.require("Transfer"); - appInstance.link("Signatures", signatures.address); - appInstance.link("StaticCall", staticCall.address); - appInstance.link("Transfer", transfer.address); - - app = { - addr: pc.address, - resolve: pc.interface.functions.resolve.sighash, - applyAction: "0x00000000", - getTurnTaker: "0x00000000", - isStateTerminal: "0x00000000" - }; - - terms = { - assetType: AssetType.ETH, - limit: Utils.UNIT_ETH.mul(2), - token: ethers.constants.AddressZero - }; - - const contractFactory = new ethers.ContractFactory( - appInstance.abi, - appInstance.binary, - unlockedAccount - ); - - testAppInstance = await contractFactory.deploy( - accounts[0], - [A.address, B.address], - keccak256(encode(appEncoding, app)), - keccak256(encode(termsEncoding, terms)), - 10 - ); - }); - - it("should resolve to payments", async () => { - const ret = await pc.functions.resolve(exampleState, terms); - expect(ret.assetType).to.be.eql(AssetType.ETH); - expect(ret.token).to.be.equalIgnoreCase(ethers.constants.AddressZero); - expect(ret.to[0]).to.be.equalIgnoreCase(A.address); - expect(ret.to[1]).to.be.equalIgnoreCase(B.address); - expect(ret.value[0]).to.be.eql(new ethers.utils.BigNumber(Utils.UNIT_ETH)); - expect(ret.value[1]).to.be.eql(new ethers.utils.BigNumber(Utils.UNIT_ETH)); - }); - - describe("setting a resolution", async () => { - it("should fail before state is settled", async () => { - const finalState = encode(pcEncoding, exampleState); - await Utils.assertRejects( - testAppInstance.functions.setResolution( - app, - finalState, - encode(termsEncoding, terms) - ) - ); - }); - it("should succeed after state is settled", async () => { - const finalState = encode(pcEncoding, exampleState); - await sendSignedFinalizationToChain(keccak256(finalState)); - await testAppInstance.functions.setResolution( - app, - finalState, - encode(termsEncoding, terms) - ); - const ret = await testAppInstance.functions.getResolution(); - expect(ret.assetType).to.be.eql(AssetType.ETH); - expect(ret.token).to.be.equalIgnoreCase(ethers.constants.AddressZero); - expect(ret.to[0]).to.be.equalIgnoreCase(A.address); - expect(ret.to[1]).to.be.equalIgnoreCase(B.address); - expect(ret.value[0]).to.be.eql( - new ethers.utils.BigNumber(Utils.UNIT_ETH) - ); - expect(ret.value[1]).to.be.eql( - new ethers.utils.BigNumber(Utils.UNIT_ETH) - ); - }); - }); -}); diff --git a/packages/contracts/test/libCondition.spec.ts b/packages/contracts/test/libCondition.spec.ts new file mode 100644 index 000000000..7ffee6c9f --- /dev/null +++ b/packages/contracts/test/libCondition.spec.ts @@ -0,0 +1,116 @@ +import { Contract, ContractFactory } from "ethers"; +import { HashZero } from "ethers/constants"; +import { JsonRpcSigner, Web3Provider } from "ethers/providers"; +import { defaultAbiCoder, keccak256, solidityKeccak256 } from "ethers/utils"; + +import { expect } from "./utils"; + +const provider = new Web3Provider((global as any).web3.currentProvider); + +contract("LibCondition", (accounts: string[]) => { + let unlockedAccount: JsonRpcSigner; + + let exampleCondition: Contract; + let libCondition: Contract; + + before(async () => { + unlockedAccount = await provider.getSigner(accounts[0]); + + const libConditionArtifact = artifacts.require("LibCondition"); + const exampleConditionArtifact = artifacts.require("ExampleCondition"); + + libConditionArtifact.link(artifacts.require("LibStaticCall")); + + exampleCondition = await new ContractFactory( + exampleConditionArtifact.abi, + exampleConditionArtifact.bytecode, + unlockedAccount + ).deploy({ gasLimit: 6e9 }); + + libCondition = await new ContractFactory( + libConditionArtifact.abi, + libConditionArtifact.binary, + unlockedAccount + ).deploy({ gasLimit: 6e9 }); + + await exampleCondition.deployed(); + await libCondition.deployed(); + }); + + describe("asserts conditions with no params", () => { + const makeCondition = (expectedValue, onlyCheckForSuccess) => ({ + onlyCheckForSuccess, + expectedValueHash: keccak256(expectedValue), + parameters: HashZero, + selector: exampleCondition.interface.functions.isSatisfiedNoParam.sighash, + to: exampleCondition.address + }); + + it("returns true if function did not fail", async () => { + const condition = makeCondition(HashZero, true); + + expect(await libCondition.functions.isSatisfied(condition)).to.be.true; + }); + + it("returns true if function returns expected result", async () => { + const condition = makeCondition( + defaultAbiCoder.encode(["bool"], [true]), + false + ); + + expect(await libCondition.functions.isSatisfied(condition)).to.be.true; + }); + + it("returns false if function returns unexpected result", async () => { + const condition = makeCondition(HashZero, false); + + expect(await libCondition.functions.isSatisfied(condition)).to.be.false; + }); + }); + + describe("asserts conditions with params", () => { + const makeCondition = (expectedValue, parameters, onlyCheckForSuccess) => ({ + onlyCheckForSuccess, + parameters, + expectedValueHash: solidityKeccak256(["bytes"], [expectedValue]), + selector: exampleCondition.interface.functions.isSatisfiedParam.sighash, + to: exampleCondition.address + }); + + const trueParam = defaultAbiCoder.encode(["tuple(bool)"], [[true]]); + + const falseParam = defaultAbiCoder.encode(["tuple(bool)"], [[false]]); + + it("returns true if function did not fail", async () => { + const condition = makeCondition(HashZero, trueParam, true); + + expect(await libCondition.functions.isSatisfied(condition)).to.be.true; + }); + + it("returns true if function did not fail but returned false", async () => { + const condition = makeCondition(HashZero, falseParam, true); + + expect(await libCondition.functions.isSatisfied(condition)).to.be.true; + }); + + it("returns true if function returns expected result", async () => { + const condition = makeCondition( + defaultAbiCoder.encode(["bool"], [true]), + trueParam, + false + ); + + expect(await libCondition.functions.isSatisfied(condition)).to.be.true; + }); + + it("returns false if function returns unexpected result", async () => { + const condition = makeCondition( + defaultAbiCoder.encode(["bool"], [true]), + falseParam, + false + ); + + expect(await libCondition.functions.isSatisfied(condition)).to.be.false; + }); + }); +}); diff --git a/packages/contracts/test/nonce-registry.spec.ts b/packages/contracts/test/nonce-registry.spec.ts new file mode 100644 index 000000000..20bdebcb3 --- /dev/null +++ b/packages/contracts/test/nonce-registry.spec.ts @@ -0,0 +1,76 @@ +import { Contract, ContractFactory } from "ethers"; +import { HashZero, One, Zero } from "ethers/constants"; +import { JsonRpcSigner, Web3Provider } from "ethers/providers"; +import { BigNumber, bigNumberify, solidityKeccak256 } from "ethers/utils"; + +import { expect } from "./utils"; + +const provider = new Web3Provider((global as any).web3.currentProvider); + +contract("NonceRegistry", accounts => { + let unlockedAccount: JsonRpcSigner; + + let nonceRegistry: Contract; + + const computeKey = (timeout: BigNumber, salt: string) => + solidityKeccak256( + ["address", "uint256", "bytes32"], + [accounts[0], timeout, salt] + ); + + before(async () => { + unlockedAccount = await provider.getSigner(accounts[0]); + + const artifact = artifacts.require("NonceRegistry"); + + nonceRegistry = await new ContractFactory( + artifact.abi, + artifact.bytecode, + unlockedAccount + ).deploy({ gasLimit: 6e9 }); + + await nonceRegistry.deployed(); + }); + + it("can set nonces", async () => { + const timeout = bigNumberify(10); + const salt = HashZero; + const value = One; + + await nonceRegistry.functions.setNonce(timeout, salt, value); + + const ret = await nonceRegistry.functions.table(computeKey(timeout, salt)); + const blockNumber = bigNumberify(await provider.getBlockNumber()); + + expect(ret.nonceValue).to.eq(One); + expect(ret.finalizesAt).to.eq(blockNumber.add(timeout)); + }); + + it("fails if nonce increment is not positive", async () => { + const timeout = 10; + const salt = HashZero; + const value = 0; // By default, all values are set to 0 + + const setNonce = nonceRegistry.functions.setNonce; + + // @ts-ignore + await expect(setNonce(timeout, salt, value)).to.be.reverted; + }); + + it("can insta-finalize nonces", async () => { + const timeout = Zero; + const salt = HashZero; + const value = One; + const key = computeKey(timeout, salt); + + await nonceRegistry.functions.setNonce(timeout, salt, value); + + const ret = await nonceRegistry.functions.table(key); + const isFinal = await nonceRegistry.functions.isFinalized(key, value); + const blockNumber = bigNumberify(await provider.getBlockNumber()); + + expect(ret.nonceValue).to.eq(value); + expect(ret.finalizesAt).to.eq(blockNumber); + expect(isFinal).to.be.true; + }); +}); diff --git a/packages/contracts/test/staticCall.spec.ts b/packages/contracts/test/staticCall.spec.ts new file mode 100644 index 000000000..ab1109b4e --- /dev/null +++ b/packages/contracts/test/staticCall.spec.ts @@ -0,0 +1,104 @@ +import { Contract, ContractFactory } from "ethers"; +import { JsonRpcSigner, Web3Provider } from "ethers/providers"; +import { + defaultAbiCoder, + hexlify, + randomBytes, + toUtf8Bytes +} from "ethers/utils"; + +import { expect } from "./utils"; + +const provider = new Web3Provider((global as any).web3.currentProvider); + +contract("StaticCall", (accounts: string[]) => { + let unlockedAccount: JsonRpcSigner; + let testCaller: Contract; + let echo: Contract; + + before(async () => { + unlockedAccount = await provider.getSigner(accounts[0]); + + const testCallerArtifact = artifacts.require("TestCaller"); + testCallerArtifact.link(artifacts.require("LibStaticCall")); + testCaller = await new ContractFactory( + testCallerArtifact.abi, + testCallerArtifact.binary, + unlockedAccount + ).deploy({ gasLimit: 6e9 }); + + const echoArtifact = artifacts.require("Echo"); + echo = await new ContractFactory( + echoArtifact.abi, + echoArtifact.binary, + unlockedAccount + ).deploy({ gasLimit: 6e9 }); + + await testCaller.deployed(); + await echo.deployed(); + }); + + describe("execStaticCall", () => { + const helloWorldString = hexlify(toUtf8Bytes("hello world")); + + it("retrieves bytes string from external pure function", async () => { + const ret = await testCaller.functions.execStaticCall( + echo.address, + echo.interface.functions.helloWorld.sighash, + "0x" + ); + + expect(ret).to.eq(helloWorldString); + }); + + it("retrieves true bool from external pure function", async () => { + const ret = await testCaller.functions.execStaticCallBool( + echo.address, + echo.interface.functions.returnArg.sighash, + defaultAbiCoder.encode(["bool"], [true]) + ); + expect(ret).to.be.true; + }); + + it("retrieves false bool from external pure function", async () => { + const ret = await testCaller.functions.execStaticCallBool( + echo.address, + echo.interface.functions.returnArg.sighash, + defaultAbiCoder.encode(["bool"], [false]) + ); + expect(ret).to.be.false; + }); + + it("retrieves argument from external pure function", async () => { + const ret = await testCaller.functions.execStaticCall( + echo.address, + echo.interface.functions.helloWorldArg.sighash, + defaultAbiCoder.encode(["string"], ["hello world"]) + ); + + expect(ret).to.eq(helloWorldString); + }); + + it("fails to read msg.sender", async () => { + await expect( + testCaller.functions.execStaticCall( + echo.address, + echo.interface.functions.msgSender.sighash, + "0x" + ) + // @ts-ignore + ).to.be.reverted; + }); + + it("reverts if the target is not a contract", async () => { + await expect( + testCaller.functions.execStaticCall( + hexlify(randomBytes(20)), + echo.interface.functions.helloWorld.sighash, + "0x" + ) + // @ts-ignore + ).to.be.reverted; + }); + }); +}); diff --git a/packages/contracts/test/transfer.spec.ts b/packages/contracts/test/transfer.spec.ts new file mode 100644 index 000000000..419a7d0fc --- /dev/null +++ b/packages/contracts/test/transfer.spec.ts @@ -0,0 +1,164 @@ +import { Contract, ContractFactory } from "ethers"; +import { AddressZero, WeiPerEther } from "ethers/constants"; +import { JsonRpcSigner, Web3Provider } from "ethers/providers"; +import { hexlify, randomBytes } from "ethers/utils"; + +import { expect } from "./utils"; + +const provider = new Web3Provider((global as any).web3.currentProvider); + +// It's necessary to pass in this argument since the DelegateProxy +// uses delegatecall which ethers can't estimate gas for. +const APPROXIMATE_ERC20_TRANSFER_GAS = 75000; +const APPROXIMATE_ERC20_TRANSFER_10_GAS = 425000; + +contract("Transfer", (accounts: string[]) => { + let unlockedAccount: JsonRpcSigner; + + let exampleTransfer: Contract; + let delegateProxy: Contract; + let dolphinCoin: Contract; + + enum AssetType { + ETH, + ERC20, + ANY + } + + before(async () => { + unlockedAccount = await provider.getSigner(accounts[0]); + + const artifact = await artifacts.require("ExampleTransfer"); + artifact.link(artifacts.require("Transfer")); + exampleTransfer = await new ContractFactory( + artifact.abi, + artifact.binary, + unlockedAccount + ).deploy({ gasLimit: 6e9 }); + + delegateProxy = await new ContractFactory( + artifacts.require("DelegateProxy").abi, + artifacts.require("DelegateProxy").bytecode, + unlockedAccount + ).deploy({ gasLimit: 6e9 }); + + dolphinCoin = await new ContractFactory( + artifacts.require("DolphinCoin").abi, + artifacts.require("DolphinCoin").bytecode, + unlockedAccount + ).deploy({ gasLimit: 6e9 }); + + await exampleTransfer.deployed(); + await delegateProxy.deployed(); + await dolphinCoin.deployed(); + }); + + describe("Executes delegated transfers for ETH", () => { + beforeEach(async () => { + await unlockedAccount.sendTransaction({ + to: delegateProxy.address, + value: WeiPerEther + }); + }); + + it("for 1 address", async () => { + const randomTarget = hexlify(randomBytes(20)); + + const details = { + value: [WeiPerEther], + assetType: AssetType.ETH, + to: [randomTarget], + token: AddressZero, + data: [] + }; + + await delegateProxy.functions.delegate( + exampleTransfer.address, + exampleTransfer.interface.functions.transfer.encode([details]), + { gasLimit: APPROXIMATE_ERC20_TRANSFER_GAS } + ); + + const balTarget = await provider.getBalance(randomTarget); + + expect(balTarget).to.eq(WeiPerEther); + }); + + it("for many addresses", async () => { + const randomTargets = Array.from({ length: 10 }, () => + hexlify(randomBytes(20)) + ); + + const details = { + value: Array(10).fill(WeiPerEther.div(10)), + assetType: AssetType.ETH, + to: randomTargets, + token: AddressZero, + data: [] + }; + + await delegateProxy.functions.delegate( + exampleTransfer.address, + exampleTransfer.interface.functions.transfer.encode([details]), + { gasLimit: APPROXIMATE_ERC20_TRANSFER_10_GAS } + ); + + for (const target of randomTargets) { + const bal = await provider.getBalance(target); + expect(bal).to.eq(WeiPerEther.div(10)); + } + }); + }); + + describe("Executes delegated transfers for ERC20", () => { + beforeEach(async () => { + await dolphinCoin.functions.transfer(delegateProxy.address, 10); + }); + + it("for 1 address", async () => { + const randomTarget = hexlify(randomBytes(20)); + + const details = { + value: [10], + assetType: AssetType.ERC20, + to: [randomTarget], + token: dolphinCoin.address, + data: [] + }; + + await delegateProxy.functions.delegate( + exampleTransfer.address, + exampleTransfer.interface.functions.transfer.encode([details]), + { gasLimit: APPROXIMATE_ERC20_TRANSFER_GAS } + ); + + const balTarget = await dolphinCoin.functions.balanceOf(randomTarget); + + expect(balTarget).to.eq(10); + }); + + it("for many addresses", async () => { + const randomTargets = Array.from({ length: 10 }, () => + hexlify(randomBytes(20)) + ); + + const details = { + value: Array(10).fill(1), + assetType: AssetType.ERC20, + to: randomTargets, + token: dolphinCoin.address, + data: [] + }; + + await delegateProxy.functions.delegate( + exampleTransfer.address, + exampleTransfer.interface.functions.transfer.encode([details]), + { gasLimit: APPROXIMATE_ERC20_TRANSFER_10_GAS } + ); + + for (const target of randomTargets) { + const bal = await dolphinCoin.functions.balanceOf(target); + expect(bal).to.eq(1); + } + }); + }); +}); diff --git a/packages/contracts/test/unit/appInstance.spec.ts b/packages/contracts/test/unit/appInstance.spec.ts deleted file mode 100644 index 6ce612fd2..000000000 --- a/packages/contracts/test/unit/appInstance.spec.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { ethers } from "ethers"; - -import { AbstractContract, buildArtifacts, expect } from "../../utils"; -import * as Utils from "../../utils/misc"; - -const web3 = (global as any).web3; -const { unlockedAccount } = Utils.setupTestEnv(web3); - -// HELPER DATA -enum Status { - ON, - DISPUTE, - OFF -} -const TIMEOUT = 30; -const [A, B] = [ - // 0xb37e49bFC97A948617bF3B63BC6942BB15285715 - new ethers.Wallet( - "0x4ccac8b1e81fb18a98bbaf29b9bfe307885561f71b76bd4680d7aec9d0ddfcfd" - ), - // 0xaeF082d339D227646DB914f0cA9fF02c8544F30b - new ethers.Wallet( - "0x3570f77380e22f8dc2274d8fd33e7830cc2d29cf76804e8c21f4f7a6cc571d27" - ) -]; - -// HELPER FUNCTION -const computeHash = (stateHash: string, nonce: number, timeout: number) => - ethers.utils.solidityKeccak256( - ["bytes1", "address[]", "uint256", "uint256", "bytes32"], - ["0x19", [A.address, B.address], nonce, timeout, stateHash] - ); - -contract("AppInstance", (accounts: string[]) => { - let appInstance: AbstractContract; - let stateChannel: ethers.Contract; - let networkID; - - let sendUpdateToChainWithNonce: ( - nonce: number, - appState?: string - ) => Promise; - - let sendSignedUpdateToChainWithNonce: ( - nonce: number, - appState?: string - ) => Promise; - - let sendSignedFinalizationToChain: () => Promise; - - const latestState = async () => { - return (await stateChannel.functions.state()).appStateHash; - }; - const latestNonce = async () => stateChannel.functions.latestNonce(); - - // @ts-ignore - before(async () => { - networkID = await AbstractContract.getNetworkID(unlockedAccount); - appInstance = await buildArtifacts.AppInstance; - - sendUpdateToChainWithNonce = (nonce: number, appState?: string) => - stateChannel.functions.setState( - appState || ethers.constants.HashZero, - nonce, - TIMEOUT, - "0x" - ); - - sendSignedUpdateToChainWithNonce = (nonce: number, appState?: string) => - stateChannel.functions.setState( - appState || ethers.constants.HashZero, - nonce, - TIMEOUT, - Utils.signMessage( - computeHash(appState || ethers.constants.HashZero, nonce, TIMEOUT), - unlockedAccount - ) - ); - - sendSignedFinalizationToChain = async () => - stateChannel.functions.setState( - await latestState(), - await latestNonce(), - 0, - Utils.signMessage( - computeHash(await latestState(), await latestNonce(), 0), - unlockedAccount - ) - ); - }); - - beforeEach(async () => { - const contractFactory = new ethers.ContractFactory( - appInstance.abi, - await appInstance.generateLinkedBytecode(networkID), - unlockedAccount - ); - stateChannel = await contractFactory.deploy( - accounts[0], - [A.address, B.address], - ethers.constants.HashZero, - ethers.constants.HashZero, - 10 - ); - }); - - it("constructor sets initial state", async () => { - const owner = await stateChannel.functions.getOwner(); - const signingKeys = await stateChannel.functions.getSigningKeys(); - expect(owner).to.equalIgnoreCase(accounts[0]); - expect(signingKeys[0]).to.equalIgnoreCase(A.address); - expect(signingKeys[1]).to.equalIgnoreCase(B.address); - }); - - it("should start without a dispute if deployed", async () => { - const state = await stateChannel.functions.state(); - expect(state.status).to.eql(Status.ON); - }); - - describe("updating app state", async () => { - describe("with owner", async () => { - it("should work with higher nonce", async () => { - expect(await latestNonce()).to.be.eql(new ethers.utils.BigNumber(0)); - await sendUpdateToChainWithNonce(1); - expect(await latestNonce()).to.be.eql(new ethers.utils.BigNumber(1)); - }); - - it("should work many times", async () => { - expect(await latestNonce()).to.be.eql(new ethers.utils.BigNumber(0)); - await sendUpdateToChainWithNonce(1); - expect(await latestNonce()).to.be.eql(new ethers.utils.BigNumber(1)); - await sendUpdateToChainWithNonce(2); - expect(await latestNonce()).to.be.eql(new ethers.utils.BigNumber(2)); - await sendUpdateToChainWithNonce(3); - expect(await latestNonce()).to.be.eql(new ethers.utils.BigNumber(3)); - }); - - it("should work with much higher nonce", async () => { - expect(await latestNonce()).to.be.eql(new ethers.utils.BigNumber(0)); - await sendUpdateToChainWithNonce(1000); - expect(await latestNonce()).to.be.eql(new ethers.utils.BigNumber(1000)); - }); - - it("shouldn't work with an equal nonce", async () => { - await Utils.assertRejects(sendUpdateToChainWithNonce(0)); - expect(await latestNonce()).to.be.eql(new ethers.utils.BigNumber(0)); - }); - - it("shouldn't work with an lower nonce", async () => { - await sendUpdateToChainWithNonce(1); - await Utils.assertRejects(sendUpdateToChainWithNonce(0)); - expect(await latestNonce()).to.be.eql(new ethers.utils.BigNumber(1)); - }); - }); - - describe("with signing keys", async () => { - it("should work with higher nonce", async () => { - expect(await latestNonce()).be.eql(new ethers.utils.BigNumber(0)); - await sendSignedUpdateToChainWithNonce(1); - expect(await latestNonce()).to.be.eql(new ethers.utils.BigNumber(1)); - }); - - it("should work many times", async () => { - expect(await latestNonce()).to.be.eql(new ethers.utils.BigNumber(0)); - await sendSignedUpdateToChainWithNonce(1); - expect(await latestNonce()).to.be.eql(new ethers.utils.BigNumber(1)); - await sendSignedUpdateToChainWithNonce(2); - expect(await latestNonce()).to.be.eql(new ethers.utils.BigNumber(2)); - await sendSignedUpdateToChainWithNonce(3); - expect(await latestNonce()).to.be.eql(new ethers.utils.BigNumber(3)); - }); - - it("should work with much higher nonce", async () => { - expect(await latestNonce()).to.be.eql(new ethers.utils.BigNumber(0)); - await sendSignedUpdateToChainWithNonce(1000); - expect(await latestNonce()).to.be.eql(new ethers.utils.BigNumber(1000)); - }); - - it("shouldn't work with an equal nonce", async () => { - await Utils.assertRejects(sendSignedUpdateToChainWithNonce(0)); - expect(await latestNonce()).to.be.eql(new ethers.utils.BigNumber(0)); - }); - - it("shouldn't work with an lower nonce", async () => { - await sendSignedUpdateToChainWithNonce(1); - await Utils.assertRejects(sendSignedUpdateToChainWithNonce(0)); - expect(await latestNonce()).to.be.eql(new ethers.utils.BigNumber(1)); - }); - }); - }); - - describe("finalizing app state", async () => { - it("should work with owner", async () => { - expect(await stateChannel.functions.isClosed()).to.eql(false); - await stateChannel.functions.setState( - await latestState(), - await latestNonce(), - 0, - ethers.constants.HashZero - ); - expect(await stateChannel.functions.isClosed()).to.eql(true); - }); - - it("should work with keys", async () => { - expect(await stateChannel.functions.isClosed()).to.eql(false); - await sendSignedFinalizationToChain(); - expect(await stateChannel.functions.isClosed()).to.eql(true); - }); - }); - - describe("waiting for timeout", async () => { - it("should block updates after the timeout", async () => { - expect(await stateChannel.functions.isClosed()).to.eql(false); - await sendUpdateToChainWithNonce(1); - await Utils.mineBlocks(TIMEOUT); - expect(await stateChannel.functions.isClosed()).to.eql(true); - await Utils.assertRejects(sendUpdateToChainWithNonce(2)); - await Utils.assertRejects(sendSignedUpdateToChainWithNonce(0)); - }); - }); -}); diff --git a/packages/contracts/test/unit/conditional.spec.ts b/packages/contracts/test/unit/conditional.spec.ts deleted file mode 100644 index 0b2f3e09b..000000000 --- a/packages/contracts/test/unit/conditional.spec.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { ethers } from "ethers"; - -import { AbstractContract, expect } from "../../utils"; -import * as Utils from "../../utils/misc"; - -const web3 = (global as any).web3; -const { unlockedAccount } = Utils.setupTestEnv(web3); - -contract("Conditional", (accounts: string[]) => { - let example: ethers.Contract; - let conditionContract: ethers.Contract; - - // @ts-ignore - before(async () => { - const staticCall = AbstractContract.fromArtifactName("StaticCall"); - const conditional = await AbstractContract.fromArtifactName("Conditional", { - StaticCall: staticCall - }); - const exampleCondition = await AbstractContract.fromArtifactName( - "ExampleCondition" - ); - example = await exampleCondition.deploy(unlockedAccount); - conditionContract = await conditional.deploy(unlockedAccount); - }); - - describe("asserts conditions with no params", () => { - const makeCondition = (expectedValue, onlyCheckForSuccess) => ({ - onlyCheckForSuccess, - expectedValueHash: ethers.utils.solidityKeccak256( - ["bytes"], - [expectedValue] - ), - parameters: ethers.constants.HashZero, - selector: example.interface.functions.isSatisfiedNoParam.sighash, - to: example.address - }); - - it("returns true if function did not fail", async () => { - const condition = makeCondition(ethers.constants.HashZero, true); - const ret = await conditionContract.functions.isSatisfied(condition); - expect(ret).to.be.eql(true); - }); - - it("returns true if function returns expected result", async () => { - const condition = makeCondition( - ethers.utils.defaultAbiCoder.encode(["bool"], [true]), - false - ); - const ret = await conditionContract.functions.isSatisfied(condition); - expect(ret).to.be.eql(true); - }); - - it("returns false if function returns unexpected result", async () => { - const condition = makeCondition(ethers.constants.HashZero, false); - const ret = await conditionContract.functions.isSatisfied(condition); - expect(ret).to.be.eql(false); - }); - }); - - describe("asserts conditions with params", () => { - const makeCondition = (expectedValue, parameters, onlyCheckForSuccess) => ({ - onlyCheckForSuccess, - parameters, - expectedValueHash: ethers.utils.solidityKeccak256( - ["bytes"], - [expectedValue] - ), - selector: example.interface.functions.isSatisfiedNoParam.sighash, - to: example.address - }); - - const trueParam = ethers.utils.defaultAbiCoder.encode( - ["tuple(bool)"], - [[true]] - ); - - const falseParam = ethers.utils.defaultAbiCoder.encode( - ["tuple(bool)"], - [[false]] - ); - - it("returns true if function did not fail", async () => { - const condition = makeCondition( - ethers.constants.HashZero, - trueParam, - true - ); - const ret = await conditionContract.functions.isSatisfied(condition); - expect(ret).to.be.eql(true); - }); - - it("returns true if function did not fail but returned false", async () => { - const condition = makeCondition( - ethers.constants.HashZero, - falseParam, - true - ); - const ret = await conditionContract.functions.isSatisfied(condition); - expect(ret).to.be.eql(true); - }); - - it("returns true if function returns expected result", async () => { - const condition = makeCondition( - ethers.utils.defaultAbiCoder.encode(["bool"], [true]), - trueParam, - false - ); - const ret = await conditionContract.functions.isSatisfied(condition); - expect(ret).to.be.eql(true); - }); - - it("returns false if function returns unexpected result", async () => { - const condition = makeCondition( - ethers.constants.HashZero, - falseParam, - false - ); - const ret = await conditionContract.functions.isSatisfied(condition); - expect(ret).to.be.eql(false); - }); - }); -}); diff --git a/packages/contracts/test/unit/conditionalTransaction.spec.ts b/packages/contracts/test/unit/conditionalTransaction.spec.ts deleted file mode 100644 index 220078ba2..000000000 --- a/packages/contracts/test/unit/conditionalTransaction.spec.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { ethers } from "ethers"; - -import { AbstractContract, expect } from "../../utils"; -import * as Utils from "../../utils/misc"; - -const web3 = (global as any).web3; -const { provider, unlockedAccount } = Utils.setupTestEnv(web3); - -contract("ConditionalTransaction", (accounts: string[]) => { - let testCondition: ethers.Contract; - let testDelegateProxy: ethers.Contract; - let ct: ethers.Contract; - - // @ts-ignore - before(async () => { - const exampleCondition = await AbstractContract.fromArtifactName( - "ExampleCondition" - ); - const delegateProxy = await AbstractContract.fromArtifactName( - "DelegateProxy" - ); - const conditionalTransaction = await AbstractContract.fromArtifactName( - "ConditionalTransaction" - ); - - testCondition = await exampleCondition.deploy(unlockedAccount); - testDelegateProxy = await delegateProxy.deploy(unlockedAccount); - ct = await conditionalTransaction.getDeployed(unlockedAccount); - }); - - describe("Pre-commit to transfer details", () => { - const makeCondition = (expectedValue, onlyCheckForSuccess) => ({ - onlyCheckForSuccess, - expectedValueHash: ethers.utils.solidityKeccak256( - ["bytes"], - [expectedValue] - ), - parameters: ethers.constants.HashZero, - selector: testCondition.interface.functions.isSatisfiedNoParam.sighash, - to: testCondition.address - }); - - const makeConditionParam = (expectedValue, parameters) => ({ - parameters, - expectedValueHash: ethers.utils.solidityKeccak256( - ["bytes"], - [expectedValue] - ), - onlyCheckForSuccess: false, - selector: testCondition.interface.functions.isSatisfiedParam.sighash, - to: testCondition.address - }); - - const trueParam = ethers.utils.defaultAbiCoder.encode( - ["tuple(bool)"], - [[true]] - ); - - const falseParam = ethers.utils.defaultAbiCoder.encode( - ["tuple(bool)"], - [[false]] - ); - - beforeEach(async () => { - await unlockedAccount.sendTransaction({ - to: testDelegateProxy.address, - value: Utils.UNIT_ETH - }); - }); - - it("transfers the funds conditionally if true", async () => { - const randomTarget = Utils.randomETHAddress(); - const tx = ct.interface.functions.executeSimpleConditionalTransaction.encode( - [ - makeCondition(ethers.constants.HashZero, true), - { - value: [Utils.UNIT_ETH], - assetType: 0, - to: [randomTarget], - token: ethers.constants.AddressZero, - data: [] - } - ] - ); - - await testDelegateProxy.functions.delegate( - ct.address, - tx, - Utils.HIGH_GAS_LIMIT - ); - - const balTarget = await provider.getBalance(randomTarget); - expect(balTarget.toHexString()).to.be.eql( - ethers.utils.hexStripZeros(Utils.UNIT_ETH.toHexString()) - ); - - const emptyBalance = new ethers.utils.BigNumber(0); - const balDelegate = await provider.getBalance(testDelegateProxy.address); - expect(balDelegate.toHexString()).to.be.eql( - ethers.utils.hexStripZeros(emptyBalance.toHexString()) - ); - }); - - it("does not transfer the funds conditionally if false", async () => { - const randomTarget = Utils.randomETHAddress(); - const tx = ct.interface.functions.executeSimpleConditionalTransaction.encode( - [ - makeConditionParam(trueParam, falseParam), - { - value: [Utils.UNIT_ETH], - assetType: 0, - to: [randomTarget], - token: ethers.constants.AddressZero, - data: [] - } - ] - ); - - await Utils.assertRejects( - testDelegateProxy.functions.delegate(ct.address, tx) - ); - - const emptyBalance = new ethers.utils.BigNumber(0); - const balTarget = await provider.getBalance(randomTarget); - expect(balTarget.toHexString()).to.be.eql( - ethers.utils.hexStripZeros(emptyBalance.toHexString()) - ); - - const balDelegate = await provider.getBalance(testDelegateProxy.address); - expect(balDelegate.toHexString()).to.be.eql( - ethers.utils.hexStripZeros(Utils.UNIT_ETH.toHexString()) - ); - }); - }); -}); diff --git a/packages/contracts/test/unit/nonceRegistry.spec.ts b/packages/contracts/test/unit/nonceRegistry.spec.ts deleted file mode 100644 index abfa20a65..000000000 --- a/packages/contracts/test/unit/nonceRegistry.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { ethers } from "ethers"; - -import { AbstractContract, expect } from "../../utils"; -import * as Utils from "../../utils/misc"; - -const web3 = (global as any).web3; -const { provider, unlockedAccount } = Utils.setupTestEnv(web3); - -contract("NonceRegistry", accounts => { - let registry: ethers.Contract; - - const computeKey = (timeout: ethers.utils.BigNumber, salt: string) => - ethers.utils.solidityKeccak256( - ["address", "uint256", "bytes32"], - [accounts[0], timeout, salt] - ); - - // @ts-ignore - before(async () => { - const nonceRegistry = await AbstractContract.fromArtifactName( - "NonceRegistry" - ); - registry = await nonceRegistry.deploy(unlockedAccount); - }); - - it("can set nonces", async () => { - const timeout = new ethers.utils.BigNumber(10); - await registry.functions.setNonce(timeout, ethers.constants.HashZero, 1); - const ret = await registry.functions.table( - computeKey(timeout, ethers.constants.HashZero) - ); - expect(ret.nonceValue).to.be.eql(new ethers.utils.BigNumber(1)); - expect(ret.finalizesAt).to.be.eql( - new ethers.utils.BigNumber((await provider.getBlockNumber()) + 10) - ); - }); - - it("fails if nonce increment is not positive", async () => { - await Utils.assertRejects( - registry.functions.setNonce( - new ethers.utils.BigNumber(10), - ethers.constants.HashZero, - 0 - ) - ); - }); - - it("can insta-finalize nonces", async () => { - const timeout = new ethers.utils.BigNumber(0); - const nonceValue = new ethers.utils.BigNumber(1); - const nonceKey = computeKey(timeout, ethers.constants.HashZero); - - await registry.functions.setNonce( - timeout, - ethers.constants.HashZero, - nonceValue - ); - const ret = await registry.functions.table(nonceKey); - expect(ret.nonceValue).to.be.eql(new ethers.utils.BigNumber(nonceValue)); - expect(ret.finalizesAt).to.be.eql( - new ethers.utils.BigNumber(await provider.getBlockNumber()) - ); - const isFinal = await registry.functions.isFinalized( - computeKey(timeout, ethers.constants.HashZero), - nonceValue - ); - expect(isFinal).to.be.eql(true); - }); -}); diff --git a/packages/contracts/test/unit/registry.spec.ts b/packages/contracts/test/unit/registry.spec.ts deleted file mode 100644 index ba2692b1f..000000000 --- a/packages/contracts/test/unit/registry.spec.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { ethers } from "ethers"; -import * as solc from "solc"; - -import { AbstractContract, expect } from "../../utils"; -import * as Utils from "../../utils/misc"; - -contract("Registry", accounts => { - const web3 = (global as any).web3; - const { unlockedAccount } = Utils.setupTestEnv(web3); - - let testRegistry: ethers.Contract; - let simpleContract: ethers.Contract; - let proxyContract: AbstractContract; - - function cfaddress(initcode, i) { - return ethers.utils.solidityKeccak256( - ["bytes1", "bytes", "uint256"], - ["0x19", initcode, i] - ); - } - const simpleContractSource = ` - contract Test { - function sayHello() public pure returns (string) { - return "hi"; - } - }`; - - // @ts-ignore - beforeEach(async () => { - proxyContract = await AbstractContract.fromArtifactName("Proxy"); - const registry = await AbstractContract.fromArtifactName("Registry"); - - testRegistry = await registry.deploy(unlockedAccount); - }); - - it("computes counterfactual addresses of bytes deployments", async () => { - expect(cfaddress(ethers.constants.HashZero, 1)).to.eql( - await testRegistry.cfaddress(ethers.constants.HashZero, 1) - ); - }); - - it("deploys a contract", done => { - const output = (solc as any).compile(simpleContractSource, 0); - const iface = JSON.parse(output.contracts[":Test"].interface); - const bytecode = `0x${output.contracts[":Test"].bytecode}`; - - const filter = testRegistry.filters.ContractCreated(null, null); - const callback = async (from, to, value, event) => { - const deployedAddress = value.args.deployedAddress; - expect(deployedAddress).to.eql( - await testRegistry.resolver(cfaddress(bytecode, 2)) - ); - simpleContract = new ethers.Contract( - deployedAddress, - iface, - unlockedAccount - ); - expect(await simpleContract.sayHello()).to.eql("hi"); - done(); - }; - const registryContract = testRegistry.on(filter, callback); - registryContract.deploy(bytecode, 2); - }); - - it("deploys a contract using msg.sender", done => { - const output = (solc as any).compile(simpleContractSource, 0); - const iface = JSON.parse(output.contracts[":Test"].interface); - const bytecode = `0x${output.contracts[":Test"].bytecode}`; - - const filter = testRegistry.filters.ContractCreated(null, null); - const callback = async (from, to, value, event) => { - const deployedAddress = value.args.deployedAddress; - expect(deployedAddress).to.eql( - await testRegistry.resolver(cfaddress(bytecode, 3)) - ); - - simpleContract = new ethers.Contract( - deployedAddress, - iface, - unlockedAccount - ); - expect(await simpleContract.sayHello()).to.eql("hi"); - done(); - }; - const registryContract = testRegistry.on(filter, callback); - registryContract.deploy(bytecode, 3); - }); - - it("deploys a ProxyContract contract through as owner", done => { - const output = (solc as any).compile(simpleContractSource, 0); - const iface = JSON.parse(output.contracts[":Test"].interface); - const initcode = - proxyContract.bytecode + - ethers.utils.defaultAbiCoder - .encode(["address"], [simpleContract.address]) - .substr(2); - - const filter = testRegistry.filters.ContractCreated(null, null); - const callback = async (from, to, value, event) => { - const deployedAddress = value.args.deployedAddress; - expect(deployedAddress).to.eql( - await testRegistry.resolver(cfaddress(initcode, 3)) - ); - - const contract = new ethers.Contract( - deployedAddress, - iface, - unlockedAccount - ); - expect(await contract.sayHello()).to.eql("hi"); - done(); - }; - - const registryContract = testRegistry.on(filter, callback); - registryContract.deploy(initcode, 3); - }); - - it("deploys a contract and passes arguments", done => { - const source = ` - contract Test { - address whatToSay; - function Test(address _whatToSay) public { - whatToSay = _whatToSay; - } - function sayHello() public view returns (address) { - return whatToSay; - } - }`; - const output = (solc as any).compile(source, 0); - const iface = JSON.parse(output.contracts[":Test"].interface); - const bytecode = `0x${output.contracts[":Test"].bytecode}`; - - const initcode = - bytecode + - ethers.utils.defaultAbiCoder.encode(["address"], [accounts[0]]).substr(2); - - const filter = testRegistry.filters.ContractCreated(null, null); - const callback = async (from, to, value, event) => { - const deployedAddress = value.args.deployedAddress; - expect(deployedAddress).to.eql( - await testRegistry.resolver(cfaddress(initcode, 4)) - ); - - const contract = new ethers.Contract( - deployedAddress, - iface, - unlockedAccount - ); - expect(await contract.sayHello()).to.equalIgnoreCase(accounts[0]); - done(); - }; - - const registryContract = testRegistry.on(filter, callback); - registryContract.deploy(initcode, 4); - }); -}); diff --git a/packages/contracts/test/unit/staticCall.spec.ts b/packages/contracts/test/unit/staticCall.spec.ts deleted file mode 100644 index f14349e22..000000000 --- a/packages/contracts/test/unit/staticCall.spec.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { ethers } from "ethers"; - -import { AbstractContract, expect } from "../../utils"; -import * as Utils from "../../utils/misc"; - -const web3 = (global as any).web3; -const { unlockedAccount } = Utils.setupTestEnv(web3); - -contract("StaticCall", (accounts: string[]) => { - let testCaller: ethers.Contract; - let echo: ethers.Contract; - - // @ts-ignore - before(async () => { - const staticCall = AbstractContract.fromArtifactName("StaticCall"); - const testCallerArtifact = await AbstractContract.fromArtifactName( - "TestCaller", - { - StaticCall: staticCall - } - ); - const echoArtifact = await AbstractContract.fromArtifactName("Echo"); - testCaller = await testCallerArtifact.deploy(unlockedAccount); - echo = await echoArtifact.deploy(unlockedAccount); - }); - - describe("execStaticCall", () => { - it("retrieves bytes string from external pure function", async () => { - const helloWorldString = ethers.utils.hexlify( - ethers.utils.toUtf8Bytes("hello world") - ); - - const ret = await testCaller.functions.execStaticCall( - echo.address, - echo.interface.functions.helloWorld.sighash, - "0x" - ); - - expect(ret).to.eql(helloWorldString); - }); - - it("retrieves true bool from external pure function", async () => { - const ret = await testCaller.functions.execStaticCallBool( - echo.address, - echo.interface.functions.returnArg.sighash, - ethers.utils.defaultAbiCoder.encode(["bool"], [true]) - ); - expect(ret).to.eql(true); - }); - - it("retrieves false bool from external pure function", async () => { - const ret = await testCaller.functions.execStaticCallBool( - echo.address, - echo.interface.functions.returnArg.sighash, - ethers.utils.defaultAbiCoder.encode(["bool"], [false]) - ); - expect(ret).to.eql(false); - }); - - it("retrieves argument from external pure function", async () => { - const helloWorldString = ethers.utils.defaultAbiCoder.encode( - ["string"], - ["hello world"] - ); - - const ret = await testCaller.functions.execStaticCall( - echo.address, - echo.interface.functions.helloWorldArg.sighash, - helloWorldString - ); - - expect(ret).to.eql( - ethers.utils.hexlify(ethers.utils.toUtf8Bytes("hello world")) - ); - }); - - it("fails to read msg.sender", async () => { - await Utils.assertRejects( - testCaller.functions.execStaticCall( - echo.address, - echo.interface.functions.msgSender.sighash, - "0x" - ) - ); - }); - - it("reverts if the target is not a contract", async () => { - await Utils.assertRejects( - testCaller.functions.execStaticCall( - ethers.utils.hexlify(ethers.utils.randomBytes(20)), - echo.interface.functions.helloWorld.sighash, - "0x" - ) - ); - }); - }); -}); diff --git a/packages/contracts/test/unit/transfer.spec.ts b/packages/contracts/test/unit/transfer.spec.ts deleted file mode 100644 index d37d3ce2a..000000000 --- a/packages/contracts/test/unit/transfer.spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "ethers"; - -import { AbstractContract } from "../../utils/contract"; -import * as Utils from "../../utils/misc"; - -const web3 = (global as any).web3; -const { provider, unlockedAccount } = Utils.setupTestEnv(web3); - -contract("Transfer", (accounts: string[]) => { - let transfer: ethers.Contract; - let delegateProxy: ethers.Contract; - let dolphinCoin: ethers.Contract; - - enum AssetType { - ETH, - ERC20, - ANY - } - - // @ts-ignore - before(async () => { - const transferArtifact = AbstractContract.fromArtifactName("Transfer"); - const exampleTransfer = await AbstractContract.fromArtifactName( - "ExampleTransfer", - { - Transfer: transferArtifact - } - ); - const delegateProxyArtifact = await AbstractContract.fromArtifactName( - "DelegateProxy" - ); - const dolphinCoinArtifact = await AbstractContract.fromArtifactName( - "DolphinCoin" - ); - transfer = await exampleTransfer.deploy(unlockedAccount); - delegateProxy = await delegateProxyArtifact.deploy(unlockedAccount); - dolphinCoin = await dolphinCoinArtifact.deploy(unlockedAccount); - }); - - describe("Executes delegated transfers for ETH", () => { - beforeEach(async () => { - await unlockedAccount.sendTransaction({ - to: delegateProxy.address, - value: Utils.UNIT_ETH - }); - }); - - it("for 1 address", async () => { - const randomTarget = Utils.randomETHAddress(); - - const details = { - value: [Utils.UNIT_ETH], - assetType: AssetType.ETH, - to: [randomTarget], - token: ethers.constants.AddressZero, - data: [] - }; - - await delegateProxy.functions.delegate( - transfer.address, - transfer.interface.functions.transfer.encode([details]), - Utils.HIGH_GAS_LIMIT - ); - - const balTarget = await provider.getBalance(randomTarget); - - expect(balTarget.toString()).to.deep.equal(Utils.UNIT_ETH.toString()); - }); - - it("for many addresses", async () => { - const randomTargets: string[] = Array.from({ length: 10 }, () => - Utils.randomETHAddress() - ); - - const details = { - value: randomTargets.map(_ => Utils.UNIT_ETH.div(10)), - assetType: AssetType.ETH, - to: randomTargets, - token: ethers.constants.AddressZero, - data: [] - }; - - await delegateProxy.functions.delegate( - transfer.address, - transfer.interface.functions.transfer.encode([details]), - Utils.HIGH_GAS_LIMIT - ); - - for (const target of randomTargets) { - const bal = await provider.getBalance(target); - expect(bal.toString()).to.deep.equal(Utils.UNIT_ETH.div(10).toString()); - } - }); - }); - - describe("Executes delegated transfers for ERC20", () => { - beforeEach(async () => { - await dolphinCoin.functions.transfer(delegateProxy.address, 10); - }); - - it("for 1 address", async () => { - const randomTarget = Utils.randomETHAddress(); - - const details = { - value: [10], - assetType: AssetType.ERC20, - to: [randomTarget], - token: dolphinCoin.address, - data: [] - }; - - await delegateProxy.functions.delegate( - transfer.address, - transfer.interface.functions.transfer.encode([details]), - Utils.HIGH_GAS_LIMIT - ); - - const balTarget = await dolphinCoin.functions.balanceOf(randomTarget); - - expect(balTarget).to.be.eql(new ethers.utils.BigNumber(10)); - }); - - it("for many addresses", async () => { - const randomTargets: string[] = Array.from({ length: 10 }, () => - Utils.randomETHAddress() - ); - - const details = { - value: randomTargets.map(_ => 1), - assetType: AssetType.ERC20, - to: randomTargets, - token: dolphinCoin.address, - data: [] - }; - - await delegateProxy.functions.delegate( - transfer.address, - transfer.interface.functions.transfer.encode([details]), - Utils.HIGH_GAS_LIMIT - ); - - for (const target of randomTargets) { - const bal = await dolphinCoin.functions.balanceOf(target); - expect(bal).to.be.eql(new ethers.utils.BigNumber(1)); - } - }); - }); -}); diff --git a/packages/contracts/test/utils/index.ts b/packages/contracts/test/utils/index.ts new file mode 100644 index 000000000..1a93a214d --- /dev/null +++ b/packages/contracts/test/utils/index.ts @@ -0,0 +1,185 @@ +import { AppIdentity } from "@counterfactual/types"; +import * as chai from "chai"; +import { solidity } from "ethereum-waffle"; +import { defaultAbiCoder, keccak256, solidityPack } from "ethers/utils"; + +export const expect = chai.use(solidity).expect; + +export enum AssetType { + ETH, + ERC20, + ANY +} + +export class App { + private static readonly ABI_ENCODER_V2_ENCODING = ` + tuple( + address owner, + address[] signingKeys, + bytes32 appInterfaceHash, + bytes32 termsHash, + uint256 defaultTimeout + ) + `; + + get id(): string { + return this.hashOfEncoding(); + } + + constructor( + readonly owner: string, + readonly signingKeys: string[], + readonly appInterfaceHash: string, + readonly termsHash: string, + readonly defaultTimeout: number + ) {} + + public toJson(): AppIdentity { + return { + owner: this.owner, + signingKeys: this.signingKeys, + appInterfaceHash: this.appInterfaceHash, + termsHash: this.termsHash, + defaultTimeout: this.defaultTimeout + }; + } + + public hashOfEncoding(): string { + return keccak256( + defaultAbiCoder.encode([App.ABI_ENCODER_V2_ENCODING], [this.toJson()]) + ); + } +} + +export class AppInterface { + private static readonly ABI_ENCODER_V2_ENCODING = ` + tuple( + address addr, + bytes4 getTurnTaker, + bytes4 applyAction, + bytes4 resolve, + bytes4 isStateTerminal + ) + `; + + constructor( + readonly addr: string, + readonly getTurnTaker: string, + readonly applyAction: string, + readonly resolve: string, + readonly isStateTerminal: string + ) {} + + public hashOfPackedEncoding(): string { + return keccak256( + defaultAbiCoder.encode( + [AppInterface.ABI_ENCODER_V2_ENCODING], + [ + { + addr: this.addr, + getTurnTaker: this.getTurnTaker, + applyAction: this.applyAction, + resolve: this.resolve, + isStateTerminal: this.isStateTerminal + } + ] + ) + ); + } +} + +export class Terms { + private static readonly ABI_ENCODER_V2_ENCODING = + "tuple(uint8 assetType, uint256 limit, address token)"; + + constructor( + readonly assetType: AssetType, + readonly limit: number, + readonly token: string + ) {} + + public hashOfPackedEncoding(): string { + return keccak256( + defaultAbiCoder.encode( + [Terms.ABI_ENCODER_V2_ENCODING], + [ + { + assetType: this.assetType, + limit: this.limit, + token: this.token + } + ] + ) + ); + } +} + +// TS version of MAppRegistryCore::computeStateHash +export const computeStateHash = ( + id: string, + appStateHash: string, + nonce: number, + timeout: number +) => + keccak256( + solidityPack( + ["bytes1", "bytes32", "uint256", "uint256", "bytes32"], + ["0x19", id, nonce, timeout, appStateHash] + ) + ); + +// TS version of MAppRegistryCore::computeActionHash +export const computeActionHash = ( + turnTaker: string, + previousState: string, + action: string, + setStateNonce: number, + disputeNonce: number +) => + keccak256( + solidityPack( + ["bytes1", "address", "bytes", "bytes", "uint256", "uint256"], + ["0x19", turnTaker, previousState, action, setStateNonce, disputeNonce] + ) + ); + +export class AppInstance { + get id(): string { + return this.hashOfEncoding(); + } + + get appIdentity(): AppIdentity { + return { + owner: this.owner, + signingKeys: this.signingKeys, + appInterfaceHash: this.appInterface.hashOfPackedEncoding(), + termsHash: this.terms.hashOfPackedEncoding(), + defaultTimeout: this.defaultTimeout + }; + } + + constructor( + readonly owner: string, + readonly signingKeys: string[], + readonly appInterface: AppInterface, + readonly terms: Terms, + readonly defaultTimeout: number + ) {} + + public hashOfEncoding(): string { + return keccak256( + defaultAbiCoder.encode( + [ + `tuple( + address owner, + address[] signingKeys, + bytes32 appInterfaceHash, + bytes32 termsHash, + uint256 defaultTimeout + )` + ], + [this.appIdentity] + ) + ); + } +} diff --git a/packages/contracts/truffle-config.js b/packages/contracts/truffle-config.js index 5ae6342db..710d13774 100644 --- a/packages/contracts/truffle-config.js +++ b/packages/contracts/truffle-config.js @@ -1,14 +1,15 @@ const HDWalletProvider = require("truffle-hdwallet-provider"); + require("dotenv").config(); module.exports = { networks: { ganache: { network_id: 7777777, - host: "127.0.0.1", - port: 9545, - gas: "0xfffffffffff", - gasPrice: "0x01" + host: "localhost", + port: 8545, + gas: 0xfffffffffff, + gasPrice: 0x01 }, rinkeby: { network_id: 4, @@ -28,6 +29,7 @@ module.exports = { } }, mocha: { + useColors: true, reporter: "eth-gas-reporter", reporterOptions: { currency: "USD", diff --git a/packages/contracts/tsconfig.json b/packages/contracts/tsconfig.json index bf7e1cb83..5fa97c80d 100644 --- a/packages/contracts/tsconfig.json +++ b/packages/contracts/tsconfig.json @@ -2,7 +2,6 @@ "extends": "../../tsconfig.json", "compilerOptions": { "module": "commonjs", - "outDir": "dist/" }, - "include": ["utils", "test"] + "include": ["src", "utils", "test"] } diff --git a/packages/contracts/utils/appInstance.ts b/packages/contracts/utils/appInstance.ts deleted file mode 100644 index b2e7e3eb2..000000000 --- a/packages/contracts/utils/appInstance.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { ethers } from "ethers"; - -import * as artifacts from "./buildArtifacts"; -import { Contract } from "./contract"; -import { signMessage } from "./misc"; -import { Multisig } from "./multisig"; -import { abiEncodingForStruct, encodeStruct } from "./structEncoding"; - -const { keccak256 } = ethers.utils; - -export enum AssetType { - ETH, - ERC20 -} - -export interface TransferTerms { - assetType: AssetType; - limit: ethers.utils.BigNumber; - token?: string; -} - -export interface AppDefinition { - addr: string; - applyAction: string; - resolve: string; - getTurnTaker: string; - isStateTerminal: string; -} - -function appFromContract(contract: ethers.Contract): AppDefinition { - return { - addr: contract.address, - applyAction: contract.interface.functions.applyAction.sighash, - resolve: contract.interface.functions.resolve.sighash, - getTurnTaker: contract.interface.functions.getTurnTaker.sighash, - isStateTerminal: contract.interface.functions.isStateTerminal.sighash - }; -} - -export class AppInstance { - public readonly app: AppDefinition; - - public contract?: Contract; - public appStateNonce: number = 0; - - constructor( - readonly signerAddrs: string[], - readonly multisig: Multisig, - appContract: ethers.Contract, - readonly appStateEncoding: string, - readonly terms: TransferTerms, - readonly defaultTimeout: number = 10 - ) { - if (this.terms.token === undefined) { - this.terms.token = ethers.constants.AddressZero; - } - this.app = appFromContract(appContract); - } - - public async deploy(sender: ethers.Wallet, registry: ethers.Contract) { - const appHash = keccak256(encodeStruct(appEncoding, this.app)); - const termsHash = keccak256(encodeStruct(termsEncoding, this.terms)); - this.contract = await (await artifacts.AppInstance).deployViaRegistry( - sender, - registry, - [ - this.multisig.address, - this.signerAddrs, - appHash, - termsHash, - this.defaultTimeout - ] - ); - } - - public async setState( - appState: object, - signers: ethers.Wallet[], - timeout: number = 0, - appStateNonce: number = this.appStateNonce + 1 - ) { - if (!this.contract) { - throw new Error("Not deployed"); - } - const appStateHash = keccak256( - encodeStruct(this.appStateEncoding, appState) - ); - const stateHash = computeStateHash( - this.signerAddrs, - appStateHash, - appStateNonce, - timeout - ); - const signatures = signMessage(stateHash, ...signers); - await this.contract.functions.setState( - appStateHash, - appStateNonce, - timeout, - signatures - ); - this.appStateNonce = appStateNonce; - } - - public async setResolution(appState: object) { - if (!this.contract) { - throw new Error("Not deployed"); - } - await this.contract.functions.setResolution( - this.app, - encodeStruct(this.appStateEncoding, appState), - encodeStruct(termsEncoding, this.terms) - ); - } -} - -/** - * Compute the raw state hash for use in the AppInstance contract. - * @param signingKeys Signing keys of the AppInstance - * @param appStateHash App state hash - * @param appStateNonce App state nonce - * @param timeout Time until finalization. - * @returns string 32-byte keccak256 hash - */ -export function computeStateHash( - signingKeys: string[], - appStateHash: string, - appStateNonce: number, - timeout: number -): string { - return ethers.utils.solidityKeccak256( - ["bytes1", "address[]", "uint256", "uint256", "bytes32"], - ["0x19", signingKeys, appStateNonce, timeout, appStateHash] - ); -} - -/** - * Compute the raw action hash for use in the AppInstance contract. - * @param turnTaker The address of the turn taker - * @param prevStateHash The previous app state hash - * @param action The action - * @param appStateNonce The app state nonce - * @param disputeNonce The dispute nonce - * @returns string 32-byte keccak256 hash - */ -export function computeActionHash( - turnTaker: string, - prevStateHash: string, - action: string, - appStateNonce: number, - disputeNonce: number -): string { - return ethers.utils.solidityKeccak256( - ["bytes1", "address", "bytes32", "bytes", "uint256", "uint256"], - ["0x19", turnTaker, prevStateHash, action, appStateNonce, disputeNonce] - ); -} - -/** - * Computes nonce registry key - * @param multisigAddress Address of Multisig contract - * @param nonceSalt Nonce salt - * @returns string 32-byte keccak256 hash - */ -export function computeNonceRegistryKey( - timeout: ethers.utils.BigNumber, - multisigAddress: string, - nonceSalt: string -) { - return ethers.utils.solidityKeccak256( - ["address", "uint256", "bytes32"], - [multisigAddress, timeout, nonceSalt] - ); -} - -/** - * Solidity struct type for the Transfer.Terms struct - */ -export const termsEncoding = abiEncodingForStruct(` - uint8 assetType; - uint256 limit; - address token; -`); - -/** - * Solidity struct type for the App struct - */ -export const appEncoding = abiEncodingForStruct(` - address addr; - bytes4 applyAction; - bytes4 resolve; - bytes4 getTurnTaker; - bytes4 isStateTerminal; -`); diff --git a/packages/contracts/utils/buildArtifacts.ts b/packages/contracts/utils/buildArtifacts.ts deleted file mode 100644 index ff58e409c..000000000 --- a/packages/contracts/utils/buildArtifacts.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * TODO: - * This file has several tslint:disable-next-line rules because it violates the - * rule of variables being camelCase or UPPER_CASE only. We do this because this is - * a bizarre case where these variables are BuildArtifacts which naturally - * represent JSON ABIs of smart contracts in a TypeScript type. We ought to clean this - * up a bit in the future. - */ -import { AbstractContract } from "./contract"; - -// tslint:disable-next-line -export const ConditionalTransaction = AbstractContract.fromArtifactName( - "ConditionalTransaction" -); - -// tslint:disable-next-line -export const NonceRegistry = AbstractContract.fromArtifactName( - "NonceRegistry" -); - -// tslint:disable-next-line -export const Registry = AbstractContract.fromArtifactName("Registry"); - -// tslint:disable-next-line -export const StaticCall = AbstractContract.fromArtifactName("StaticCall"); - -// tslint:disable-next-line -export const Signatures = AbstractContract.fromArtifactName("Signatures"); - -// tslint:disable-next-line -export const Transfer = AbstractContract.fromArtifactName("Transfer"); - -// tslint:disable-next-line -export const AppInstance = AbstractContract.fromArtifactName("AppInstance", { - StaticCall, - Signatures, - Transfer -}); - -// tslint:disable-next-line -export const MinimumViableMultisig = AbstractContract.fromArtifactName( - "MinimumViableMultisig", - { - Signatures - } -); - -// tslint:disable-next-line -export const VirtualAppAgreement = AbstractContract.fromArtifactName( - "ETHVirtualAppAgreement", - { - Transfer, - Registry - } -); - -export default { - AppInstance, - ConditionalTransaction, - MinimumViableMultisig, - NonceRegistry, - Registry, - Signatures, - StaticCall, - Transfer, - VirtualAppAgreement -}; diff --git a/packages/contracts/utils/contract.ts b/packages/contracts/utils/contract.ts deleted file mode 100644 index 8ec25db78..000000000 --- a/packages/contracts/utils/contract.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { ethers } from "ethers"; - -import { HIGH_GAS_LIMIT } from "./misc"; - -const { solidityKeccak256 } = ethers.utils; - -/** - * Simple wrapper around ethers.Contract to include information about Counterfactual instantiation. - */ -export class Contract extends ethers.Contract { - public salt?: string; - public cfAddress?: string; - public registry?: Contract; -} - -/** - * Convenience class for an undeployed contract i.e. only the ABI and bytecode. - */ -export class AbstractContract { - /** - * Load build artifact by name into an abstract contract - * @example - * const CountingApp = AbstractContract.fromArtifactName("CountingApp", {StaticCall}); - * @param artifactName The name of the artifact to load - * @param links Optional AbstractContract libraries to link. - * @returns Truffle artifact wrapped in an AbstractContract. - */ - public static async fromArtifactName( - artifactName: string, - links?: { [name: string]: Promise } - ): Promise { - // these ABI JSON files are generated by truffle - const contract: BuildArtifact = await import(`../../build/contracts/${artifactName}.json`); - return AbstractContract.fromBuildArtifact(contract, links, artifactName); - } - - /** - * Wrap build artifact in abstract contract - * @param buildArtifact Truffle contract to wrap - * @param links Optional AbstractContract libraries to link. - * @returns Truffle artifact wrapped in an AbstractContract. - */ - public static async fromBuildArtifact( - buildArtifact: BuildArtifact, - links?: { [name: string]: Promise }, - artifactName: string = "UntitledContract" - ): Promise { - return new AbstractContract( - buildArtifact.abi, - buildArtifact.bytecode, - buildArtifact.networks, - links, - artifactName - ); - } - - public static async getNetworkID(wallet: ethers.Wallet): Promise { - return (await wallet.provider.getNetwork()).chainId; - } - - /** - * @param abi ABI of the abstract contract - * @param bytecode Binary of the abstract contract - * @param networks Network mapping of deployed addresses - * @param links - * @param contractName - */ - constructor( - readonly abi: string[] | string, - readonly bytecode: string, - readonly networks: NetworkMapping, - readonly links?: { [contractName: string]: Promise }, - readonly contractName?: string - ) {} - - /** - * Get the deployed singleton instance of this abstract contract, if it exists - * @param Signer (with provider) to use for contract calls - * @throws Error if AbstractContract has no deployed address - */ - public async getDeployed(wallet: ethers.Wallet): Promise { - if (!wallet.provider) { - throw new Error("Signer requires provider"); - } - const networkId = (await wallet.provider.getNetwork()).chainId; - const address = this.getDeployedAddress(networkId); - return new Contract(address, this.abi, wallet); - } - - /** - * Deploy new instance of contract - * @param wallet Wallet (with provider) to use for contract calls - * @param args Optional arguments to pass to contract constructor - * @returns New contract instance - */ - public async deploy(wallet: ethers.Wallet, args?: any[]): Promise { - if (!wallet.provider) { - throw new Error("Signer requires provider"); - } - - const networkId = (await wallet.provider.getNetwork()).chainId; - const bytecode = (await this.links) - ? await this.generateLinkedBytecode(networkId) - : this.bytecode; - const contractFactory = new ethers.ContractFactory( - this.abi, - bytecode, - wallet - ); - return contractFactory.deploy(...(args || [])); - } - - /** - * Connect to a deployed instance of this abstract contract - * @param signer Signer (with provider) to use for contract calls - * @param address Address of deployed instance to connect to - * @returns Contract instance - */ - public async connect( - signer: ethers.Signer, - address: string - ): Promise { - return new Contract(address, this.abi, signer); - } - - /** - * Deploys new contract instance through a Counterfactual Registry - * @param signer Signer (with provider) to use for contract calls - * @param registry Counterfactual Registry instance to use - * @param args Optional arguments to pass to contract constructor - * @param salt Optional salt for Counterfactual deployment - * @returns Contract instance - */ - public async deployViaRegistry( - signer: ethers.Signer, - registry: ethers.Contract, - args?: any[], - salt?: string - ): Promise { - const definitelySalt = - salt || - solidityKeccak256(["uint256"], [Math.round(Math.random() * 2e10)]); - - if (!signer.provider) { - throw new Error("Signer requires provider"); - } - - const networkId = (await signer.provider.getNetwork()).chainId; - const bytecode = await this.generateLinkedBytecode(networkId); - const initcode = new ethers.utils.Interface(this.abi).deployFunction.encode( - bytecode, - args || [] - ); - await registry.functions.deploy(initcode, definitelySalt, HIGH_GAS_LIMIT); - const cfAddress = solidityKeccak256( - ["bytes1", "bytes", "uint256"], - ["0x19", initcode, definitelySalt] - ); - - const address = await registry.functions.resolver(cfAddress); - const contract = new Contract(address, this.abi, signer); - contract.cfAddress = cfAddress; - contract.salt = definitelySalt; - contract.registry = registry; - return contract; - } - - public getDeployedAddress(networkId: number): string { - const info = this.networks[networkId]; - if (!info) { - throw new Error( - `Abstract contract ${ - this.contractName - } not deployed on network ${networkId}` - ); - } - return info.address; - } - - public async generateLinkedBytecode(networkId: number): Promise { - if (this.links === undefined) { - throw new Error("Nothing to link"); - } - let bytecode = this.bytecode; - for (const name of Object.keys(this.links)) { - const library = this.links[name]; - const regex = new RegExp(`__${name}_+`, "g"); - const address = (await library).getDeployedAddress(networkId); - const addressHex = address.replace("0x", ""); - bytecode = bytecode.replace(regex, addressHex); - } - return bytecode; - } -} diff --git a/packages/contracts/utils/index.ts b/packages/contracts/utils/index.ts deleted file mode 100644 index d84335bf4..000000000 --- a/packages/contracts/utils/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as chai from "chai"; -import chaiAsPromised from "chai-as-promised"; -import chaiBignumber from "chai-bignumber"; -import chaiString from "chai-string"; -import { ethers } from "ethers"; - -export * from "./appInstance"; -export * from "./structEncoding"; -export * from "./contract"; -export * from "./multisig"; - -export { default as buildArtifacts } from "./buildArtifacts"; - -export const expect = chai - .use(chaiString) - .use(chaiAsPromised) - .use(chaiBignumber(ethers.utils.BigNumber)).expect; diff --git a/packages/contracts/utils/misc.ts b/packages/contracts/utils/misc.ts deleted file mode 100644 index 0c9100122..000000000 --- a/packages/contracts/utils/misc.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { ethers } from "ethers"; - -export const UNIT_ETH = ethers.utils.parseEther("1"); -export const HIGH_GAS_LIMIT = { gasLimit: 6e9 }; - -export const deployContract = async ( - contract: any, - wallet: ethers.Wallet, - args?: any[] -): Promise => { - const contractFactory = new ethers.ContractFactory( - contract.abi, - contract.binary, - wallet - ); - return contractFactory.deploy(...(args || [])); -}; - -export const getDeployedContract = async ( - contract: any, - providerOrSigner: ethers.Wallet | ethers.providers.Provider -): Promise => { - return new ethers.Contract( - (await contract.deployed()).address, - contract.abi, - providerOrSigner - ); -}; - -export const randomETHAddress = (): string => - ethers.utils.hexlify(ethers.utils.randomBytes(20)); - -export function generateEthWallets( - count: number, - provider?: ethers.providers.Provider -): ethers.Wallet[] { - return Array(count) - .fill(0) - .map(() => { - let wallet = ethers.Wallet.createRandom(); - if (provider) { - wallet = wallet.connect(provider); - } - return wallet; - }); -} - -export const setupTestEnv = (web3: any) => { - const provider = new ethers.providers.Web3Provider(web3.currentProvider); - const unlockedAccount = new ethers.Wallet( - process.env.npm_package_config_unlockedAccount!, - provider - ); - return { provider, unlockedAccount }; -}; - -export function signMessageVRS(message, wallet): [number, string, string] { - const signingKey = new ethers.utils.SigningKey(wallet.privateKey); - const sig = signingKey.signDigest(message); - if (typeof sig.recoveryParam === "undefined") { - throw Error("Signature failed."); - } - return [sig.recoveryParam + 27, sig.r, sig.s]; -} - -export function signMessageBytes(message: string, wallet: ethers.Wallet) { - const [v, r, s] = signMessageVRS(message, wallet); - return ( - ethers.utils.hexlify(ethers.utils.padZeros(r, 32)).substring(2) + - ethers.utils.hexlify(ethers.utils.padZeros(s, 32)).substring(2) + - v.toString(16) - ); -} - -export function signMessage(message, ...wallets: ethers.Wallet[]) { - wallets.sort((a, b) => a.address.localeCompare(b.address)); - const signatures = wallets.map(w => signMessageBytes(message, w)); - return `0x${signatures.join("")}`; -} - -export function getParamFromTxEvent( - transaction, - eventName, - paramName, - contract, - contractFactory -) { - let logs = transaction.logs; - if (eventName != null) { - logs = logs.filter(l => l.event === eventName && l.address === contract); - } - chai.assert.equal(logs.length, 1, "too many logs found!"); - const param = logs[0].args[paramName]; - return contractFactory != null ? contractFactory.at(param) : param; -} - -export async function assertRejects(q: Promise) { - let res; - let exceptionThrown = false; - try { - res = await q; - } catch (e) { - exceptionThrown = true; - } finally { - if (!exceptionThrown) { - chai.assert.fail( - res, - null, - "assertRejects: expected a promise rejection" - ); - } - } -} - -export const mineOneBlock = () => { - const web3 = (global as any).web3; - return new Promise((resolve, reject) => { - web3.currentProvider.send( - { - id: new Date().getTime(), - jsonrpc: "2.0", - method: "evm_mine", - params: [] - }, - (err, result) => { - if (err) { - return reject(err); - } - return resolve(result); - } - ); - }); -}; - -export const mineBlocks = async function(n: number) { - for (const _ of Array(n)) { - await mineOneBlock(); - } -}; diff --git a/packages/contracts/utils/multisig.ts b/packages/contracts/utils/multisig.ts deleted file mode 100644 index ce1a6f53f..000000000 --- a/packages/contracts/utils/multisig.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { ethers } from "ethers"; - -import { Signatures } from "./buildArtifacts"; -import { AbstractContract } from "./contract"; -import { HIGH_GAS_LIMIT, signMessage } from "./misc"; - -const enum Operation { - Call = 0, - Delegatecall = 1 -} - -/** - * Helper class for dealing with Multisignature wallets in tests. - * Usage: - * const multisig = new Multisig([alice.address, bob.address]); - * await multisig.deploy(masterAccount); - */ -export class Multisig { - private contract?: ethers.Contract; - - /** - * Creates new undeployed Multisig instance - * @param owners List of owner addresses - */ - constructor(readonly owners: string[]) { - owners.sort((a, b) => a.localeCompare(b)); - } - - /** - * Gets the on-chain address of the Multisig - */ - get address() { - if (!this.contract) { - throw new Error("Must deploy Multisig contract first"); - } - return this.contract.address; - } - - /** - * Deploy Multisig contract on-chain - * @param wallet The wallet (with provider) for the on-chain transaction - */ - public async deploy(wallet: ethers.Wallet) { - const minimumViableMultisig = await AbstractContract.fromArtifactName( - "MinimumViableMultisig", - { - Signatures - } - ); - this.contract = await minimumViableMultisig.deploy(wallet); - await this.contract.functions.setup(this.owners); - } - - /** - * Execute delegatecall originating from Multisig contract - * @param toContract Contract instance to send delegatecall to - * @param funcName The name of the function to execute - * @param args Arguments for the function call - * @param signers The signers of the transaction - */ - public async execDelegatecall( - toContract: ethers.Contract, - funcName: string, - args: any[], - signers: ethers.Wallet[] - ): Promise { - if (toContract.interface.functions[funcName] === undefined) { - throw new Error( - `Tried to execute delegateCall to ${funcName} but no such function exists on the target contract` - ); - } - return this.execTransaction( - toContract, - funcName, - args, - Operation.Delegatecall, - signers - ); - } - - /** - * Execute call originating from Multisig contract - * @param toContract Contract instance to send call to - * @param funcName The name of the function to execute - * @param args Arguments for the function call - * @param signers The signers of the transaction - */ - public async execCall( - toContract: ethers.Contract, - funcName: string, - args: any[], - signers: ethers.Wallet[] - ): Promise { - if (toContract.interface.functions[funcName] === undefined) { - throw new Error( - `Tried to execute call to ${funcName} but no such function exists on the target contract` - ); - } - return this.execTransaction( - toContract, - funcName, - args, - Operation.Call, - signers - ); - } - - private async execTransaction( - toContract: ethers.Contract, - funcName: string, - args: any[], - operation: Operation, - wallets: ethers.Wallet[] - ): Promise { - if (!this.contract) { - throw new Error("Must deploy Multisig contract first"); - } - const value = 0; - const calldata = toContract.interface.functions[funcName].encode(args); - - const transactionHash = await this.contract.functions.getTransactionHash( - toContract.address, - value, - calldata, - operation - ); - - // estimateGas() doesn't work well for delegatecalls, so need to hardcode gas limit - const options = operation === Operation.Delegatecall ? HIGH_GAS_LIMIT : {}; - const signatures = signMessage(transactionHash, ...wallets); - - return this.contract.functions.execTransaction( - toContract.address, - value, - calldata, - operation, - signatures, - options - ); - } -} diff --git a/packages/contracts/utils/structEncoding.ts b/packages/contracts/utils/structEncoding.ts deleted file mode 100644 index c57ca5cc5..000000000 --- a/packages/contracts/utils/structEncoding.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ethers } from "ethers"; - -export function abiEncodingForStruct(structDefinition: string): string { - const definitions: string[] = []; - const lines = structDefinition.split(";"); - for (const line of lines) { - const definition = line.trim(); - if (definition.length === 0) { - continue; - } - const parts = definition.split(" "); - if (parts.length !== 2) { - throw new Error( - `Invalid struct field. Expected '[type] [name]', got '${definition}'` - ); - } - definitions.push(definition); - } - return `tuple(${definitions.join(", ")})`; -} - -export function encodeStruct(encoding: string, struct: object): string { - return ethers.utils.defaultAbiCoder.encode([encoding], [struct]); -} diff --git a/packages/dapp-high-roller/tsconfig.json b/packages/dapp-high-roller/tsconfig.json index 16bad1328..734c8baa5 100644 --- a/packages/dapp-high-roller/tsconfig.json +++ b/packages/dapp-high-roller/tsconfig.json @@ -9,7 +9,9 @@ ], "module": "esnext", "jsx": "react", - "jsxFactory": "h" + "jsxFactory": "h", + // @types/mocha has a duplicate conflict, this is the suggested temp fix. + "skipLibCheck": true, }, "include": [ "src" diff --git a/packages/machine/.env.example b/packages/machine/.env.example new file mode 100644 index 000000000..244846af9 --- /dev/null +++ b/packages/machine/.env.example @@ -0,0 +1,5 @@ +# Values used for test suite +DEV_GANACHE_HOST= +DEV_GANACHE_PORT= +DEV_GANACHE_MNEMONIC= +DEV_GANACHE_NETWORK_ID= diff --git a/packages/machine/.gitignore b/packages/machine/.gitignore index 550c847a6..036cf8169 100644 --- a/packages/machine/.gitignore +++ b/packages/machine/.gitignore @@ -1 +1,2 @@ +networks jest-cache diff --git a/packages/machine/README.md b/packages/machine/README.md index 498550afe..0b6e00c55 100644 --- a/packages/machine/README.md +++ b/packages/machine/README.md @@ -1,6 +1,6 @@ # [@counterfactual/machine](https://github.com/counterfactual/monorepo/packages/machine) -This is the typescript implementation of the [Counterfactual protocol](https://github.com/counterfactual/specs/blob/master/v0/protocols.md). It's responsible for executing these protocols, _producing state commitments_, and thereby effectively facilitating user interaction for off-chain channelized applications. +This is the TypeScript implementation of the [Counterfactual protocol](https://github.com/counterfactual/specs/blob/master/v0/protocols.md). It is responsible for executing the Counterfactual protocols [specified here](https://specs.counterfactual.com) and producing correctly constructed signed commitments that correspond to state transitions of the users' state channels. The specific design philosophy it adopts is the middleware pattern. That is, all of these protocols are naturally broken down into steps, for each of which there is a middleware responsible for executing that step. @@ -34,22 +34,6 @@ yarn build ### Tests -Presently for some of the tests to work, you need to have a `ganache-cli` instance running in the background. To do this, run using: - -```shell -cd ../../ -yarn ganache -cd packages/machine -``` - -You also need to migrate the contracts in the contracts package to generate a `networks` file which the `machine` package directly consumes (for now). - -```shell -cd ../contracts -yarn migrate -cd ../machine -``` - To run all tests: ```shell @@ -59,8 +43,7 @@ yarn test To run only specific tests: ```shell -cd packages/machine -yarn test +yarn test ``` will run tests in files whose filename matches `` (see [Jest's CLI reference](https://jestjs.io/docs/en/cli.html#running-from-the-command-line)). diff --git a/packages/machine/bin/deploy_contracts b/packages/machine/bin/deploy_contracts deleted file mode 100755 index 2dcf6c8b5..000000000 --- a/packages/machine/bin/deploy_contracts +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -set -e - -cd /machine/contracts -./node_modules/.bin/truffle migrate --network docker -cd /machine diff --git a/packages/machine/bin/test b/packages/machine/bin/test deleted file mode 100755 index 007f6e9d1..000000000 --- a/packages/machine/bin/test +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -set -e - -inputArgs=$@ -yarn run test:jest ${inputArgs:-test/*.spec.ts} diff --git a/packages/machine/bin/test-debug b/packages/machine/bin/test-debug deleted file mode 100755 index a17f331ac..000000000 --- a/packages/machine/bin/test-debug +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -# Runs the tests with the break on debug set to true, useful for connecting Chrome/VSC debuggers -set -e - -inputArgs=$@ -yarn run test-debug:jest ${inputArgs:-test/*.spec.ts} diff --git a/packages/machine/build b/packages/machine/build new file mode 120000 index 000000000..111228ee9 --- /dev/null +++ b/packages/machine/build @@ -0,0 +1 @@ +../contracts/build \ No newline at end of file diff --git a/packages/machine/jest.config.js b/packages/machine/jest.config.js new file mode 100644 index 000000000..c7ab8002a --- /dev/null +++ b/packages/machine/jest.config.js @@ -0,0 +1,27 @@ +// TODO: I'm guessing some of the below options are set to the default +// values of Jest configs. Haven't spent the time to make this config +// more minimal and not redundant but ought to do that at some point. + +module.exports = { + "verbose": false, + "bail": true, + "rootDir": ".", + "cacheDirectory": "jest-cache", + "transform": { + "^.+\\.tsx?$": "ts-jest" + }, + "testRegex": "\\.spec.(jsx?|tsx?)$", + "testPathIgnorePatterns": [ + "node_modules", + "dist" + ], + "roots": [ + "test" + ], + "moduleFileExtensions": [ + "ts", + "js", + "json" + ], + "testURL": "http://localhost/" +} diff --git a/packages/machine/launch.json b/packages/machine/launch.json deleted file mode 100644 index 6a9ab272f..000000000 --- a/packages/machine/launch.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Debug Jest Tests", - "type": "node", - "request": "launch", - "runtimeArgs": [ - "--inspect-brk", - "${workspaceRoot}/node_modules/.bin/jest", - "--runInBand" - ], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen" - } - ] -} diff --git a/packages/machine/package.json b/packages/machine/package.json index 78b67f74b..30f8550e1 100644 --- a/packages/machine/package.json +++ b/packages/machine/package.json @@ -10,60 +10,34 @@ ], "license": "MIT", "scripts": { - "clean": "rm -rf .rpt2_cache jest-cache build dist", - "build": "tsc -p tsconfig.test.json && rollup -c", - "test": "tsc -b && node --trace-warnings node_modules/.bin/jest --runInBand --detectOpenHandles --bail", - "test-debug": "node --inspect-brk node_modules/.bin/jest --runInBand", - "test-debug-ide": "node $NODE_DEBUG_OPTION node_modules/.bin/jest --runInBand", - "lint:fix": "tslint -c tslint.json -p . --fix", + "build": "tsc -p . && rollup -c", + "test": "./scripts/test.sh", + "test:unit": "jest --detectOpenHandles unit", + "lint:fix": "yarn lint --fix", "lint": "tslint -c tslint.json -p ." }, "devDependencies": { - "@counterfactual/contracts": "0.0.2", + "@counterfactual/types": "0.0.1", + "@counterfactual/typescript-typings": "0.0.1", + "@types/chai": "^4.1.7", "@types/jest": "^23.3.3", "@types/node": "^10.9.3", - "ethers": "^4.0.17", + "chai": "^4.2.0", + "dotenv-safe": "^6.1.0", + "ganache-cli": "6.2.4", "jest": "23.6.0", "rollup": "^0.68.0", "rollup-plugin-json": "^3.1.0", "rollup-plugin-typescript2": "^0.18.0", + "truffle": "5.0.0-beta.1", + "truffle-hdwallet-provider": "^1.0.0-web3one.5", "ts-jest": "23.10.5", "tslint": "^5.11.0", - "typescript": "^3.1.2" + "typescript": "^3.1.2", + "typescript-memoize": "^1.0.0-alpha.3" }, "dependencies": { - "@counterfactual/cf.js": "0.0.1", - "@counterfactual/typescript-typings": "0.0.1", - "ethers": "^4.0.17" - }, - "config": { - "unlockedAccount0": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257a", - "unlockedAccount1": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257b", - "unlockedAccount2": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257c", - "unlockedAccount3": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d", - "etherBalance": "1000000000000000000000000" - }, - "jest": { - "verbose": false, - "bail": true, - "rootDir": ".", - "cacheDirectory": "jest-cache", - "transform": { - "^.+\\.tsx?$": "ts-jest" - }, - "testRegex": "\\.spec.(jsx?|tsx?)$", - "testPathIgnorePatterns": [ - "node_modules", - "dist" - ], - "roots": [ - "test" - ], - "moduleFileExtensions": [ - "ts", - "js", - "json" - ], - "testURL": "http://localhost/" + "@counterfactual/contracts": "0.0.3", + "ethers": "4.0.20" } } diff --git a/packages/machine/rollup.config.js b/packages/machine/rollup.config.js index 0463d8f5a..09854b595 100644 --- a/packages/machine/rollup.config.js +++ b/packages/machine/rollup.config.js @@ -36,9 +36,7 @@ export default [ ...Object.keys(pkg.peerDependencies || {}) ], plugins: [ - typescript({ - typescript: require("typescript") - }), + typescript(), json({ compact: true }) ] } diff --git a/packages/machine/scripts/rollup.sh b/packages/machine/scripts/rollup.sh deleted file mode 100755 index 708ab8ee4..000000000 --- a/packages/machine/scripts/rollup.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -for file in ./rollup/rollup.config.*.js; do - ./node_modules/.bin/rollup -c $file -done diff --git a/packages/machine/scripts/test.sh b/packages/machine/scripts/test.sh new file mode 100755 index 000000000..443af069d --- /dev/null +++ b/packages/machine/scripts/test.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +set -e + +function clean { + kill ${PID_FOR_GANACHE_CLI} +} + +trap clean INT TERM EXIT + +# Gets the val of a key=val pair by key specified in an env file +# +# Args: +# $1 The key +# $2 The filename +# $3 Default value if value does not exist +read_var() { + VAR=$(grep $1 $2 --no-messages | xargs) + IFS="=" read -ra VAR <<< "$VAR" + echo ${VAR[1]:=$3} +} + +[[ "${PWD##*/}" != machine ]] && \ + { echo "Test script must be run in /machine directory"; exit 1; } + +echo "📧 Reading environment values from .env, defaulting to values in test.sh" +export DEV_GANACHE_HOST=$(read_var DEV_GANACHE_HOST .env 127.0.0.1) +export DEV_GANACHE_PORT=$(read_var DEV_GANACHE_PORT .env 8546) +export DEV_GANACHE_NETWORK_ID=$(read_var DEV_GANACHE_NETWORK_ID .env 8888888) +export DEV_GANACHE_MNEMONIC=$( + read_var DEV_GANACHE_MNEMONIC .env \ + "brain surround have swap horror body response double fire dumb bring hazard" +) + +{ + long_console_info="⛓ Starting ganache-cli at " + long_console_info+="http://${DEV_GANACHE_HOST}:${DEV_GANACHE_PORT} " + long_console_info+="(network_id: ${DEV_GANACHE_NETWORK_ID})" +} +echo ${long_console_info} +ganache-cli \ + --defaultBalanceEther 10000 \ + --gasLimit 0xfffffffffff \ + --gasPrice 0x01 \ + --host ${DEV_GANACHE_HOST} \ + --port ${DEV_GANACHE_PORT} \ + --mnemonic "${DEV_GANACHE_MNEMONIC}" `# must be quoted to include spaces` \ + --networkId ${DEV_GANACHE_NETWORK_ID} \ + --quiet \ + &> /dev/null \ + & + +PID_FOR_GANACHE_CLI=$! + +echo "⚙️ Running migrations with build artifacts from @counterfactual/contracts" +# TODO: For some reason this re-compiles all of the contracts unnecessarily +# and there isn't a --no-compile option on the command :( +yarn run truffle migrate --network machine --reset > /dev/null + +echo "🧪 Starting jest test suites" +jest --detectOpenHandles $1 diff --git a/packages/machine/src/action.ts b/packages/machine/src/action.ts deleted file mode 100644 index 7bd8db427..000000000 --- a/packages/machine/src/action.ts +++ /dev/null @@ -1,78 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; - -import { Context, InstructionExecutor } from "./instruction-executor"; -import { Opcode } from "./opcodes"; -import { InternalMessage, StateProposal } from "./types"; - -export class ActionExecution { - public actionName: cf.legacy.node.ActionName; - public instructions: (Opcode | Function)[]; - public clientMessage: cf.legacy.node.ClientActionMessage; - public instructionExecutor: InstructionExecutor; - - constructor( - actionName: cf.legacy.node.ActionName, - instructions: (Opcode | Function)[], - clientMessage: cf.legacy.node.ClientActionMessage, - instructionExecutor: InstructionExecutor - ) { - this.actionName = actionName; - this.instructions = instructions; - this.clientMessage = clientMessage; - this.instructionExecutor = instructionExecutor; - } - - public createContext(): Context { - return { - intermediateResults: { - outbox: [], - inbox: [] - }, - // https://github.com/counterfactual/monorepo/issues/136 - instructionExecutor: this.instructionExecutor - }; - } - - public async runAll(): Promise { - let instructionPointer = 0; - const context = this.createContext(); - - while (instructionPointer < this.instructions.length) { - try { - const instruction = this.instructions[instructionPointer]; - - if (typeof instruction === "function") { - instruction( - new InternalMessage( - this.actionName, - Object.create(null), - this.clientMessage - ), - context, - this.instructionExecutor.node - ); - instructionPointer += 1; - continue; - } - - const internalMessage = new InternalMessage( - this.actionName, - instruction, - this.clientMessage - ); - - await this.instructionExecutor.middleware.run(internalMessage, context); - instructionPointer += 1; - } catch (e) { - throw Error( - `While executing op number ${instructionPointer} at seq ${ - this.clientMessage.seq - } of protocol ${ - this.actionName - }, execution failed with the following error. ${e.stack}` - ); - } - } - return context.intermediateResults.proposedStateTransition!; - } -} diff --git a/packages/machine/src/flows/index.ts b/packages/machine/src/flows/index.ts new file mode 100644 index 000000000..2d9ac5163 --- /dev/null +++ b/packages/machine/src/flows/index.ts @@ -0,0 +1,30 @@ +import { Protocol } from "../types"; + +import { INSTALL_PROTOCOL } from "./install"; +import { INSTALL_VIRTUAL_APP_PROTOCOL } from "./install-virtual-app"; +import { SETUP_PROTOCOL } from "./setup"; +import { UNINSTALL_PROTOCOL } from "./uninstall"; +import { UPDATE_PROTOCOL } from "./update"; + +const protocolsByName = { + [Protocol.Setup]: SETUP_PROTOCOL, + [Protocol.Install]: INSTALL_PROTOCOL, + [Protocol.Update]: UPDATE_PROTOCOL, + [Protocol.Uninstall]: UNINSTALL_PROTOCOL, + [Protocol.InstallVirtualApp]: INSTALL_VIRTUAL_APP_PROTOCOL +}; + +export function getProtocolFromName(protocolName: Protocol) { + if (!(protocolName in protocolsByName)) { + throw Error(`Received invalid protocol type ${protocolName}`); + } + return protocolsByName[protocolName]; +} + +export { + INSTALL_PROTOCOL, + INSTALL_VIRTUAL_APP_PROTOCOL, + SETUP_PROTOCOL, + UNINSTALL_PROTOCOL, + UPDATE_PROTOCOL +}; diff --git a/packages/machine/src/flows/install-virtual-app.ts b/packages/machine/src/flows/install-virtual-app.ts index 8a296ac39..6ba9c8b46 100644 --- a/packages/machine/src/flows/install-virtual-app.ts +++ b/packages/machine/src/flows/install-virtual-app.ts @@ -1,50 +1,56 @@ -import { Context } from "../instruction-executor"; +import { StateChannel } from "../models"; import { Opcode } from "../opcodes"; -import { InternalMessage } from "../types"; +import { ProtocolMessage } from "../protocol-types-tbd"; +import { Context } from "../types"; -export const INSTALL_VIRTUAL_APP_FLOW = { +/** + * @description This exchange is described at the following URL: + * + * FIXME: @xuanji pls add + * + */ +// FIXME: Not fully implemented yet +export const INSTALL_VIRTUAL_APP_PROTOCOL = { 0: [ - (message: InternalMessage, context: Context, node) => { + (message: ProtocolMessage, context: Context, state: StateChannel) => { // copy client message - context.intermediateResults.outbox.push(message.clientMessage); - context.intermediateResults.outbox[0].seq = 1; - context.intermediateResults.outbox[0].toAddress = - message.clientMessage.data.intermediary; + context.outbox.push(message); + context.outbox[0].seq = 1; + // context.outbox[0].toAddress = message.data.intermediary; }, // send to intermediary Opcode.IO_SEND ], + 1: [ - (message: InternalMessage, context: Context, node) => { - const clientMessage = message.clientMessage; - context.intermediateResults.outbox.push(clientMessage); - context.intermediateResults.outbox[0].seq = 2; - context.intermediateResults.outbox[0].fromAddress = - clientMessage.data.initiating; - context.intermediateResults.outbox[0].toAddress = - clientMessage.data.responding; + (message: ProtocolMessage, context: Context, state: StateChannel) => { + context.outbox.push(message); + context.outbox[0].seq = 2; + // context.outbox[0].fromAddress = message.data.initiating; + // context.outbox[0].toAddress = message.data.responding; }, + Opcode.IO_SEND, + // wait for the install countersign Opcode.IO_WAIT, + () => {} // // send the self-remove // Opcode.IO_SEND, // Opcode.IO_SEND ], + 2: [ - (message: InternalMessage, context: Context, node) => { - // countersign - const clientMessage = message.clientMessage; - context.intermediateResults.outbox.push(clientMessage); - context.intermediateResults.outbox[0].seq = 3; - context.intermediateResults.outbox[0].fromAddress = - clientMessage.data.responding; - context.intermediateResults.outbox[0].toAddress = - clientMessage.data.intermediary; + (message: ProtocolMessage, context: Context, state: StateChannel) => { + context.outbox.push(message); + context.outbox[0].seq = 3; + // context.outbox[0].fromAddress = message.data.responding; + // context.outbox[0].toAddress = message.data.intermediary; }, + Opcode.IO_SEND // // wait for self-remove diff --git a/packages/machine/src/flows/install.ts b/packages/machine/src/flows/install.ts index 9f0f97b9b..c4a01154b 100644 --- a/packages/machine/src/flows/install.ts +++ b/packages/machine/src/flows/install.ts @@ -1,74 +1,131 @@ -import { ClientActionMessage } from "@counterfactual/cf.js/dist/src/legacy/node"; +import { AssetType, NetworkContext } from "@counterfactual/types"; -import { Context } from "../instruction-executor"; -import { NextMsgGenerator } from "../middleware/middleware"; -import { EthOpGenerator } from "../middleware/protocol-operation"; -import { InstallProposer } from "../middleware/state-transition/install-proposer"; +import { InstallCommitment } from "../middleware/protocol-operation"; +import { AppInstance, StateChannel } from "../models"; import { Opcode } from "../opcodes"; -import { InternalMessage } from "../types"; +import { InstallData, ProtocolMessage } from "../protocol-types-tbd"; +import { Context } from "../types"; -const swap = (msg: ClientActionMessage) => { - const from = msg.fromAddress; - const to = msg.toAddress; - msg.fromAddress = to; - msg.toAddress = from; -}; +import { prepareToSendSignature } from "./signature-forwarder"; -export const INSTALL_FLOW = { +/** + * @description This exchange is described at the following URL: + * + * specs.counterfactual.com/05-install-protocol#messages + * + */ +export const INSTALL_PROTOCOL = { 0: [ - (message, context: Context, node) => { - context.intermediateResults.proposedStateTransition = InstallProposer.propose( - message, - context, - node - ); - context.intermediateResults.operation = EthOpGenerator.install( - message, - context, - node, - context.intermediateResults.proposedStateTransition.state, - context.intermediateResults.proposedStateTransition.cfAddr! - ); - }, + // Compute the next state of the channel + proposeStateTransition, + + // Decide whether or not to sign the transition Opcode.OP_SIGN, - (message: InternalMessage, context: Context) => { - const ret = NextMsgGenerator.generate2( - message.clientMessage, - context.intermediateResults.signature! - ); - context.intermediateResults.outbox.push(ret); - }, + + // Wrap the signature into a message to be sent + prepareToSendSignature, + + // Send the message to your counterparty Opcode.IO_SEND, + + // Wait for them to countersign the message Opcode.IO_WAIT, + + // Verify they did indeed countersign the right thing Opcode.OP_SIGN_VALIDATE, + + // Consider the state transition finished and commit it Opcode.STATE_TRANSITION_COMMIT ], + 1: [ - (message, context: Context, node) => { - swap(message.clientMessage); - context.intermediateResults.proposedStateTransition = InstallProposer.propose( - message, - context, - node - ); - context.intermediateResults.operation = EthOpGenerator.install( - message, - context, - node, - context.intermediateResults.proposedStateTransition.state, - context.intermediateResults.proposedStateTransition.cfAddr! - ); - }, + // Compute the _proposed_ next state of the channel + proposeStateTransition, + + // Validate your counterparties signature is for the above proposal Opcode.OP_SIGN_VALIDATE, + + // Sign the same state update yourself Opcode.OP_SIGN, - (message: InternalMessage, context: Context) => { - const ret = NextMsgGenerator.generate2( - message.clientMessage, - context.intermediateResults.signature! - ); - context.intermediateResults.outbox.push(ret); - }, + + // Wrap the signature into a message to be sent + prepareToSendSignature, + + // Send the message to your counterparty Opcode.IO_SEND, + + // Consider the state transition finished and commit it Opcode.STATE_TRANSITION_COMMIT ] }; + +function proposeStateTransition( + message: ProtocolMessage, + context: Context, + state: StateChannel +) { + const { + aliceBalanceDecrement, + bobBalanceDecrement, + signingKeys, + initialState, + terms, + appInterface, + defaultTimeout + } = message.params as InstallData; + + const app = new AppInstance( + state.multisigAddress, + signingKeys, + defaultTimeout, + appInterface, + terms, + // KEY: Sets it to NOT be a MetaChannelApp + false, + // KEY: The app sequence number + // TODO: Should validate that the proposed app sequence number is also + // the computed value here and is ALSO still the number compute + // inside the installApp function below + state.numInstalledApps + 1, + initialState, + // KEY: Set the nonce to be 0 + 0, + defaultTimeout + ); + + context.stateChannel = state.installApp( + app, + aliceBalanceDecrement, + bobBalanceDecrement + ); + + context.operation = constructInstallOp( + context.network, + context.stateChannel, + app.id + ); +} + +export function constructInstallOp( + network: NetworkContext, + stateChannel: StateChannel, + appInstanceId: string +) { + const app = stateChannel.getAppInstance(appInstanceId); + + const freeBalance = stateChannel.getFreeBalanceFor(AssetType.ETH); + + return new InstallCommitment( + network, + stateChannel.multisigAddress, + stateChannel.multisigOwners, + app.identity, + app.terms, + freeBalance.identity, + freeBalance.terms, + freeBalance.hashOfLatestState, + freeBalance.nonce, + freeBalance.timeout, + freeBalance.appSeqNo + ); +} diff --git a/packages/machine/src/flows/setup.ts b/packages/machine/src/flows/setup.ts index 8435ca92b..5b294d83e 100644 --- a/packages/machine/src/flows/setup.ts +++ b/packages/machine/src/flows/setup.ts @@ -1,67 +1,84 @@ -import { ClientActionMessage } from "@counterfactual/cf.js/dist/src/legacy/node"; +import { AssetType, NetworkContext } from "@counterfactual/types"; -import { Context } from "../instruction-executor"; -import { NextMsgGenerator } from "../middleware/middleware"; -import { EthOpGenerator } from "../middleware/protocol-operation"; -import { SetupProposer } from "../middleware/state-transition/setup-proposer"; +import { SetupCommitment } from "../middleware/protocol-operation"; +import { StateChannel } from "../models/state-channel"; import { Opcode } from "../opcodes"; -import { InternalMessage } from "../types"; +import { ProtocolMessage } from "../protocol-types-tbd"; +import { Context } from "../types"; -const swap = (msg: ClientActionMessage) => { - const from = msg.fromAddress; - const to = msg.toAddress; - msg.fromAddress = to; - msg.toAddress = from; -}; +import { prepareToSendSignature } from "./signature-forwarder"; -export const SETUP_FLOW = { +/** + * @description This exchange is described at the following URL: + * + * specs.counterfactual.com/04-setup-protocol#messages + * + */ +export const SETUP_PROTOCOL = { 0: [ - (message, context, node) => { - context.intermediateResults.proposedStateTransition = SetupProposer.propose( - message - ); - context.intermediateResults.operation = EthOpGenerator.setup( - message, - node, - context.intermediateResults.proposedStateTransition.state - ); - }, + // Compute the next state of the channel + proposeStateTransition, + + // Decide whether or not to sign the transition Opcode.OP_SIGN, - (message: InternalMessage, context: Context) => { - const ret = NextMsgGenerator.generate2( - message.clientMessage, - context.intermediateResults.signature! - ); - context.intermediateResults.outbox.push(ret); - }, + + // Wrap the signature into a message to be sent + prepareToSendSignature, + + // Send the message to your counterparty Opcode.IO_SEND, + + // Wait for them to countersign the message Opcode.IO_WAIT, + + // Verify they did indeed countersign the right thing Opcode.OP_SIGN_VALIDATE, + + // Consider the state transition finished and commit it Opcode.STATE_TRANSITION_COMMIT ], + 1: [ - (message: InternalMessage, context: Context, node) => { - swap(message.clientMessage); - - context.intermediateResults.proposedStateTransition = SetupProposer.propose( - message - ); - context.intermediateResults.operation = EthOpGenerator.setup( - message, - node, - context.intermediateResults.proposedStateTransition.state - ); - }, + // Compute the _proposed_ next state of the channel + proposeStateTransition, + + // Validate your counterparties signature is for the above proposal Opcode.OP_SIGN_VALIDATE, + + // Sign the same state update yourself Opcode.OP_SIGN, - (message: InternalMessage, context: Context) => { - const ret = NextMsgGenerator.generate2( - message.clientMessage, - context.intermediateResults.signature! - ); - context.intermediateResults.outbox.push(ret); - }, + + // Wrap the signature into a message to be sent + prepareToSendSignature, + + // Send the message to your counterparty Opcode.IO_SEND, + + // Consider the state transition finished and commit it Opcode.STATE_TRANSITION_COMMIT ] }; + +function proposeStateTransition( + message: ProtocolMessage, + context: Context, + state: StateChannel +) { + context.stateChannel = state.setupChannel(context.network); + context.operation = constructSetupOp(context.network, context.stateChannel); +} + +export function constructSetupOp( + network: NetworkContext, + stateChannel: StateChannel +) { + const freeBalance = stateChannel.getFreeBalanceFor(AssetType.ETH); + + return new SetupCommitment( + network, + stateChannel.multisigAddress, + stateChannel.multisigOwners, + freeBalance.identity, + freeBalance.terms + ); +} diff --git a/packages/machine/src/flows/signature-forwarder.ts b/packages/machine/src/flows/signature-forwarder.ts new file mode 100644 index 000000000..b01f25da9 --- /dev/null +++ b/packages/machine/src/flows/signature-forwarder.ts @@ -0,0 +1,15 @@ +import { StateChannel } from "../models"; +import { ProtocolMessage } from "../protocol-types-tbd"; +import { Context } from "../types"; + +export function prepareToSendSignature( + message: ProtocolMessage, + context: Context, + state: StateChannel +) { + context.outbox.push({ + ...message, + signature: context.signature, + seq: message.seq + 1 + }); +} diff --git a/packages/machine/src/flows/uninstall.ts b/packages/machine/src/flows/uninstall.ts index 3c2a977b1..cb71b1377 100644 --- a/packages/machine/src/flows/uninstall.ts +++ b/packages/machine/src/flows/uninstall.ts @@ -1,70 +1,127 @@ -import { ClientActionMessage } from "@counterfactual/cf.js/dist/src/legacy/node"; +import { + AssetType, + ETHBucketAppState, + NetworkContext +} from "@counterfactual/types"; -import { Context } from "../instruction-executor"; -import { NextMsgGenerator } from "../middleware/middleware"; -import { EthOpGenerator } from "../middleware/protocol-operation"; -import { UninstallProposer } from "../middleware/state-transition/uninstall-proposer"; +import { UninstallCommitment } from "../middleware/protocol-operation"; +import { StateChannel } from "../models"; import { Opcode } from "../opcodes"; -import { InternalMessage } from "../types"; +import { ProtocolMessage, UninstallData } from "../protocol-types-tbd"; +import { Context } from "../types"; -const swap = (msg: ClientActionMessage) => { - const from = msg.fromAddress; - const to = msg.toAddress; - msg.fromAddress = to; - msg.toAddress = from; -}; +import { prepareToSendSignature } from "./signature-forwarder"; -export const UNINSTALL_FLOW = { +/** + * @description This exchange is described at the following URL: + * + * specs.counterfactual.com/06-uninstall-protocol#messages + * + */ +export const UNINSTALL_PROTOCOL = { 0: [ - (message, context, node) => { - context.intermediateResults.proposedStateTransition = UninstallProposer.propose( - message, - node - ); - context.intermediateResults.operation = EthOpGenerator.generate( - message, - () => {}, - context, - node - ); - }, + // Compute the next state of the channel + proposeStateTransition, + + // Decide whether or not to sign the transition Opcode.OP_SIGN, - (message: InternalMessage, context: Context) => { - const ret = NextMsgGenerator.generate2( - message.clientMessage, - context.intermediateResults.signature! - ); - context.intermediateResults.outbox.push(ret); - }, + + // Wrap the signature into a message to be sent + prepareToSendSignature, + + // Send the message to your counterparty Opcode.IO_SEND, + + // Wait for them to countersign the message Opcode.IO_WAIT, + + // Verify they did indeed countersign the right thing Opcode.OP_SIGN_VALIDATE, + + // Consider the state transition finished and commit it Opcode.STATE_TRANSITION_COMMIT ], + 1: [ - (message, context, node) => { - swap(message.clientMessage); - context.intermediateResults.proposedStateTransition = UninstallProposer.propose( - message, - node - ); - context.intermediateResults.operation = EthOpGenerator.generate( - message, - () => {}, - context, - node - ); - }, + // Compute the _proposed_ next state of the channel + proposeStateTransition, + + // Validate your counterparties signature is for the above proposal Opcode.OP_SIGN_VALIDATE, + + // Sign the same state update yourself Opcode.OP_SIGN, - (message: InternalMessage, context: Context) => { - const ret = NextMsgGenerator.generate2( - message.clientMessage, - context.intermediateResults.signature! - ); - context.intermediateResults.outbox.push(ret); - }, + + // Wrap the signature into a message to be sent + prepareToSendSignature, + + // Send the message to your counterparty Opcode.IO_SEND, + + // Consider the state transition finished and commit it Opcode.STATE_TRANSITION_COMMIT ] }; + +function proposeStateTransition( + message: ProtocolMessage, + context: Context, + state: StateChannel +) { + const { + appInstanceId, + aliceBalanceIncrement, + bobBalanceIncrement + } = message.params as UninstallData; + + context.stateChannel = state.uninstallApp( + appInstanceId, + aliceBalanceIncrement, + bobBalanceIncrement + ); + + context.operation = constructUninstallOp( + context.network, + context.stateChannel, + appInstanceId + ); +} + +export function constructUninstallOp( + network: NetworkContext, + stateChannel: StateChannel, + uninstallTargetId: string +) { + if (uninstallTargetId === undefined) { + throw new Error( + `Request to uninstall an undefined app id: ${uninstallTargetId}` + ); + } + + const freeBalance = stateChannel.getFreeBalanceFor(AssetType.ETH); + + // FIXME: We need a means of checking if proposed resolution is good + // if (.isValidUninstall(app.state, uninstallResolutions)) { + // continue; + // } + + // NOTE: You might be wondering ... why isn't aliceBalanceIncrement and + // bobBalanceIncrmeent in the scope of this function? Well, the answer + // is that the Uninstall Protocol requires users to sign a FULL OVERWRITE + // of the FreeBalance app's state. We already assigned the new values in + // the method on the StateChannel earlier, which is in scope + // at this point. So, when we pass it into UninstallCommitment below, it reads + // from the newly updated latestState property to generate the commitment. + + return new UninstallCommitment( + network, + stateChannel.multisigAddress, + stateChannel.multisigOwners, + freeBalance.identity, + freeBalance.terms, + freeBalance.state as ETHBucketAppState, + freeBalance.nonce, + freeBalance.timeout, + freeBalance.appSeqNo + ); +} diff --git a/packages/machine/src/flows/update.ts b/packages/machine/src/flows/update.ts index 5e18f3c38..b93e116f4 100644 --- a/packages/machine/src/flows/update.ts +++ b/packages/machine/src/flows/update.ts @@ -1,70 +1,90 @@ -import { ClientActionMessage } from "@counterfactual/cf.js/dist/src/legacy/node"; +import { NetworkContext } from "@counterfactual/types"; -import { Context } from "../instruction-executor"; -import { NextMsgGenerator } from "../middleware/middleware"; -import { EthOpGenerator } from "../middleware/protocol-operation"; -import { UpdateProposer } from "../middleware/state-transition/update-proposer"; +import { SetStateCommitment } from "../middleware/protocol-operation"; +import { StateChannel } from "../models/state-channel"; import { Opcode } from "../opcodes"; -import { InternalMessage } from "../types"; +import { ProtocolMessage, UpdateData } from "../protocol-types-tbd"; +import { Context } from "../types"; -const swap = (msg: ClientActionMessage) => { - const from = msg.fromAddress; - const to = msg.toAddress; - msg.fromAddress = to; - msg.toAddress = from; -}; +import { prepareToSendSignature } from "./signature-forwarder"; -export const UPDATE_FLOW = { +/** + * @description This exchange is described at the following URL: + * + * specs.counterfactual.com/07-update-protocol#messages + * + */ +export const UPDATE_PROTOCOL = { 0: [ - (message, context, node) => { - context.intermediateResults.proposedStateTransition = UpdateProposer.propose( - message, - node - ); - context.intermediateResults.operation = EthOpGenerator.generate( - message, - () => {}, - context, - node - ); - }, + // Compute the next state of the channel + proposeStateTransition, + + // Decide whether or not to sign the transition Opcode.OP_SIGN, - (message: InternalMessage, context: Context) => { - const ret = NextMsgGenerator.generate2( - message.clientMessage, - context.intermediateResults.signature! - ); - context.intermediateResults.outbox.push(ret); - }, + + // Wrap the signature into a message to be sent + prepareToSendSignature, + + // Send the message to your counterparty Opcode.IO_SEND, + + // Wait for them to countersign the message Opcode.IO_WAIT, + + // Verify they did indeed countersign the right thing Opcode.OP_SIGN_VALIDATE, + + // Consider the state transition finished and commit it Opcode.STATE_TRANSITION_COMMIT ], + 1: [ - (message, context, node) => { - swap(message.clientMessage); - context.intermediateResults.proposedStateTransition = UpdateProposer.propose( - message, - node - ); - context.intermediateResults.operation = EthOpGenerator.generate( - message, - () => {}, - context, - node - ); - }, + // Compute the _proposed_ next state of the channel + proposeStateTransition, + + // Validate your counterparties signature is for the above proposal Opcode.OP_SIGN_VALIDATE, + + // Sign the same state update yourself Opcode.OP_SIGN, - (message: InternalMessage, context: Context) => { - const ret = NextMsgGenerator.generate2( - message.clientMessage, - context.intermediateResults.signature! - ); - context.intermediateResults.outbox.push(ret); - }, + + // Wrap the signature into a message to be sent + prepareToSendSignature, + + // Send the message to your counterparty Opcode.IO_SEND, + + // Consider the state transition finished and commit it Opcode.STATE_TRANSITION_COMMIT ] }; + +function proposeStateTransition( + message: ProtocolMessage, + context: Context, + state: StateChannel +) { + const { appInstanceId, newState } = message.params as UpdateData; + context.stateChannel = state.setState(appInstanceId, newState); + context.operation = constructUpdateOp( + context.network, + context.stateChannel, + appInstanceId + ); +} + +export function constructUpdateOp( + network: NetworkContext, + stateChannel: StateChannel, + appInstanceId: string +) { + const app = stateChannel.getAppInstance(appInstanceId); + + return new SetStateCommitment( + network, + app.identity, + app.hashOfLatestState, + app.nonce, + app.timeout + ); +} diff --git a/packages/machine/src/index.ts b/packages/machine/src/index.ts index c191be2de..636fea0be 100644 --- a/packages/machine/src/index.ts +++ b/packages/machine/src/index.ts @@ -1,19 +1,3 @@ -import * as instructionExecutor from "./instruction-executor"; -import * as instructions from "./instructions"; -import * as middleware from "./middleware/middleware"; -import * as protocolOperations from "./middleware/protocol-operation"; -import * as protocolTypes from "./middleware/protocol-operation/types"; -import * as mixins from "./mixins"; -import * as state from "./node"; -import * as types from "./types"; +import { InstructionExecutor } from "./instruction-executor"; -export { - protocolOperations, - protocolTypes, - instructions, - middleware, - mixins, - state, - types, - instructionExecutor -}; +export { InstructionExecutor }; diff --git a/packages/machine/src/instruction-executor.ts b/packages/machine/src/instruction-executor.ts index 4c12768be..754ebcbbc 100644 --- a/packages/machine/src/instruction-executor.ts +++ b/packages/machine/src/instruction-executor.ts @@ -1,210 +1,149 @@ -import * as cf from "@counterfactual/cf.js"; -import { ethers } from "ethers"; - -import { ActionExecution } from "./action"; -import { FLOWS } from "./instructions"; -import { Middleware } from "./middleware/middleware"; -import { ProtocolOperation } from "./middleware/protocol-operation/types"; -import { Node } from "./node"; +import { getProtocolFromName } from "./flows"; +import { Middleware } from "./middleware"; +import { StateChannel } from "./models"; import { Opcode } from "./opcodes"; -import { InstructionMiddlewareCallback, StateProposal } from "./types"; - -export class InstructionExecutorConfig { - constructor( - readonly responseHandler: cf.legacy.node.ResponseSink, - readonly network: cf.legacy.network.NetworkContext, - readonly state?: cf.legacy.channel.StateChannelInfos - ) {} +import { + InstallData, + MetaChannelInstallAppData, + ProtocolMessage, + UninstallData, + UpdateData +} from "./protocol-types-tbd"; +import { + Context, + Instruction, + InstructionMiddlewareCallback, + Protocol +} from "./types"; + +function genericProtocolMessageFields(sc: StateChannel) { + return { + multisigAddress: sc.multisigAddress, + // TODO: Figure out how to fetch these + fromAddress: "0x0", + toAddress: "0x0", + seq: 0, + signature: undefined + }; } export class InstructionExecutor { - /** - * The object responsible for processing each Instruction in the Vm. - */ public middleware: Middleware; - /** - * The delegate handler we send responses to. - */ - public responseHandler: cf.legacy.node.ResponseSink; - /** - * The underlying state for the entire machine. All state here is a result of - * a completed and commited protocol. - */ - public node: Node; - - constructor(config: InstructionExecutorConfig) { - this.responseHandler = config.responseHandler; - this.node = new Node(config.state || {}, config.network); - this.middleware = new Middleware(this.node); - } - - public dispatchReceivedMessage(msg: cf.legacy.node.ClientActionMessage) { - this.execute( - new ActionExecution(msg.action, FLOWS[msg.action][msg.seq], msg, this) - ); - } - - public runUpdateProtocol( - fromAddress: string, - toAddress: string, - multisigAddress: string, - appInstanceId: string, - encodedAppState: string, - appStateHash: cf.legacy.utils.H256 - ) { - this.execute( - new ActionExecution( - cf.legacy.node.ActionName.UPDATE, - FLOWS[cf.legacy.node.ActionName.UPDATE][0], - { - appInstanceId, - multisigAddress, - fromAddress, - toAddress, - action: cf.legacy.node.ActionName.UPDATE, - data: { - encodedAppState, - appStateHash - }, - seq: 0 - }, - this - ) - ); - } - public runUninstallProtocol( - fromAddress: string, - toAddress: string, - multisigAddress: string, - peerAmounts: cf.legacy.utils.PeerBalance[], - appInstanceId: string - ) { - this.execute( - new ActionExecution( - cf.legacy.node.ActionName.UNINSTALL, - FLOWS[cf.legacy.node.ActionName.UNINSTALL][0], - { - appInstanceId, - multisigAddress, - fromAddress, - toAddress, - action: cf.legacy.node.ActionName.UNINSTALL, - data: { - peerAmounts - }, - seq: 0 - }, - this - ) - ); + constructor(public readonly network) { + this.middleware = new Middleware(); } - public runInstallProtocol( - fromAddress: string, - toAddress: string, - multisigAddress: string, - installData: cf.legacy.app.InstallData - ) { - this.execute( - new ActionExecution( - cf.legacy.node.ActionName.INSTALL, - FLOWS[cf.legacy.node.ActionName.INSTALL][0], - { - multisigAddress, - toAddress, - fromAddress, - action: cf.legacy.node.ActionName.INSTALL, - data: installData, - seq: 0 - }, - this - ) - ); + public register(scope: Opcode, method: InstructionMiddlewareCallback) { + this.middleware.add(scope, method); } - public runSetupProtocol( - fromAddress: string, - toAddress: string, - multisigAddress: string - ) { - this.execute( - new ActionExecution( - cf.legacy.node.ActionName.SETUP, - FLOWS[cf.legacy.node.ActionName.SETUP][0], - { - multisigAddress, - toAddress, - fromAddress, - seq: 0, - action: cf.legacy.node.ActionName.SETUP - }, - this - ) - ); + public async dispatchReceivedMessage(msg: ProtocolMessage, sc: StateChannel) { + const protocol = getProtocolFromName(msg.protocol); + const step = protocol[msg.seq]; + if (step === undefined) { + throw Error( + `Received invalid seq ${msg.seq} for protocol ${msg.protocol}` + ); + } + return this.runProtocol(sc, step, msg); } - public runInstallVirtualAppProtocol( - fromAddress: string, - toAddress: string, - intermediaryAddress: string, - multisigAddress: string - ) { - this.execute( - new ActionExecution( - cf.legacy.node.ActionName.INSTALL_VIRTUAL_APP, - FLOWS[cf.legacy.node.ActionName.INSTALL_VIRTUAL_APP][0], - { - multisigAddress, - toAddress, - fromAddress, - seq: 0, - action: cf.legacy.node.ActionName.INSTALL_VIRTUAL_APP, - data: { - initiating: fromAddress, - responding: toAddress, - intermediary: intermediaryAddress - } - }, - this - ) - ); + public async runUpdateProtocol(sc: StateChannel, params: UpdateData) { + const protocol = Protocol.Update; + return this.runProtocol(sc, getProtocolFromName(protocol)[0], { + params, + protocol, + ...genericProtocolMessageFields(sc) + }); } - public async execute(execution: ActionExecution) { - await this.run(execution); + public async runUninstallProtocol(sc: StateChannel, params: UninstallData) { + const protocol = Protocol.Uninstall; + return this.runProtocol(sc, getProtocolFromName(protocol)[0], { + params, + protocol, + ...genericProtocolMessageFields(sc) + }); } - public async run(execution: ActionExecution) { - const ret = await execution.runAll(); - this.sendResponse(cf.legacy.node.ResponseStatus.COMPLETED); - return ret; + public async runInstallProtocol(sc: StateChannel, params: InstallData) { + const protocol = Protocol.Install; + return this.runProtocol(sc, getProtocolFromName(protocol)[0], { + params, + protocol, + ...genericProtocolMessageFields(sc) + }); } - public sendResponse(status: cf.legacy.node.ResponseStatus) { - this.responseHandler.sendResponse(new cf.legacy.node.Response(status)); + public async runSetupProtocol(sc: StateChannel) { + const protocol = Protocol.Setup; + return this.runProtocol(sc, getProtocolFromName(protocol)[0], { + protocol, + params: {}, + ...genericProtocolMessageFields(sc) + }); } - public mutateState(state: cf.legacy.channel.StateChannelInfos) { - Object.assign(this.node.channelStates, state); + public async runInstallVirtualAppProtocol( + sc: StateChannel, + params: MetaChannelInstallAppData + ) { + const protocol = Protocol.InstallVirtualApp; + return this.runProtocol(sc, getProtocolFromName(protocol)[0], { + params, + protocol, + ...genericProtocolMessageFields(sc) + }); } - public register(scope: Opcode, method: InstructionMiddlewareCallback) { - this.middleware.add(scope, method); + private async runProtocol( + sc: StateChannel, + instructions: Instruction[], + msg: ProtocolMessage + ) { + const context: Context = { + network: this.network, + outbox: [], + inbox: [], + stateChannel: sc, + operation: undefined, + signature: undefined + }; + + let instructionPointer = 0; + + while (instructionPointer < instructions.length) { + const instruction = instructions[instructionPointer]; + try { + if (typeof instruction === "function") { + // TODO: it might be possible to not have to pass in sc + instruction.call(null, msg, context, sc); + } else { + await this.middleware.run(msg, instruction, context); + } + instructionPointer += 1; + } catch (e) { + throw Error( + `While executing op number ${instructionPointer} at seq ${ + msg.seq + } of protocol ${ + msg.protocol + }, execution failed with the following error. ${e.stack}` + ); + } + } + + if (context.stateChannel === undefined) { + throw Error("State transition was computed to be undefined :("); + } + + // TODO: it is possible to compute a diff of the original state channel + // object and the computed new state channel object at this point + // probably useful! + // + // const diff = sc.diff(context.stateChannel) + + return context.stateChannel; } } - -export interface IntermediateResults { - outbox: cf.legacy.node.ClientActionMessage[]; - inbox: cf.legacy.node.ClientActionMessage[]; - proposedStateTransition?: StateProposal; - operation?: ProtocolOperation; - signature?: ethers.utils.Signature; -} - -export interface Context { - intermediateResults: IntermediateResults; - // TODO: @IIIIllllIIIIllllIIIIllllIIIIllllIIIIll the following fields are very special-purpose and only accessed - // in one place; it would be nice to get rid of them - instructionExecutor: InstructionExecutor; -} diff --git a/packages/machine/src/instructions.ts b/packages/machine/src/instructions.ts deleted file mode 100644 index 9a160beca..000000000 --- a/packages/machine/src/instructions.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; - -import { INSTALL_FLOW } from "./flows/install"; -import { INSTALL_VIRTUAL_APP_FLOW } from "./flows/install-virtual-app"; -import { SETUP_FLOW } from "./flows/setup"; -import { UNINSTALL_FLOW } from "./flows/uninstall"; -import { UPDATE_FLOW } from "./flows/update"; - -export const FLOWS = { - [cf.legacy.node.ActionName.UPDATE]: UPDATE_FLOW, - [cf.legacy.node.ActionName.SETUP]: SETUP_FLOW, - [cf.legacy.node.ActionName.INSTALL]: INSTALL_FLOW, - [cf.legacy.node.ActionName.UNINSTALL]: UNINSTALL_FLOW, - [cf.legacy.node.ActionName.INSTALL_VIRTUAL_APP]: INSTALL_VIRTUAL_APP_FLOW -}; diff --git a/packages/machine/src/middleware.ts b/packages/machine/src/middleware.ts new file mode 100644 index 000000000..3b23c4540 --- /dev/null +++ b/packages/machine/src/middleware.ts @@ -0,0 +1,54 @@ +import { Opcode } from "./opcodes"; +import { ProtocolMessage } from "./protocol-types-tbd"; +import { + Context, + InstructionMiddlewareCallback, + InstructionMiddlewares +} from "./types"; + +export class Middleware { + public readonly middlewares: InstructionMiddlewares = { + [Opcode.IO_SEND]: [], + [Opcode.IO_WAIT]: [], + [Opcode.OP_SIGN]: [], + [Opcode.OP_SIGN_VALIDATE]: [], + [Opcode.STATE_TRANSITION_COMMIT]: [], + [Opcode.STATE_TRANSITION_PROPOSE]: [] + }; + + public add(scope: Opcode, method: InstructionMiddlewareCallback) { + this.middlewares[scope].push({ scope, method }); + } + + public async run(msg: ProtocolMessage, opCode: Opcode, context: Context) { + let counter = 0; + const middlewares = this.middlewares; + + async function callback() { + if (counter === middlewares[opCode].length - 1) { + return null; + } + + // This is hacky, prevents next from being called more than once + counter += 1; + + const middleware = middlewares[opCode][counter]; + + if (middleware.scope === opCode) { + return middleware.method(msg, callback, context); + } + + return callback(); + } + + const middleware = this.middlewares[opCode][0]; + + if (middleware === undefined) { + throw Error( + `Attempted to run middleware for opcode ${opCode} but none existed` + ); + } + + return middleware.method(msg, callback, context); + } +} diff --git a/packages/machine/src/middleware/middleware.ts b/packages/machine/src/middleware/middleware.ts deleted file mode 100644 index ed43bcee2..000000000 --- a/packages/machine/src/middleware/middleware.ts +++ /dev/null @@ -1,83 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import { ClientActionMessage } from "@counterfactual/cf.js/src/legacy/node"; -import { Signature } from "ethers/utils"; - -import { Context } from "../instruction-executor"; -import { Node } from "../node"; -import { Opcode } from "../opcodes"; -import { - InstructionMiddlewareCallback, - InstructionMiddlewares, - InternalMessage -} from "../types"; - -/** - * Middleware is the container holding the groups of middleware responsible - * for executing a given instruction in the Counterfactual InstructionExecutor. - */ -export class Middleware { - /** - * Maps instruction to list of middleware that will process the instruction. - */ - public middlewares: InstructionMiddlewares = { - [Opcode.IO_SEND]: [], - [Opcode.IO_WAIT]: [], - [Opcode.OP_SIGN]: [], - [Opcode.OP_SIGN_VALIDATE]: [], - [Opcode.STATE_TRANSITION_COMMIT]: [ - { - scope: Opcode.STATE_TRANSITION_COMMIT, - method: (message, next, context) => { - const newState = context.intermediateResults.proposedStateTransition!; - context.instructionExecutor.mutateState(newState.state); - next(); - } - } - ], - [Opcode.STATE_TRANSITION_PROPOSE]: [] - }; - - constructor(readonly node: Node) {} - - public add(scope: Opcode, method: InstructionMiddlewareCallback) { - this.middlewares[scope].push({ scope, method }); - } - - public async run(msg: InternalMessage, context: Context) { - let counter = 0; - const middlewares = this.middlewares; - const opCode = msg.opCode; - - async function callback() { - if (counter === middlewares[opCode].length - 1) { - return null; - } - // This is hacky, prevents next from being called more than once - counter += 1; - const middleware = middlewares[opCode][counter]; - if (middleware.scope === opCode) { - return middleware.method(msg, callback, context); - } - return callback(); - } - return this.middlewares[opCode][0].method(msg, callback, context); - } -} - -export class NextMsgGenerator { - public static generate2( - message: cf.legacy.node.ClientActionMessage, - signature: Signature - ): ClientActionMessage { - return { - signature, - appInstanceId: message.appInstanceId, - action: message.action, - data: message.data, - multisigAddress: message.multisigAddress, - toAddress: message.toAddress, - fromAddress: message.fromAddress, - seq: message.seq + 1 - }; - } -} diff --git a/packages/machine/src/middleware/protocol-operation/common.ts b/packages/machine/src/middleware/protocol-operation/common.ts deleted file mode 100644 index 603d08e4d..000000000 --- a/packages/machine/src/middleware/protocol-operation/common.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import AppInstanceJson from "@counterfactual/contracts/build/contracts/AppInstance.json"; -import RegistryJson from "@counterfactual/contracts/build/contracts/Registry.json"; -import { ethers } from "ethers"; - -/** - * Returns the calldata for a call to `registry` that looks up `appCfAddr` and - * calls the resulting address's `setState` function with the provided `appStateHash`, - * `appLocalNonce`, `timeout` and `signatures` - */ -export function proxyCallSetStateData( - networkContext: cf.legacy.network.NetworkContext, - appStateHash: cf.legacy.utils.H256, - appCfAddr: cf.legacy.utils.H256, - appLocalNonce: number, - timeout: number, - signatures: cf.legacy.utils.Bytes -): cf.legacy.utils.Bytes { - return new ethers.utils.Interface( - RegistryJson.abi - ).functions.proxyCall.encode([ - networkContext.registryAddr, - appCfAddr, - new ethers.utils.Interface(AppInstanceJson.abi).functions.setState.encode([ - appStateHash, - appLocalNonce, - timeout, - signatures - ]) - ]); -} diff --git a/packages/machine/src/middleware/protocol-operation/index.ts b/packages/machine/src/middleware/protocol-operation/index.ts index 20c3bfc44..10946af4a 100644 --- a/packages/machine/src/middleware/protocol-operation/index.ts +++ b/packages/machine/src/middleware/protocol-operation/index.ts @@ -1,7 +1,11 @@ -import { EthOpGenerator } from "./op-generator"; -import { OpInstall } from "./op-install"; -import { OpSetState } from "./op-set-state"; -import { OpSetup } from "./op-setup"; -import { OpUninstall } from "./op-uninstall"; +import { InstallCommitment } from "./op-install"; +import { SetStateCommitment } from "./op-set-state"; +import { SetupCommitment } from "./op-setup"; +import { UninstallCommitment } from "./op-uninstall"; -export { OpInstall, OpSetState, OpSetup, OpUninstall, EthOpGenerator }; +export { + InstallCommitment, + SetStateCommitment, + SetupCommitment, + UninstallCommitment +}; diff --git a/packages/machine/src/middleware/protocol-operation/multi-send-op.ts b/packages/machine/src/middleware/protocol-operation/multi-send-op.ts deleted file mode 100644 index fb41673d4..000000000 --- a/packages/machine/src/middleware/protocol-operation/multi-send-op.ts +++ /dev/null @@ -1,95 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import NonceRegistryJson from "@counterfactual/contracts/build/contracts/NonceRegistry.json"; -import { ethers } from "ethers"; - -import * as common from "./common"; -import { MultisigTxOp } from "./multisig-tx-op"; -import { MultiSend, MultisigInput, Operation } from "./types"; - -const { keccak256 } = ethers.utils; - -export abstract class MultiSendOp extends MultisigTxOp { - constructor( - readonly networkContext: cf.legacy.network.NetworkContext, - readonly multisig: cf.legacy.utils.Address, - readonly freeBalance: cf.legacy.utils.FreeBalance, - readonly dependencyNonce: cf.legacy.utils.Nonce - ) { - super(multisig, freeBalance); - } - - public freeBalanceInput(): MultisigInput { - const to = this.networkContext.registryAddr; - const val = 0; - const data = this.freeBalanceData(); - const op = Operation.Delegatecall; - return new MultisigInput(to, val, data, op); - } - - public freeBalanceData(): cf.legacy.utils.Bytes { - const terms = cf.legacy.utils.FreeBalance.terms(); - const app = cf.legacy.utils.FreeBalance.contractInterface( - this.networkContext - ); - const freeBalanceCfAddress = new cf.legacy.app.AppInstance( - this.networkContext, - this.multisig, - [this.freeBalance.alice, this.freeBalance.bob], - app, - terms, - this.freeBalance.timeout, - this.freeBalance.uniqueId - ).cfAddress(); - - const appStateHash = keccak256( - cf.utils.abi.encode( - ["address", "address", "uint256", "uint256"], - [ - this.freeBalance.alice, - this.freeBalance.bob, - this.freeBalance.aliceBalance, - this.freeBalance.bobBalance - ] - ) - ); - // don't need signatures since the multisig is the owner - const signatures = "0x0"; - return common.proxyCallSetStateData( - this.networkContext, - appStateHash, - freeBalanceCfAddress, - this.freeBalance.localNonce, - this.freeBalance.timeout, - signatures - ); - } - - public dependencyNonceInput(): MultisigInput { - // FIXME: new NonceRegistryJson design will obviate timeout - // https://github.com/counterfactual/monorepo/issues/122 - const timeout = 0; - const to = this.networkContext.nonceRegistryAddr; - const val = 0; - const data = new ethers.utils.Interface( - NonceRegistryJson.abi - ).functions.setNonce.encode([ - timeout, - this.dependencyNonce.salt, - this.dependencyNonce.nonceValue - ]); - const op = Operation.Call; - return new MultisigInput(to, val, data, op); - } - - public abstract eachMultisigInput(): MultisigInput[]; - - /** - * @returns the input for the transaction from the multisig that will trigger - * a multisend transaction. - */ - multisigInput(): MultisigInput { - return new MultiSend(this.eachMultisigInput(), this.networkContext).input( - this.networkContext.multiSendAddr - ); - } -} diff --git a/packages/machine/src/middleware/protocol-operation/multisig-tx-op.ts b/packages/machine/src/middleware/protocol-operation/multisig-tx-op.ts deleted file mode 100644 index 5a3fce1ec..000000000 --- a/packages/machine/src/middleware/protocol-operation/multisig-tx-op.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import MinimumViableMultisigJson from "@counterfactual/contracts/build/contracts/MinimumViableMultisig.json"; -import { ethers } from "ethers"; - -import { MultisigInput, ProtocolOperation, Transaction } from "./types"; - -const { keccak256 } = ethers.utils; -const { abi } = cf.utils; - -export abstract class MultisigTxOp extends ProtocolOperation { - abstract multisigInput(): MultisigInput; - - constructor( - readonly multisig: cf.legacy.utils.Address, - readonly freeBalance: cf.legacy.utils.FreeBalance - ) { - super(); - } - - public transaction(sigs: ethers.utils.Signature[]): Transaction { - const multisigInput = this.multisigInput(); - const signatureBytes = cf.utils.signaturesToSortedBytes( - this.hashToSign(), - ...sigs - ); - const txData = new ethers.utils.Interface( - MinimumViableMultisigJson.abi - ).functions.execTransaction.encode([ - multisigInput.to, - multisigInput.val, - multisigInput.data, - multisigInput.op, - signatureBytes - ]); - return new Transaction(this.multisig, 0, txData); - } - - public hashToSign(): string { - const multisigInput = this.multisigInput(); - const owners = [this.freeBalance.alice, this.freeBalance.bob]; - return keccak256( - abi.encodePacked( - ["bytes1", "address[]", "address", "uint256", "bytes", "uint8"], - [ - "0x19", - owners, - multisigInput.to, - multisigInput.val, - multisigInput.data, - multisigInput.op - ] - ) - ); - } -} diff --git a/packages/machine/src/middleware/protocol-operation/op-generator.ts b/packages/machine/src/middleware/protocol-operation/op-generator.ts deleted file mode 100644 index 123b61fd6..000000000 --- a/packages/machine/src/middleware/protocol-operation/op-generator.ts +++ /dev/null @@ -1,224 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import { ethers } from "ethers"; - -import { Context } from "../../instruction-executor"; -import { Node } from "../../node"; -import { InternalMessage } from "../../types"; - -import { OpInstall } from "./op-install"; -import { OpSetState } from "./op-set-state"; -import { OpSetup } from "./op-setup"; -import { OpUninstall } from "./op-uninstall"; -import { ProtocolOperation } from "./types"; - -export class EthOpGenerator { - public static generate( - message: InternalMessage, - next: Function, - context: Context, - node: Node - ): ProtocolOperation { - const proposedState = context.intermediateResults.proposedStateTransition!; - let op; - if (message.actionName === cf.legacy.node.ActionName.UPDATE) { - op = this.update(message, context, node, proposedState.state); - } else if (message.actionName === cf.legacy.node.ActionName.INSTALL) { - op = this.install( - message, - context, - node, - proposedState.state, - proposedState.cfAddr! - ); - } else if (message.actionName === cf.legacy.node.ActionName.UNINSTALL) { - op = this.uninstall(message, context, node, proposedState.state); - } - return op; - } - - public static update( - message: InternalMessage, - context: Context, - node: Node, - proposedUpdate: any - ): ProtocolOperation { - const multisig: cf.legacy.utils.Address = - message.clientMessage.multisigAddress; - if (message.clientMessage.appInstanceId === undefined) { - // FIXME: handle more gracefully - // https://github.com/counterfactual/monorepo/issues/121 - throw Error("update message must have appId set"); - } - const appChannel = - proposedUpdate[multisig].appInstances[ - message.clientMessage.appInstanceId - ]; - - // TODO: ensure these members are typed instead of having to reconstruct - // class instances - // https://github.com/counterfactual/monorepo/issues/135 - appChannel.cfApp = new cf.legacy.app.AppInterface( - appChannel.cfApp.address, - appChannel.cfApp.applyAction, - appChannel.cfApp.resolve, - appChannel.cfApp.getTurnTaker, - appChannel.cfApp.isStateTerminal, - appChannel.cfApp.abiEncoding - ); - - appChannel.terms = new cf.legacy.app.Terms( - appChannel.terms.assetType, - appChannel.terms.limit, - appChannel.terms.token - ); - - const signingKeys = [ - message.clientMessage.fromAddress, - message.clientMessage.toAddress - ]; - signingKeys.sort( - (addrA: cf.legacy.utils.Address, addrB: cf.legacy.utils.Address) => { - return new ethers.utils.BigNumber(addrA).lt(addrB) ? -1 : 1; - } - ); - - return new OpSetState( - node.networkContext, - multisig, - // FIXME: signing keys should be app-specific ephemeral keys - // https://github.com/counterfactual/monorepo/issues/120 - signingKeys, - appChannel.appStateHash, - appChannel.uniqueId, - appChannel.terms, - appChannel.cfApp, - appChannel.localNonce, - appChannel.timeout - ); - } - - public static setup( - message: InternalMessage, - node: Node, - proposedSetup: any - ): ProtocolOperation { - const multisig: cf.legacy.utils.Address = - message.clientMessage.multisigAddress; - const freeBalance: cf.legacy.utils.FreeBalance = - proposedSetup[multisig].freeBalance; - const nonce = freeBalance.dependencyNonce; - const newFreeBalance = new cf.legacy.utils.FreeBalance( - freeBalance.alice, - freeBalance.aliceBalance, - freeBalance.bob, - freeBalance.bobBalance, - freeBalance.uniqueId, - freeBalance.localNonce, - freeBalance.timeout, - freeBalance.dependencyNonce - ); - const canon = cf.legacy.utils.CanonicalPeerBalance.canonicalize( - new cf.legacy.utils.PeerBalance(message.clientMessage.fromAddress, 0), - new cf.legacy.utils.PeerBalance(message.clientMessage.toAddress, 0) - ); - const signingKeys = [canon.peerA.address, canon.peerB.address]; - const freeBalanceAppInstance = new cf.legacy.app.AppInstance( - node.networkContext, - multisig, - signingKeys, - cf.legacy.utils.FreeBalance.contractInterface(node.networkContext), - cf.legacy.utils.FreeBalance.terms(), - freeBalance.timeout, - freeBalance.uniqueId - ); - - return new OpSetup( - node.networkContext, - multisig, - freeBalanceAppInstance, - newFreeBalance, - nonce - ); - } - - public static install( - message: InternalMessage, - context: Context, - node: Node, - proposedInstall: cf.legacy.channel.StateChannelInfos, - cfAddr: cf.legacy.utils.H256 - ) { - const channel = proposedInstall[message.clientMessage.multisigAddress]; - const freeBalance = channel.freeBalance; - const multisig: cf.legacy.utils.Address = - message.clientMessage.multisigAddress; - const appChannel = channel.appInstances[cfAddr]; - - const signingKeys = [appChannel.keyA!, appChannel.keyB!]; - - const app = new cf.legacy.app.AppInstance( - node.networkContext, - multisig, - signingKeys, - appChannel.cfApp, - appChannel.terms, - appChannel.timeout, - appChannel.uniqueId - ); - const newFreeBalance = new cf.legacy.utils.FreeBalance( - freeBalance.alice, - freeBalance.aliceBalance, - freeBalance.bob, - freeBalance.bobBalance, - freeBalance.uniqueId, - freeBalance.localNonce, - freeBalance.timeout, - freeBalance.dependencyNonce - ); - - const op = new OpInstall( - node.networkContext, - multisig, - app, - newFreeBalance, - appChannel.dependencyNonce - ); - return op; - } - - public static uninstall( - message: InternalMessage, - context: Context, - node: Node, - proposedUninstall: any - ): ProtocolOperation { - const multisig: cf.legacy.utils.Address = - message.clientMessage.multisigAddress; - const cfAddr = message.clientMessage.appInstanceId; - if (cfAddr === undefined) { - throw new Error("update message must have appId set"); - } - - const freeBalance = proposedUninstall[multisig].freeBalance; - const appChannel = proposedUninstall[multisig].appInstances[cfAddr]; - - const newFreeBalance = new cf.legacy.utils.FreeBalance( - freeBalance.alice, - freeBalance.aliceBalance, - freeBalance.bob, - freeBalance.bobBalance, - freeBalance.uniqueId, - freeBalance.localNonce, - freeBalance.timeout, - freeBalance.dependencyNonce - ); - - const op = new OpUninstall( - node.networkContext, - multisig, - newFreeBalance, - appChannel.dependencyNonce - ); - return op; - } -} diff --git a/packages/machine/src/middleware/protocol-operation/op-install.ts b/packages/machine/src/middleware/protocol-operation/op-install.ts index 1d0107ed8..4e157c7f2 100644 --- a/packages/machine/src/middleware/protocol-operation/op-install.ts +++ b/packages/machine/src/middleware/protocol-operation/op-install.ts @@ -1,58 +1,75 @@ -import * as cf from "@counterfactual/cf.js"; -import ConditionalTransactionJson from "@counterfactual/contracts/build/contracts/ConditionalTransaction.json"; -import { ethers } from "ethers"; +import StateChannelTransaction from "@counterfactual/contracts/build/contracts/StateChannelTransaction.json"; +import { AppIdentity, NetworkContext, Terms } from "@counterfactual/types"; +import { + defaultAbiCoder, + Interface, + keccak256, + solidityPack +} from "ethers/utils"; -import { MultiSendOp } from "./multi-send-op"; -import { MultisigInput, Operation } from "./types"; +import { DependencyValue } from "../../models/app-instance"; -const { keccak256 } = ethers.utils; -const { abi } = cf.utils; +import { MultisigOperation, MultisigTransaction } from "./types"; +import { APP_IDENTITY } from "./utils/encodings"; +import { MultiSendCommitment } from "./utils/multi-send-op"; -export class OpInstall extends MultiSendOp { +const iface = new Interface(StateChannelTransaction.abi); + +export class InstallCommitment extends MultiSendCommitment { constructor( - readonly networkContext: cf.legacy.network.NetworkContext, - readonly multisig: cf.legacy.utils.Address, - readonly app: cf.legacy.app.AppInstance, - readonly freeBalance: cf.legacy.utils.FreeBalance, - readonly dependencyNonce: cf.legacy.utils.Nonce + public readonly networkContext: NetworkContext, + public readonly multisig: string, + public readonly multisigOwners: string[], + public readonly appIdentity: AppIdentity, + public readonly terms: Terms, + public readonly freeBalanceAppIdentity: AppIdentity, + public readonly freeBalanceTerms: Terms, + public readonly freeBalanceStateHash: string, + public readonly freeBalanceNonce: number, + public readonly freeBalanceTimeout: number, + public readonly dependencyNonce: number ) { - super(networkContext, multisig, freeBalance, dependencyNonce); + super( + networkContext, + multisig, + multisigOwners, + freeBalanceAppIdentity, + freeBalanceTerms, + freeBalanceStateHash, + freeBalanceNonce, + freeBalanceTimeout, + keccak256(defaultAbiCoder.encode(["uint256"], [dependencyNonce])), + DependencyValue.NOT_UNINSTALLED + ); } - /** - * @override common.MultiSendOp - */ - public eachMultisigInput(): MultisigInput[] { + public eachMultisigInput() { return [this.freeBalanceInput(), this.conditionalTransactionInput()]; } - private conditionalTransactionInput(): MultisigInput { - const to = this.networkContext.conditionalTransactionAddr; - const val = 0; + private conditionalTransactionInput(): MultisigTransaction { const uninstallKey = keccak256( - abi.encodePacked( + solidityPack( ["address", "uint256", "uint256"], - [this.multisig, 0, this.dependencyNonce.salt] + [this.multisig, 0, this.dependencyNonceSalt] ) ); - const data = new ethers.utils.Interface( - ConditionalTransactionJson.abi - ).functions.executeAppConditionalTransaction.encode([ - this.networkContext.registryAddr, - this.networkContext.nonceRegistryAddr, - uninstallKey, - this.appCfAddress, - { - assetType: this.app.terms.assetType, - limit: this.app.terms.limit, - token: this.app.terms.token - } - ]); - const op = Operation.Delegatecall; - return new MultisigInput(to, val, data, op); - } - get appCfAddress(): cf.legacy.utils.H256 { - return this.app.cfAddress(); + const appInstanceId = keccak256( + defaultAbiCoder.encode([APP_IDENTITY], [this.appIdentity]) + ); + + return { + to: this.networkContext.StateChannelTransaction, + value: 0, + data: iface.functions.executeAppConditionalTransaction.encode([ + this.networkContext.AppRegistry, + this.networkContext.NonceRegistry, + uninstallKey, + appInstanceId, + this.terms + ]), + operation: MultisigOperation.DelegateCall + }; } } diff --git a/packages/machine/src/middleware/protocol-operation/op-set-state.ts b/packages/machine/src/middleware/protocol-operation/op-set-state.ts index ae7276ce1..d9ca1bcca 100644 --- a/packages/machine/src/middleware/protocol-operation/op-set-state.ts +++ b/packages/machine/src/middleware/protocol-operation/op-set-state.ts @@ -1,66 +1,62 @@ -import * as cf from "@counterfactual/cf.js"; -import { ethers } from "ethers"; +import AppRegistry from "@counterfactual/contracts/build/contracts/AppRegistry.json"; +import { + AppIdentity, + NetworkContext, + SignedStateHashUpdate +} from "@counterfactual/types"; +import { Interface, keccak256, Signature, solidityPack } from "ethers/utils"; -import * as common from "./common"; -import { ProtocolOperation, Transaction } from "./types"; +import { EthereumCommitment, Transaction } from "./types"; +import { appIdentityToHash } from "./utils/app-identity"; +import { signaturesToSortedBytes } from "./utils/signature"; -const { keccak256 } = ethers.utils; -const { abi } = cf.utils; +const iface = new Interface(AppRegistry.abi); -export class OpSetState extends ProtocolOperation { +export class SetStateCommitment extends EthereumCommitment { constructor( - readonly ctx: cf.legacy.network.NetworkContext, - readonly multisig: cf.legacy.utils.Address, - readonly signingKeys: cf.legacy.utils.Address[], - readonly appStateHash: string, - readonly appUniqueId: number, - readonly terms: cf.legacy.app.Terms, - readonly app: cf.legacy.app.AppInterface, - readonly appLocalNonce: number, - readonly timeout: number + public readonly networkContext: NetworkContext, + public readonly appIdentity: AppIdentity, + public readonly encodedAppState: string, + public readonly appLocalNonce: number, + public readonly timeout: number ) { super(); } public hashToSign(): string { return keccak256( - abi.encodePacked( - ["bytes1", "address[]", "uint256", "uint256", "bytes32"], + solidityPack( + ["bytes1", "bytes32", "uint256", "uint256", "bytes32"], [ "0x19", - this.signingKeys, + appIdentityToHash(this.appIdentity), this.appLocalNonce, this.timeout, - this.appStateHash + keccak256(this.encodedAppState) ] ) ); } - /** - * @returns a tx that executes a proxyCall through the registry to call - * `setState` on AppInstance.sol. - */ - public transaction(sigs: ethers.utils.Signature[]): Transaction { - const appCfAddr = new cf.legacy.app.AppInstance( - this.ctx, - this.multisig, - this.signingKeys, - this.app, - this.terms, - this.timeout, - this.appUniqueId - ).cfAddress(); - const to = this.ctx.registryAddr; - const val = 0; - const data = common.proxyCallSetStateData( - this.ctx, - this.appStateHash, - appCfAddr, - this.appLocalNonce, - this.timeout, - cf.utils.signaturesToSortedBytes(this.hashToSign(), ...sigs) - ); - return new Transaction(to, val, data); + public transaction(sigs: Signature[]): Transaction { + return { + to: this.networkContext.AppRegistry, + value: 0, + data: iface.functions.setState.encode([ + this.appIdentity, + this.getSignedStateHashUpdate(sigs) + ]) + }; + } + + private getSignedStateHashUpdate( + signatures: Signature[] + ): SignedStateHashUpdate { + return { + stateHash: keccak256(this.encodedAppState), + nonce: this.appLocalNonce, + timeout: this.timeout, + signatures: signaturesToSortedBytes(this.hashToSign(), ...signatures) + }; } } diff --git a/packages/machine/src/middleware/protocol-operation/op-setup.ts b/packages/machine/src/middleware/protocol-operation/op-setup.ts index 1a41ca1ce..897521400 100644 --- a/packages/machine/src/middleware/protocol-operation/op-setup.ts +++ b/packages/machine/src/middleware/protocol-operation/op-setup.ts @@ -1,52 +1,63 @@ -import * as cf from "@counterfactual/cf.js"; -import ConditionalTransactionJson from "@counterfactual/contracts/build/contracts/ConditionalTransaction.json"; -import { ethers } from "ethers"; +import StateChannelTransaction from "@counterfactual/contracts/build/contracts/StateChannelTransaction.json"; +import { AppIdentity, NetworkContext, Terms } from "@counterfactual/types"; +import { + defaultAbiCoder, + Interface, + keccak256, + solidityPack +} from "ethers/utils"; -import { MultisigTxOp } from "./multisig-tx-op"; -import { MultisigInput, Operation } from "./types"; +import { DependencyValue } from "../../models/app-instance"; -const { keccak256 } = ethers.utils; -const { abi } = cf.utils; +import { MultisigOperation, MultisigTransaction } from "./types"; +import { appIdentityToHash } from "./utils/app-identity"; +import { MultisigTransactionCommitment } from "./utils/multisig-tx-op"; -export class OpSetup extends MultisigTxOp { +const iface = new Interface(StateChannelTransaction.abi); + +export class SetupCommitment extends MultisigTransactionCommitment { public constructor( - readonly networkContext: cf.legacy.network.NetworkContext, - readonly multisig: cf.legacy.utils.Address, - readonly freeBalanceStateChannel: cf.legacy.app.AppInstance, - readonly freeBalance: cf.legacy.utils.FreeBalance, - readonly dependencyNonce: cf.legacy.utils.Nonce + public readonly networkContext: NetworkContext, + public readonly multisigAddress: string, + public readonly multisigOwners: string[], + public readonly freeBalanceAppIdentity: AppIdentity, + public readonly freeBalanceTerms: Terms ) { - super(multisig, freeBalance); - if (dependencyNonce === undefined) { - throw new Error("Undefined dependency nonce"); - } + super(multisigAddress, multisigOwners); } - multisigInput(): MultisigInput { - const terms = cf.legacy.utils.FreeBalance.terms(); + public getTransactionDetails(): MultisigTransaction { + return { + to: this.networkContext.StateChannelTransaction, + value: 0, + data: iface.functions.executeAppConditionalTransaction.encode([ + this.networkContext.AppRegistry, + this.networkContext.NonceRegistry, + this.getUninstallKeyForNonceRegistry(), + appIdentityToHash(this.freeBalanceAppIdentity), + this.freeBalanceTerms + ]), + operation: MultisigOperation.DelegateCall + }; + } - const uninstallKey = keccak256( - abi.encodePacked( - ["address", "uint256", "uint256"], - [this.multisig, 0, this.dependencyNonce.salt] + private getUninstallKeyForNonceRegistry() { + return keccak256( + solidityPack( + ["address", "uint256", "bytes32"], + [ + this.multisigAddress, + // The timeout is hard-coded to be 0 as is defined by the protocol + 0, + this.getSaltForDependencyNonce() + ] ) ); + } - const multisigCalldata = new ethers.utils.Interface( - ConditionalTransactionJson.abi - ).functions.executeAppConditionalTransaction.encode([ - this.networkContext.registryAddr, - this.networkContext.nonceRegistryAddr, - uninstallKey, - this.freeBalanceStateChannel.cfAddress(), - [terms.assetType, terms.limit, terms.token] - ]); - - return new MultisigInput( - this.networkContext.conditionalTransactionAddr, - 0, - multisigCalldata, - Operation.Delegatecall + private getSaltForDependencyNonce() { + return keccak256( + defaultAbiCoder.encode(["uint256"], [DependencyValue.NOT_UNINSTALLED]) ); } } diff --git a/packages/machine/src/middleware/protocol-operation/op-uninstall.ts b/packages/machine/src/middleware/protocol-operation/op-uninstall.ts index a9ba59728..6f7a74ff9 100644 --- a/packages/machine/src/middleware/protocol-operation/op-uninstall.ts +++ b/packages/machine/src/middleware/protocol-operation/op-uninstall.ts @@ -1,22 +1,63 @@ -import * as cf from "@counterfactual/cf.js"; +import NonceRegistry from "@counterfactual/contracts/build/contracts/NonceRegistry.json"; +import { + AppIdentity, + ETHBucketAppState, + NetworkContext, + Terms +} from "@counterfactual/types"; +import { defaultAbiCoder, Interface, keccak256 } from "ethers/utils"; -import { MultiSendOp } from "./multi-send-op"; -import { MultisigInput } from "./types"; +import { DependencyValue } from "../../models/app-instance"; -export class OpUninstall extends MultiSendOp { +import { MultisigOperation, MultisigTransaction } from "./types"; +import { encodeFreeBalanceState } from "./utils/free-balance"; +import { MultiSendCommitment } from "./utils/multi-send-op"; + +const nonceRegistryIface = new Interface(NonceRegistry.abi); + +export class UninstallCommitment extends MultiSendCommitment { constructor( - readonly networkContext: cf.legacy.network.NetworkContext, - readonly multisig: cf.legacy.utils.Address, - readonly freeBalance: cf.legacy.utils.FreeBalance, - readonly dependencyNonce: cf.legacy.utils.Nonce + public readonly networkContext: NetworkContext, + public readonly multisig: string, + public readonly multisigOwners: string[], + public readonly freeBalanceAppIdentity: AppIdentity, + public readonly freeBalanceTerms: Terms, + public readonly freeBalanceState: ETHBucketAppState, + public readonly freeBalanceNonce: number, + public readonly freeBalanceTimeout: number, + public readonly dependencyNonce: number ) { - super(networkContext, multisig, freeBalance, dependencyNonce); + super( + networkContext, + multisig, + multisigOwners, + freeBalanceAppIdentity, + freeBalanceTerms, + keccak256(encodeFreeBalanceState(freeBalanceState)), + freeBalanceNonce, + freeBalanceTimeout, + keccak256(defaultAbiCoder.encode(["uint256"], [dependencyNonce])), + // Hard coded the update to 1 because that is the value + // that represents an app as being "uninstalled" + DependencyValue.UNINSTALLED + ); + } + + // TODO: I am suspicious of this + public dependencyNonceInput(): MultisigTransaction { + return { + to: this.networkContext.NonceRegistry, + value: 0, + data: nonceRegistryIface.functions.setNonce.encode([ + 0, // Timeout is 0 for dependencyNonce! + this.dependencyNonceSalt, + this.dependencyNonceValue + ]), + operation: MultisigOperation.Call + }; } - /** - * @override common.MultiSendOp - */ - public eachMultisigInput(): MultisigInput[] { + public eachMultisigInput() { return [this.freeBalanceInput(), this.dependencyNonceInput()]; } } diff --git a/packages/machine/src/middleware/protocol-operation/types.ts b/packages/machine/src/middleware/protocol-operation/types.ts index 4b84ace74..942f57a3b 100644 --- a/packages/machine/src/middleware/protocol-operation/types.ts +++ b/packages/machine/src/middleware/protocol-operation/types.ts @@ -1,69 +1,30 @@ -import * as cf from "@counterfactual/cf.js"; -import MultiSendJson from "@counterfactual/contracts/build/contracts/MultiSend.json"; -import { ethers } from "ethers"; +import { Signature } from "ethers/utils"; -export abstract class ProtocolOperation { - public abstract hashToSign(): cf.legacy.utils.H256; - - public abstract transaction(sigs: ethers.utils.Signature[]): Transaction; +export abstract class EthereumCommitment { + public abstract hashToSign(): string; + public abstract transaction(sigs: Signature[]): Transaction; } -const { abi } = cf.utils; - -export enum Operation { +export enum MultisigOperation { Call = 0, - Delegatecall = 1 + DelegateCall = 1, + // Gnosis Safe uses "2" for CREATE, but we don't actually + // make use of it in our code. Still, I put this here to be + // maximally explicit that we based the data structure on + // Gnosis's implementation of a Multisig + Create = 2 } -export class Transaction { - constructor( - readonly to: cf.legacy.utils.Address, - readonly value: number, - readonly data: string - ) {} -} +export type Transaction = { + to: string; + value: number; + data: string; +}; -export class MultisigTransaction extends Transaction { - constructor( - readonly to: cf.legacy.utils.Address, - readonly value: number, - readonly data: cf.legacy.utils.Bytes, - readonly operation: Operation - ) { - super(to, value, data); - } -} +export type MultisigTransaction = Transaction & { + operation: MultisigOperation; +}; -export class MultisigInput { - constructor( - readonly to: cf.legacy.utils.Address, - readonly val: number, - readonly data: cf.legacy.utils.Bytes, - readonly op: Operation, - readonly signatures?: ethers.utils.Signature[] - ) {} -} - -export class MultiSend { - constructor( - readonly transactions: MultisigInput[], - readonly networkContext: cf.legacy.network.NetworkContext - ) {} - - public input(multisend: cf.legacy.utils.Address): MultisigInput { - let txs: string = "0x"; - for (const transaction of this.transactions) { - txs += abi - .encode( - ["uint256", "address", "uint256", "bytes"], - [transaction.op, transaction.to, transaction.val, transaction.data] - ) - .substr(2); - } - - const data = new ethers.utils.Interface( - MultiSendJson.abi - ).functions.multiSend.encode([txs]); - return new MultisigInput(multisend, 0, data, Operation.Delegatecall); - } -} +export type ExecTransactionCalldata = MultisigTransaction & { + signatures: Signature[]; +}; diff --git a/packages/machine/src/middleware/protocol-operation/utils/app-identity.ts b/packages/machine/src/middleware/protocol-operation/utils/app-identity.ts new file mode 100644 index 000000000..c2f7812d4 --- /dev/null +++ b/packages/machine/src/middleware/protocol-operation/utils/app-identity.ts @@ -0,0 +1,8 @@ +import { AppIdentity } from "@counterfactual/types"; +import { defaultAbiCoder, keccak256 } from "ethers/utils"; + +import { APP_IDENTITY } from "./encodings"; + +export function appIdentityToHash(appIdentity: AppIdentity) { + return keccak256(defaultAbiCoder.encode([APP_IDENTITY], [appIdentity])); +} diff --git a/packages/machine/src/middleware/protocol-operation/utils/encodings.ts b/packages/machine/src/middleware/protocol-operation/utils/encodings.ts new file mode 100644 index 000000000..008955cc1 --- /dev/null +++ b/packages/machine/src/middleware/protocol-operation/utils/encodings.ts @@ -0,0 +1,35 @@ +// NOTE: It is important that the strings end with a comma and not a semicolon, +// these are not struct declarations but simply multi-line tuple encodings. + +export const TERMS = ` + tuple( + uint8 assetType, + uint256 limit, + address token + )`; + +export const APP_INTERFACE = ` + tuple( + address addr, + bytes4 getTurnTaker, + bytes4 applyAction, + bytes4 resolve, + bytes4 isStateTerminal + )`; + +export const APP_IDENTITY = ` + tuple( + address owner, + address[] signingKeys, + bytes32 appInterfaceHash, + bytes32 termsHash, + uint256 defaultTimeout + )`; + +export const SIGNED_STATE_HASH_UPDATE = ` + tuple( + bytes32 stateHash, + uint256 nonce, + uint256 timeout, + bytes signatures + )`; diff --git a/packages/machine/src/middleware/protocol-operation/utils/free-balance.ts b/packages/machine/src/middleware/protocol-operation/utils/free-balance.ts new file mode 100644 index 000000000..fe35f93c2 --- /dev/null +++ b/packages/machine/src/middleware/protocol-operation/utils/free-balance.ts @@ -0,0 +1,69 @@ +import ETHBucket from "@counterfactual/contracts/build/contracts/ETHBucket.json"; +import { AppInterface, ETHBucketAppState } from "@counterfactual/types"; +import { AddressZero, MaxUint256 } from "ethers/constants"; +import { + defaultAbiCoder, + // formatParamType, + Interface, + keccak256 +} from "ethers/utils"; + +import { APP_INTERFACE, TERMS } from "./encodings"; + +// FIXME: Use this when it returns named version. +// export const freeBalanceStateEncoding = formatParamType( +// new Interface(ETHBucket.abi).functions.resolve.inputs[0] +// ); +export const freeBalanceStateEncoding = ` + tuple( + address alice, + address bob, + uint256 aliceBalance, + uint256 bobBalance + ) +`; + +export function getFreeBalanceAppInterface(addr: string): AppInterface { + return { + addr, + resolve: new Interface(ETHBucket.abi).functions.resolve.sighash, + // NOTE: The following methods are always 0x00000000 because the + // ETHBucketApp has no notion of state transitions. Every state + // update is a 2-of-2 signed update of each persons' balance + getTurnTaker: "0x00000000", + isStateTerminal: "0x00000000", + applyAction: "0x00000000", + stateEncoding: freeBalanceStateEncoding, + actionEncoding: undefined // because no actions exist for ETHBucket + }; +} + +export function getFreeBalanceAppInterfaceHash(ethBucketAppAddress: string) { + return keccak256( + defaultAbiCoder.encode( + [APP_INTERFACE], + [getFreeBalanceAppInterface(ethBucketAppAddress)] + ) + ); +} + +export const freeBalanceTerms = { + assetType: 0, + limit: MaxUint256, + token: AddressZero +}; + +export const freeBalanceTermsHash = keccak256( + defaultAbiCoder.encode([TERMS], [freeBalanceTerms]) +); + +export function encodeFreeBalanceState(state: ETHBucketAppState) { + return defaultAbiCoder.encode( + [freeBalanceStateEncoding], + // NOTE: We will be able to replace the following line with [state] after + // @ricmoo implements the feature to add tuple names to the result of + // formatParamType. See: github.com/ethers-io/ethers.js/issues/325 + // [[state.alice, state.bob, state.aliceBalance, state.bobBalance]] + [state] + ); +} diff --git a/packages/machine/src/middleware/protocol-operation/utils/index.ts b/packages/machine/src/middleware/protocol-operation/utils/index.ts new file mode 100644 index 000000000..30699f31b --- /dev/null +++ b/packages/machine/src/middleware/protocol-operation/utils/index.ts @@ -0,0 +1,3 @@ +import { EthereumCommitment } from "../types"; + +export { EthereumCommitment }; diff --git a/packages/machine/src/middleware/protocol-operation/utils/multi-send-op.ts b/packages/machine/src/middleware/protocol-operation/utils/multi-send-op.ts new file mode 100644 index 000000000..b83f52b04 --- /dev/null +++ b/packages/machine/src/middleware/protocol-operation/utils/multi-send-op.ts @@ -0,0 +1,61 @@ +import AppRegistry from "@counterfactual/contracts/build/contracts/AppRegistry.json"; +import MultiSend from "@counterfactual/contracts/build/contracts/MultiSend.json"; +import { AppIdentity, Terms } from "@counterfactual/types"; +import { HashZero } from "ethers/constants"; +import { Interface } from "ethers/utils"; + +import { MultisigOperation, MultisigTransaction } from "../types"; + +import { encodeTransactions } from "./multisend-encoder"; +import { MultisigTransactionCommitment } from "./multisig-tx-op"; + +const appRegistryIface = new Interface(AppRegistry.abi); +const multisendIface = new Interface(MultiSend.abi); + +export abstract class MultiSendCommitment extends MultisigTransactionCommitment { + public abstract eachMultisigInput(): MultisigTransaction[]; + + constructor( + readonly networkContext: any, + readonly multisig: string, + readonly multisigOwners: string[], + readonly freeBalanceAppIdentity: AppIdentity, + readonly freeBalanceTerms: Terms, + readonly freeBalanceStateHash: string, + readonly freeBalanceNonce: number, + readonly freeBalanceTimeout: number, + readonly dependencyNonceSalt: string, + readonly dependencyNonceValue: number + ) { + super(multisig, multisigOwners); + } + + public getTransactionDetails(): MultisigTransaction { + return { + to: this.networkContext.MultiSend, + value: 0, + data: multisendIface.functions.multiSend.encode([ + encodeTransactions(this.eachMultisigInput()) + ]), + operation: MultisigOperation.DelegateCall + }; + } + + public freeBalanceInput(): MultisigTransaction { + return { + to: this.networkContext.AppRegistry, + value: 0, + data: appRegistryIface.functions.setState.encode([ + this.freeBalanceAppIdentity, + { + stateHash: this.freeBalanceStateHash, + nonce: this.freeBalanceNonce, + timeout: this.freeBalanceTimeout, + // Don't need signatures since a multisig is always calling MultiSend + signatures: HashZero + } + ]), + operation: MultisigOperation.Call + }; + } +} diff --git a/packages/machine/src/middleware/protocol-operation/utils/multisend-decoder.ts b/packages/machine/src/middleware/protocol-operation/utils/multisend-decoder.ts new file mode 100644 index 000000000..8986b62ad --- /dev/null +++ b/packages/machine/src/middleware/protocol-operation/utils/multisend-decoder.ts @@ -0,0 +1,79 @@ +import { defaultAbiCoder, hexDataLength, hexDataSlice } from "ethers/utils"; + +/** + * A decoder for decoding the arguments passed to a MultiSend::multiSend. + * + * @summary This code was adapted to mimic the following Solidity code: + * + * ```solidity + * function multiSend(bytes transactions) public + * { + * assembly { + * let length := mload(transactions) + * let i := 0x20 + * for { } lt(i, length) { } { + * let operation := mload(add(transactions, i)) + * let to := mload(add(transactions, add(i, 0x20))) + * let value := mload(add(transactions, add(i, 0x40))) + * let dataLength := mload(add(transactions, add(i, 0x80))) + * let data := add(transactions, add(i, 0xa0)) + * i := add(i, add(0xa0, mul(div(add(dataLength, 0x1f), 0x20), 0x20))) + * } + * } + * } + * ``` + * + * @param txs A string representing the bytes array of encoded versions of + * [uint, address, uint, bytes] tuples; each representing a transaction + * for a Multisignature Wallet to execute. Equivalent to the `transactions` + * argument to the `multiSend` function in the Solidity code. + * + * It is crucially important to realize that the transactions are *not* + * encoded as tuple(...)s. They are encoded reguarly as the four distinct + * types. There is an important distinction here. + * + * @returns An array of [op, to, val, data] javascript arrays. + */ +export function decodeMultisendCalldata(txs: string) { + const ret: [number, string, number, string][] = []; + + let i = 0; + + while (i < hexDataLength(txs)) { + // We expect 0x80 to be a hard-coded pointer to the `data` location. Refer to + // https://solidity.readthedocs.io/en/v0.5.0/abi-spec.html for ABI specification + const ptr = hexDataSlice(txs, i + 0x60, i + 0x80); + if (parseInt(ptr, 16) !== 0x80) { + throw Error( + `Incorrectly encoded transactions. Expected ${ptr} to be hard-coded as 0x80` + ); + } + + // let operation := mload(add(transactions, i)) + const op = hexDataSlice(txs, i, i + 0x20); + + // let to := mload(add(transactions, add(i, 0x20))) + const to = hexDataSlice(txs, i + 0x20, i + 0x40); + + // let value := mload(add(transactions, add(i, 0x40))) + const value = hexDataSlice(txs, i + 0x40, i + 0x60); + + // let dataLength := mload(add(transactions, add(i, 0x80))) + const dataLength = parseInt(hexDataSlice(txs, i + 0x80, i + 0xa0), 16); + + // let data := add(transactions, add(i, 0xa0)) + const data = hexDataSlice(txs, i + 0xa0, i + 0xa0 + dataLength); + + ret.push([ + defaultAbiCoder.decode(["uint8"], op)[0], + defaultAbiCoder.decode(["address"], to)[0], + defaultAbiCoder.decode(["uint256"], value)[0], + data + ]); + + // i := add(i, add(0xa0, mul(div(add(dataLength, 0x1f), 0x20), 0x20))) + i += 0xa0 + Math.ceil(dataLength / 0x20) * 0x20; + } + + return ret; +} diff --git a/packages/machine/src/middleware/protocol-operation/utils/multisend-encoder.ts b/packages/machine/src/middleware/protocol-operation/utils/multisend-encoder.ts new file mode 100644 index 000000000..bc669e2e4 --- /dev/null +++ b/packages/machine/src/middleware/protocol-operation/utils/multisend-encoder.ts @@ -0,0 +1,13 @@ +import { defaultAbiCoder } from "ethers/utils"; + +import { MultisigTransaction } from "../types"; + +const ENCODING = ["uint256", "address", "uint256", "bytes"]; + +export function encodeTransactions(txs: MultisigTransaction[]) { + return txs + .map(x => + defaultAbiCoder.encode(ENCODING, [x.operation, x.to, x.value, x.data]) + ) + .reduce((acc, v) => acc + v.substr(2), "0x"); +} diff --git a/packages/machine/src/middleware/protocol-operation/utils/multisig-tx-op.ts b/packages/machine/src/middleware/protocol-operation/utils/multisig-tx-op.ts new file mode 100644 index 000000000..3327b7de0 --- /dev/null +++ b/packages/machine/src/middleware/protocol-operation/utils/multisig-tx-op.ts @@ -0,0 +1,46 @@ +import MinimumViableMultisig from "@counterfactual/contracts/build/contracts/MinimumViableMultisig.json"; +import { Interface, keccak256, Signature, solidityPack } from "ethers/utils"; + +import { EthereumCommitment, MultisigTransaction, Transaction } from "../types"; + +import { signaturesToSortedBytes } from "./signature"; + +export abstract class MultisigTransactionCommitment extends EthereumCommitment { + constructor( + readonly multisigAddress: string, + readonly multisigOwners: string[] + ) { + super(); + } + + abstract getTransactionDetails(): MultisigTransaction; + + public transaction(sigs: Signature[]): Transaction { + const multisigInput = this.getTransactionDetails(); + + const signatureBytes = signaturesToSortedBytes(this.hashToSign(), ...sigs); + + const txData = new Interface( + MinimumViableMultisig.abi + ).functions.execTransaction.encode([ + multisigInput.to, + multisigInput.value, + multisigInput.data, + multisigInput.operation, + signatureBytes + ]); + + // TODO: Deterministically compute `to` address + return { to: this.multisigAddress, value: 0, data: txData }; + } + + public hashToSign(): string { + const { to, value, data, operation } = this.getTransactionDetails(); + return keccak256( + solidityPack( + ["bytes1", "address[]", "address", "uint256", "bytes", "uint8"], + ["0x19", this.multisigOwners, to, value, data, operation] + ) + ); + } +} diff --git a/packages/machine/src/middleware/protocol-operation/utils/signature.ts b/packages/machine/src/middleware/protocol-operation/utils/signature.ts new file mode 100644 index 000000000..d1e99fa11 --- /dev/null +++ b/packages/machine/src/middleware/protocol-operation/utils/signature.ts @@ -0,0 +1,26 @@ +import { + BigNumber, + joinSignature, + recoverAddress, + Signature +} from "ethers/utils"; + +export function signaturesToBytes(...signatures: Signature[]): string { + return signatures + .map(joinSignature) + .map(s => s.substr(2)) + .reduce((acc, v) => acc + v, "0x"); +} + +export function signaturesToSortedBytes( + digest: string, + ...signatures: Signature[] +): string { + const sigs = signatures.slice(); + sigs.sort((sigA, sigB) => { + const addrA = recoverAddress(digest, signaturesToBytes(sigA)); + const addrB = recoverAddress(digest, signaturesToBytes(sigB)); + return new BigNumber(addrA).lt(addrB) ? -1 : 1; + }); + return signaturesToBytes(...sigs); +} diff --git a/packages/machine/src/middleware/state-transition/install-proposer.ts b/packages/machine/src/middleware/state-transition/install-proposer.ts deleted file mode 100644 index fa2318d06..000000000 --- a/packages/machine/src/middleware/state-transition/install-proposer.ts +++ /dev/null @@ -1,158 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import { ethers } from "ethers"; - -import { Context } from "../../instruction-executor"; -import { Node, StateChannelInfoImpl } from "../../node"; -import { InternalMessage, StateProposal } from "../../types"; - -export class InstallProposer { - public static propose( - message: InternalMessage, - context: Context, - node: Node - ): StateProposal { - const multisig: cf.legacy.utils.Address = - message.clientMessage.multisigAddress; - - const data: cf.legacy.app.InstallData = message.clientMessage.data; - const app = new cf.legacy.app.AppInterface( - data.app.address, - data.app.applyAction, - data.app.resolve, - data.app.getTurnTaker, - data.app.isStateTerminal, - data.app.stateEncoding - ); - const terms = new cf.legacy.app.Terms( - data.terms.assetType, - data.terms.limit, - data.terms.token - ); - const uniqueId = InstallProposer.nextUniqueId(node, multisig); - const signingKeys = InstallProposer.newSigningKeys(context, data); - const cfAddr = InstallProposer.proposedCfAddress( - node, - message, - app, - terms, - signingKeys, - uniqueId - ); - const existingFreeBalance = node.stateChannel(multisig).freeBalance; - const newAppInstance = InstallProposer.newAppInstance( - cfAddr, - data, - app, - terms, - signingKeys, - uniqueId - ); - const [peerA, peerB] = InstallProposer.newPeers(existingFreeBalance, data); - const freeBalance = new cf.legacy.utils.FreeBalance( - peerA.address, - peerA.balance, - peerB.address, - peerB.balance, - existingFreeBalance.uniqueId, - existingFreeBalance.localNonce + 1, - data.timeout, - existingFreeBalance.dependencyNonce - ); - const updatedStateChannel = new StateChannelInfoImpl( - message.clientMessage.toAddress, - message.clientMessage.fromAddress, - multisig, - { [newAppInstance.id]: newAppInstance }, - freeBalance - ); - - return { - cfAddr, - state: { [multisig]: updatedStateChannel } - }; - } - - private static newSigningKeys( - context: Context, - data: cf.legacy.app.InstallData - ): string[] { - const signingKeys = [data.keyA!, data.keyB!]; - - // TODO: Feels like this is the wrong place for this sorting... - // https://github.com/counterfactual/monorepo/issues/129 - signingKeys.sort( - (addrA: cf.legacy.utils.Address, addrB: cf.legacy.utils.Address) => { - return new ethers.utils.BigNumber(addrA).lt(addrB) ? -1 : 1; - } - ); - - return signingKeys; - } - - private static newAppInstance( - cfAddr: cf.legacy.utils.H256, - data: cf.legacy.app.InstallData, - app: cf.legacy.app.AppInterface, - terms: cf.legacy.app.Terms, - signingKeys: string[], - uniqueId: number - ): cf.legacy.app.AppInstanceInfo { - return { - uniqueId, - terms, - id: cfAddr, - peerA: data.peerA, - peerB: data.peerB, - keyA: signingKeys[0], - keyB: signingKeys[1], - encodedState: data.encodedAppState, - localNonce: 1, - timeout: data.timeout, - cfApp: app, - dependencyNonce: new cf.legacy.utils.Nonce(false, uniqueId, 0) - }; - } - - private static proposedCfAddress( - node: Node, - message: InternalMessage, - app: cf.legacy.app.AppInterface, - terms: cf.legacy.app.Terms, - signingKeys: string[], - uniqueId: number - ): cf.legacy.utils.H256 { - return new cf.legacy.app.AppInstance( - node.networkContext, - message.clientMessage.multisigAddress, - signingKeys, - app, - terms, - message.clientMessage.data.timeout, - uniqueId - ).cfAddress(); - } - - private static newPeers( - existingFreeBalance: cf.legacy.utils.FreeBalance, - data: cf.legacy.app.InstallData - ): [cf.legacy.utils.PeerBalance, cf.legacy.utils.PeerBalance] { - const peerA = new cf.legacy.utils.PeerBalance( - existingFreeBalance.alice, - existingFreeBalance.aliceBalance.sub(data.peerA.balance) - ); - const peerB = new cf.legacy.utils.PeerBalance( - existingFreeBalance.bob, - existingFreeBalance.bobBalance.sub(data.peerB.balance) - ); - return [peerA, peerB]; - } - - private static nextUniqueId( - state: Node, - multisig: cf.legacy.utils.Address - ): number { - const channel = state.channelStates[multisig]; - // + 1 for the free balance - return Object.keys(channel.appInstances).length + 1; - } -} diff --git a/packages/machine/src/middleware/state-transition/setup-proposer.ts b/packages/machine/src/middleware/state-transition/setup-proposer.ts deleted file mode 100644 index 0e77236ee..000000000 --- a/packages/machine/src/middleware/state-transition/setup-proposer.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import { ethers } from "ethers"; - -import { StateChannelInfoImpl } from "../../node"; -import { InternalMessage, StateProposal } from "../../types"; - -const FREE_BALANCE_TIMEOUT = 100; -/** - * UniqueId corresponds to the number of apps maintained by a particular - * multisig. Since the free balance is the first app, its id is 0. - */ -const FREE_BALANCE_UNIQUE_ID = 0; - -/** - * Similar to the unique id, the dependency nonce for every app is - * determined Hash(multisig || salt), and so for the salt, we use a - * counter on the number of apps associated with the multisig. For the - * free balance this number is 0. - */ -export class SetupProposer { - public static propose(message: InternalMessage): StateProposal { - const toAddress = message.clientMessage.toAddress; - const fromAddress = message.clientMessage.fromAddress; - - const balances = cf.legacy.utils.PeerBalance.balances( - toAddress, - ethers.utils.bigNumberify(0), - fromAddress, - ethers.utils.bigNumberify(0) - ); - const localNonce = 0; - const freeBalance = new cf.legacy.utils.FreeBalance( - balances.peerA.address, - balances.peerA.balance, - balances.peerB.address, - balances.peerB.balance, - FREE_BALANCE_UNIQUE_ID, - localNonce, - FREE_BALANCE_TIMEOUT, - new cf.legacy.utils.Nonce(false, FREE_BALANCE_UNIQUE_ID, 0) - ); - const stateChannel = new StateChannelInfoImpl( - toAddress, - fromAddress, - message.clientMessage.multisigAddress, - {}, - freeBalance - ); - return { - state: { - [String(message.clientMessage.multisigAddress)]: stateChannel - } - }; - } -} diff --git a/packages/machine/src/middleware/state-transition/uninstall-proposer.ts b/packages/machine/src/middleware/state-transition/uninstall-proposer.ts deleted file mode 100644 index 4541b1f1a..000000000 --- a/packages/machine/src/middleware/state-transition/uninstall-proposer.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; - -import { Node, StateChannelInfoImpl } from "../../node"; -import { InternalMessage, StateProposal } from "../../types"; - -export class UninstallProposer { - public static propose(message: InternalMessage, node: Node): StateProposal { - const multisig: cf.legacy.utils.Address = - message.clientMessage.multisigAddress; - const channels = node.stateChannelInfosCopy(); - const appId = message.clientMessage.appInstanceId; - if (appId === undefined) { - throw new Error("uninstall message must have appId set"); - } - // delete the app by bumping the nonce - channels[multisig].appInstances[appId].dependencyNonce.nonceValue += 1; - channels[multisig].appInstances[appId].dependencyNonce.isSet = true; - // add balance and update nonce - const canon = cf.legacy.utils.CanonicalPeerBalance.canonicalize( - message.clientMessage.data.peerAmounts[0], - message.clientMessage.data.peerAmounts[1] - ); - const oldFreeBalance = channels[multisig].freeBalance; - const newFreeBalance = new cf.legacy.utils.FreeBalance( - oldFreeBalance.alice, - oldFreeBalance.aliceBalance.add(canon.peerA.balance), - oldFreeBalance.bob, - oldFreeBalance.bobBalance.add(canon.peerB.balance), - oldFreeBalance.uniqueId, - oldFreeBalance.localNonce + 1, - oldFreeBalance.timeout, - oldFreeBalance.dependencyNonce - ); - const channel = channels[multisig]; - // now replace the state channel with a newly updated one - channels[multisig] = new StateChannelInfoImpl( - channel.counterParty, - channel.me, - multisig, - channel.appInstances, - newFreeBalance - ); - return { state: channels }; - } -} diff --git a/packages/machine/src/middleware/state-transition/update-proposer.ts b/packages/machine/src/middleware/state-transition/update-proposer.ts deleted file mode 100644 index dbb1078f5..000000000 --- a/packages/machine/src/middleware/state-transition/update-proposer.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; - -import { Node } from "../../node"; -import { InternalMessage, StateProposal } from "../../types"; - -export class UpdateProposer { - public static propose(message: InternalMessage, node: Node): StateProposal { - const multisig: cf.legacy.utils.Address = - message.clientMessage.multisigAddress; - const channels = node.stateChannelInfosCopy(); - - if (message.clientMessage.appInstanceId === undefined) { - throw new Error("update message must have appId set"); - } - - const appId: cf.legacy.utils.H256 = message.clientMessage.appInstanceId; - const updateData: cf.legacy.app.UpdateData = message.clientMessage.data; - - const app = channels[multisig].appInstances[appId]; - app.appStateHash = updateData.appStateHash; - app.encodedState = updateData.encodedAppState; - app.localNonce += 1; - - return { state: channels }; - } -} diff --git a/packages/machine/src/mixins/apply.ts b/packages/machine/src/mixins/apply.ts deleted file mode 100644 index 9abbb6171..000000000 --- a/packages/machine/src/mixins/apply.ts +++ /dev/null @@ -1,9 +0,0 @@ -// https://www.typescriptlang.org/docs/handbook/mixins.html - -export function applyMixins(derivedCtor: any, baseCtors: any[]) { - baseCtors.forEach(baseCtor => { - Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => { - derivedCtor.prototype[name] = baseCtor.prototype[name]; - }); - }); -} diff --git a/packages/machine/src/mixins/index.ts b/packages/machine/src/mixins/index.ts deleted file mode 100644 index 0c20097d3..000000000 --- a/packages/machine/src/mixins/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { applyMixins } from "./apply"; -import { NotificationType, Observable } from "./observable"; - -export { applyMixins, NotificationType, Observable }; diff --git a/packages/machine/src/mixins/observable.ts b/packages/machine/src/mixins/observable.ts deleted file mode 100644 index ff05a378a..000000000 --- a/packages/machine/src/mixins/observable.ts +++ /dev/null @@ -1,25 +0,0 @@ -export type NotificationType = string; - -export class Observable { - public observers: Map = new Map(); - - public registerObserver(type: NotificationType, callback: Function) { - if (!this.observers[type]) { - this.observers[type] = []; - } - this.observers[type].push(callback); - } - - public unregisterObserver(type: NotificationType, callback: Function) { - const index = this.observers[type].indexOf(callback); - this.observers[type].splice(index, 1); - } - - public notifyObservers(type: NotificationType, data: object) { - if (this.observers[type]) { - this.observers[type].forEach(callback => { - callback(data); - }); - } - } -} diff --git a/packages/machine/src/models/app-instance.ts b/packages/machine/src/models/app-instance.ts new file mode 100644 index 000000000..283e24efd --- /dev/null +++ b/packages/machine/src/models/app-instance.ts @@ -0,0 +1,238 @@ +import { AppIdentity, AppInterface, Terms } from "@counterfactual/types"; +import { defaultAbiCoder, keccak256, solidityPack } from "ethers/utils"; +import { Memoize } from "typescript-memoize"; + +import { appIdentityToHash } from "../middleware/protocol-operation/utils/app-identity"; +import { + APP_INTERFACE, + TERMS +} from "../middleware/protocol-operation/utils/encodings"; + +/** + * Representation of the values a dependency nonce can take on. + */ +export enum DependencyValue { + NOT_UNINSTALLED = 0, + UNINSTALLED = 1 +} + +export type AppInstanceJson = { + multisigAddress: string; + signingKeys: string[]; + defaultTimeout: number; + appInterface: AppInterface; + terms: Terms; + isMetachannelApp: boolean; + appSeqNo: number; + latestState: object; + latestNonce: number; + latestTimeout: number; + hasBeenUninstalled: boolean; +}; + +/** + * Representation of an AppInstance. + * + * @property owner The address of the multisignature wallet on-chain for the + * state channel that hold the state this AppInstance controls. + + * @property signingKeys The sorted array of public keys used by the users of + * this AppInstance for which n-of-n consensus is needed on updates. + + * @property defaultTimeout The default timeout used when a new update is made. + + * @property appInterface An AppInterface object representing the logic this + * AppInstance relies on for verifying and proposing state updates. + + * @property isMetachannelApp A flag indicating whether this AppInstance's state + * deposits are within a metachannel or in a single on-chain deposit. + + * @property terms The terms for which this AppInstance is based on. + + * @property latestState The unencoded representation of the latest state. + + * @property latestNonce The nonce of the latest signed state update. + + * @property latestTimeout The timeout used in the latest signed state update. + */ +// TODO: dont forget dependnecy nonce docstring +export class AppInstance { + private readonly json: AppInstanceJson; + + constructor( + multisigAddress: string, + signingKeys: string[], + defaultTimeout: number, + appInterface: AppInterface, + terms: Terms, + isMetachannelApp: boolean, + appSeqNo: number, + latestState: object, + latestNonce: number, + latestTimeout: number + ) { + this.json = { + multisigAddress, + signingKeys, + defaultTimeout, + appInterface, + terms, + isMetachannelApp, + appSeqNo, + latestState, + latestNonce, + latestTimeout, + hasBeenUninstalled: false + }; + } + + public static fromJson(json: AppInstanceJson) { + const ret = new AppInstance( + json.multisigAddress, + json.signingKeys, + json.defaultTimeout, + json.appInterface, + json.terms, + json.isMetachannelApp, + json.appSeqNo, + json.latestState, + json.latestNonce, + json.latestTimeout + ); + ret.json.hasBeenUninstalled = json.hasBeenUninstalled; + return ret; + } + + @Memoize() + public get id() { + return appIdentityToHash(this.identity); + } + + @Memoize() + public get identity(): AppIdentity { + const encodedAppInterface = defaultAbiCoder.encode( + [APP_INTERFACE], + [this.json.appInterface] + ); + const encodedTerms = defaultAbiCoder.encode([TERMS], [this.json.terms]); + return { + owner: this.json.multisigAddress, + signingKeys: this.json.signingKeys, + appInterfaceHash: keccak256(encodedAppInterface), + termsHash: keccak256(encodedTerms), + defaultTimeout: this.json.defaultTimeout + }; + } + + @Memoize() + public get hashOfLatestState() { + return keccak256(this.encodedLatestState); + } + + @Memoize() + public get encodedLatestState() { + return defaultAbiCoder.encode( + [this.json.appInterface.stateEncoding], + [this.json.latestState] + ); + } + + @Memoize() + public get encodedTerms() { + return defaultAbiCoder.encode([TERMS], [this.json.terms]); + } + + @Memoize() + public get uninstallKey() { + // The unique "key" in the NonceRegistry is computed to be: + // hash(, , hash()) + // where is 0 since FreeBalance is assumed to be the + // firstmost intalled app in the channel. + return keccak256( + solidityPack( + ["address", "uint256", "bytes32"], + [ + this.json.multisigAddress, + 0, + keccak256( + solidityPack( + ["uint256"], + // In this case, we expect the variable to be + // 1 since this newly installed app is the only app installed + // after the ETH FreeBalance was installed. + [this.json.appSeqNo] + ) + ) + ] + ) + ); + } + + // TODO: All these getters seems a bit silly, would be nice to improve + // the implementation to make it cleaner. + + public get terms() { + return this.json.terms; + } + + public get state() { + return this.json.latestState; + } + + public get nonce() { + return this.json.latestNonce; + } + + public get timeout() { + return this.json.latestTimeout; + } + + public get appInterface() { + return this.json.appInterface; + } + + public get defaultTimeout() { + return this.json.defaultTimeout; + } + + public get appSeqNo() { + return this.json.appSeqNo; + } + + public get multisigAddress() { + return this.json.multisigAddress; + } + + public get signingKeys() { + return this.json.signingKeys; + } + + public get isMetachannelApp() { + return this.json.isMetachannelApp; + } + + public setState( + newState: object, + timeout: number = this.json.defaultTimeout + ) { + try { + defaultAbiCoder.encode( + [this.json.appInterface.stateEncoding], + [newState] + ); + } catch (e) { + // TODO: Catch ethers.errors.INVALID_ARGUMENT specifically in catch {} + console.error( + "Attempted to setState on an app with an invalid state object" + ); + throw e; + } + + return AppInstance.fromJson({ + ...this.json, + latestState: newState, + latestNonce: this.json.latestNonce + 1, + latestTimeout: timeout + }); + } +} diff --git a/packages/machine/src/models/index.ts b/packages/machine/src/models/index.ts new file mode 100644 index 000000000..0f58144b3 --- /dev/null +++ b/packages/machine/src/models/index.ts @@ -0,0 +1,4 @@ +import { AppInstance } from "./app-instance"; +import { StateChannel } from "./state-channel"; + +export { AppInstance, StateChannel }; diff --git a/packages/machine/src/models/state-channel.ts b/packages/machine/src/models/state-channel.ts new file mode 100644 index 000000000..559b91afe --- /dev/null +++ b/packages/machine/src/models/state-channel.ts @@ -0,0 +1,239 @@ +import { + AssetType, + ETHBucketAppState, + NetworkContext +} from "@counterfactual/types"; +import { Zero } from "ethers/constants"; +import { INSUFFICIENT_FUNDS } from "ethers/errors"; +import { BigNumber } from "ethers/utils"; + +import { + freeBalanceTerms, + getFreeBalanceAppInterface +} from "../middleware/protocol-operation/utils/free-balance"; + +import { AppInstance } from "./app-instance"; + +// TODO: Hmmm this code should probably be somewhere else? +const HARD_CODED_ASSUMPTIONS = { + freeBalanceDefaultTimeout: 10, + freeBalanceInitialStateTimeout: 10, + appSequenceNumberForFreeBalance: 0 +}; + +const ERRORS = { + APPS_NOT_EMPTY: size => + `Expected the appInstances list to be empty but size ${size}`, + APP_DOES_NOT_EXIST: id => + `Attempted to edit an appInstance that does not exist: id = ${id}`, + FREE_BALANCE_MISSING: "Cannot find ETH Free Balance App in StateChannel", + FREE_BALANCE_IDX_CORRUPT: idx => + `Index ${idx} used to find ETH Free Balance is broken`, + INSUFFICIENT_FUNDS: + "Attempted to install an appInstance without sufficient funds" +}; + +function createETHFreeBalance( + multisigAddress: string, + multisigOwners: string[], + ethBucketAddress: string +) { + // Making these values constants to be extremely explicit about + // the built-in assumption here. + const signingKeyForFreeBalanceForPerson1 = multisigOwners[0]; + const signingKeyForFreeBalanceForPerson2 = multisigOwners[1]; + + return new AppInstance( + multisigAddress, + multisigOwners, + HARD_CODED_ASSUMPTIONS.freeBalanceDefaultTimeout, + getFreeBalanceAppInterface(ethBucketAddress), + freeBalanceTerms, + false, + HARD_CODED_ASSUMPTIONS.appSequenceNumberForFreeBalance, + { + alice: signingKeyForFreeBalanceForPerson1, + bob: signingKeyForFreeBalanceForPerson2, + aliceBalance: Zero, + bobBalance: Zero + }, + 0, + HARD_CODED_ASSUMPTIONS.freeBalanceInitialStateTimeout + ); +} + +export class StateChannel { + constructor( + public readonly multisigAddress: string, + public readonly multisigOwners: string[], + private readonly appInstances: ReadonlyMap = new Map< + string, + AppInstance + >([]), + private readonly freeBalanceAppIndexes: ReadonlyMap< + AssetType, + string + > = new Map([]), + private readonly monotonicNumInstalledApps: number = 0 + ) {} + + public get numInstalledApps() { + return this.monotonicNumInstalledApps; + } + + public get numActiveApps() { + return this.appInstances.size; + } + + public getAppInstance(appInstanceId: string): AppInstance { + if (!this.appInstances.has(appInstanceId)) { + throw Error(`${ERRORS.APP_DOES_NOT_EXIST}({appInstance.id})`); + } + return this.appInstances.get(appInstanceId)!; + } + + public isAppInstanceInstalled(appInstanceId: string) { + return this.appInstances.has(appInstanceId); + } + + public getFreeBalanceFor(assetType: AssetType): AppInstance { + if (!this.freeBalanceAppIndexes.has(assetType)) { + throw Error(ERRORS.FREE_BALANCE_MISSING); + } + + const idx = this.freeBalanceAppIndexes.get(assetType); + + if (!this.appInstances.has(idx!)) { + throw Error(`${ERRORS.FREE_BALANCE_IDX_CORRUPT}({idx})`); + } + + return this.appInstances.get(idx!)!; + } + + public setupChannel(network: NetworkContext) { + const size = this.appInstances.size; + + if (size > 0) throw Error(`${ERRORS.APPS_NOT_EMPTY}({size})`); + + const fb = createETHFreeBalance( + this.multisigAddress, + this.multisigOwners, + network.ETHBucket + ); + + const appInstances = new Map( + this.appInstances.entries() + ); + + const freeBalanceAppIndexes = new Map( + this.freeBalanceAppIndexes.entries() + ); + + appInstances.set(fb.id, fb); + + freeBalanceAppIndexes.set(AssetType.ETH, fb.id); + + return new StateChannel( + this.multisigAddress, + this.multisigOwners, + appInstances, + freeBalanceAppIndexes, + this.monotonicNumInstalledApps + 1 + ); + } + + public setState(appInstanceId: string, state: object) { + const appInstance = this.getAppInstance(appInstanceId); + + const appInstances = new Map( + this.appInstances.entries() + ); + + const freeBalanceAppIndexes = new Map( + this.freeBalanceAppIndexes.entries() + ); + + appInstances.set(appInstanceId, appInstance.setState(state)); + + return new StateChannel( + this.multisigAddress, + this.multisigOwners, + appInstances, + freeBalanceAppIndexes, + this.monotonicNumInstalledApps + ); + } + + public installApp( + appInstance: AppInstance, + aliceBalanceDecrement: BigNumber, + bobBalanceDecrement: BigNumber + ) { + const fb = this.getFreeBalanceFor(AssetType.ETH); + const currentState = fb.state as ETHBucketAppState; + + const aliceBalance = currentState.aliceBalance.sub(aliceBalanceDecrement); + const bobBalance = currentState.bobBalance.sub(bobBalanceDecrement); + + if (aliceBalance.lt(Zero) || bobBalance.lt(Zero)) { + throw Error(INSUFFICIENT_FUNDS); + } + + const appInstances = new Map( + this.appInstances.entries() + ); + + const freeBalanceAppIndexes = new Map( + this.freeBalanceAppIndexes.entries() + ); + + appInstances + .set(appInstance.id, appInstance) + .set(fb.id, fb.setState({ ...currentState, aliceBalance, bobBalance })); + + return new StateChannel( + this.multisigAddress, + this.multisigOwners, + appInstances, + freeBalanceAppIndexes, + this.monotonicNumInstalledApps + 1 + ); + } + + public uninstallApp( + appInstanceId: string, + aliceBalanceIncrement: BigNumber, + bobBalanceIncrement: BigNumber + ) { + const fb = this.getFreeBalanceFor(AssetType.ETH); + const appToBeUninstalled = this.getAppInstance(appInstanceId); + + const currentState = fb.state as ETHBucketAppState; + + const aliceBalance = currentState.aliceBalance.sub(aliceBalanceIncrement); + const bobBalance = currentState.bobBalance.sub(bobBalanceIncrement); + + const appInstances = new Map( + this.appInstances.entries() + ); + + const freeBalanceAppIndexes = new Map( + this.freeBalanceAppIndexes.entries() + ); + + appInstances.delete(appToBeUninstalled.id); + + appInstances.set( + fb.id, + fb.setState({ ...currentState, aliceBalance, bobBalance }) + ); + + return new StateChannel( + this.multisigAddress, + this.multisigOwners, + appInstances, + freeBalanceAppIndexes, + this.monotonicNumInstalledApps + ); + } +} diff --git a/packages/machine/src/node.ts b/packages/machine/src/node.ts deleted file mode 100644 index 9c77adc1a..000000000 --- a/packages/machine/src/node.ts +++ /dev/null @@ -1,91 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; - -/** - * Node encapsulates the state of all the channels. - */ -export class Node { - public channelStates: cf.legacy.channel.StateChannelInfos; - public networkContext: cf.legacy.network.NetworkContext; - - constructor( - channelStates: cf.legacy.channel.StateChannelInfos, - network: cf.legacy.network.NetworkContext - ) { - this.channelStates = channelStates; - this.networkContext = network; - } - - public stateChannel( - multisig: cf.legacy.utils.Address - ): cf.legacy.channel.StateChannelInfo { - return this.channelStates[multisig]; - } - - public stateChannelFromMultisigAddress( - multisigAddress: cf.legacy.utils.Address - ): cf.legacy.channel.StateChannelInfo { - const multisig = this.channelStates[multisigAddress]; - if (multisig) { - return this.channelStates[multisigAddress]; - } - throw Error(`Could not find multisig of address ${multisigAddress}`); - } - - public app( - multisig: cf.legacy.utils.Address, - cfAddr: cf.legacy.utils.H256 - ): cf.legacy.app.AppInstanceInfo { - return this.channelStates[multisig].appInstances[cfAddr]; - } - - public freeBalanceFromMultisigAddress( - multisigAddress: cf.legacy.utils.Address - ): cf.legacy.utils.FreeBalance { - const multisig = this.channelStates[multisigAddress]; - if (multisig) { - return this.channelStates[multisigAddress].freeBalance; - } - throw Error(`Could not find multisig of address ${multisigAddress}`); - } - - /** - * @returns a deep copy of the StateChannelInfos. - */ - public stateChannelInfosCopy(): cf.legacy.channel.StateChannelInfos { - return cf.legacy.utils.serializer.deserialize( - JSON.parse(JSON.stringify(this.channelStates)) - ); - } - - public appChannelInfos(): cf.legacy.app.AppInstanceInfos { - const infos = {}; - for (const channel of Object.keys(this.channelStates)) { - for (const appChannel of Object.keys( - this.channelStates[channel].appInstances - )) { - infos[appChannel] = this.channelStates[channel].appInstances[ - appChannel - ]; - } - } - return infos; - } -} - -export class StateChannelInfoImpl - implements cf.legacy.channel.StateChannelInfo { - constructor( - readonly counterParty: cf.legacy.utils.Address, - readonly me: cf.legacy.utils.Address, - readonly multisigAddress: cf.legacy.utils.Address, - readonly appInstances: cf.legacy.app.AppInstanceInfos = {}, - readonly freeBalance: cf.legacy.utils.FreeBalance - ) {} - - /** - * @returns the toAddress, fromAddress in alphabetical order. - */ - public owners(): string[] { - return [this.counterParty, this.me].sort((a, b) => (a < b ? -1 : 1)); - } -} diff --git a/packages/machine/src/opcodes.ts b/packages/machine/src/opcodes.ts index 28635c0b9..993a95778 100644 --- a/packages/machine/src/opcodes.ts +++ b/packages/machine/src/opcodes.ts @@ -4,26 +4,31 @@ export enum Opcode { * completes. Useful for other opcodes that may need to know about such state, * for example, to generate the correct cf operation. */ - STATE_TRANSITION_PROPOSE = 0, + STATE_TRANSITION_PROPOSE, + /** * Saves the new state upon completion of a protocol, using the state from * STATE_TRANSITION_PROPOSE. Assumes all messages have been exchanged and * the state has gone through PROPOSE and PREPARE already. */ STATE_TRANSITION_COMMIT, + /** * Requests a signature on the hash of a previously generated ProtocolOperation. */ OP_SIGN, + /** * Ensures a signature is both correclty signed and is representative of a * correctly formed cf operation. */ OP_SIGN_VALIDATE, + /** * Sends a ClientMessage to a peer. */ IO_SEND, + /** * Blocks the action execution until the next message is received by a peer. * The registered middleware for this instruction *must* return the received diff --git a/packages/machine/src/protocol-types-tbd.ts b/packages/machine/src/protocol-types-tbd.ts new file mode 100644 index 000000000..d86d84a56 --- /dev/null +++ b/packages/machine/src/protocol-types-tbd.ts @@ -0,0 +1,59 @@ +// TODO: Probably merge this file with ./types.ts + +import { AppInterface, Terms } from "@counterfactual/types"; +import { BigNumber, Signature } from "ethers/utils"; + +import { Protocol } from "./types"; + +export type StateData = { + [x: string]: string | number | boolean | StateData | StateDataArray; +}; + +// I think this should be a `type` not an `interface` but self-referencial +// types is not supported: github.com/Microsoft/TypeScript/issues/6230 +export interface StateDataArray + extends Array {} + +export type ProtocolMessage = { + protocol: Protocol; + multisigAddress: string; + params: ProtocolParameters; + fromAddress: string; + toAddress: string; + seq: number; + signature?: Signature; +}; + +export type SetupData = {}; + +export type UpdateData = { + appInstanceId: string; + newState: StateData; +}; + +export type InstallData = { + aliceBalanceDecrement: BigNumber; + bobBalanceDecrement: BigNumber; + signingKeys: string[]; + initialState: StateData; + terms: Terms; + appInterface: AppInterface; + defaultTimeout: number; +}; + +export type UninstallData = { + appInstanceId: string; + aliceBalanceIncrement: BigNumber; + bobBalanceIncrement: BigNumber; +}; + +export type MetaChannelInstallAppData = { + /* TODO: @xuanji */ +}; + +type ProtocolParameters = + | SetupData + | UpdateData + | InstallData + | UninstallData + | MetaChannelInstallAppData; diff --git a/packages/machine/src/types.ts b/packages/machine/src/types.ts index 1f142195d..77e949f0f 100644 --- a/packages/machine/src/types.ts +++ b/packages/machine/src/types.ts @@ -1,39 +1,21 @@ -import * as cf from "@counterfactual/cf.js"; +import { NetworkContext } from "@counterfactual/types"; +import { Signature } from "ethers/utils"; -import { Context } from "./instruction-executor"; -import { Node } from "./node"; +import { EthereumCommitment } from "./middleware/protocol-operation/utils"; +import { StateChannel } from "./models"; import { Opcode } from "./opcodes"; - -/** - * The return value from the STATE_TRANSITION_PROPOSE middleware. - */ -export interface StateProposal { - state: cf.legacy.channel.StateChannelInfos; - cfAddr?: cf.legacy.utils.H256; -} - -export type ProposerActionsHash = { - [Name in cf.legacy.node.ActionName]?: ContextualizedStateProposer -}; - -export interface ContextualizedStateProposer { - propose( - message: InternalMessage, - context: Context, - node: Node - ): StateProposal; -} - -export class InternalMessage { - constructor( - public actionName: cf.legacy.node.ActionName, - public opCode: Opcode, - public clientMessage: cf.legacy.node.ClientActionMessage - ) {} +import { ProtocolMessage } from "./protocol-types-tbd"; + +export enum Protocol { + Setup = "setup", + Install = "install", + Update = "update", + Uninstall = "uninstall", + InstallVirtualApp = "install-virtual-app" } export type InstructionMiddlewareCallback = { - (message: InternalMessage, next: Function, context: Context); + (message: ProtocolMessage, next: Function, context: Context); }; export interface InstructionMiddleware { @@ -41,4 +23,15 @@ export interface InstructionMiddleware { method: InstructionMiddlewareCallback; } +export type Instruction = Function | Opcode; + export type InstructionMiddlewares = { [I in Opcode]: InstructionMiddleware[] }; + +export interface Context { + network: NetworkContext; + outbox: ProtocolMessage[]; + inbox: ProtocolMessage[]; + stateChannel: StateChannel; + operation?: EthereumCommitment; + signature?: Signature; +} diff --git a/packages/machine/test/integration/cf-operations.spec.ts b/packages/machine/test/integration/cf-operations.spec.ts deleted file mode 100644 index 4bcc54a33..000000000 --- a/packages/machine/test/integration/cf-operations.spec.ts +++ /dev/null @@ -1,593 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import AppInstanceJson from "@counterfactual/contracts/build/contracts/AppInstance.json"; -import MinimumViableMultisigJson from "@counterfactual/contracts/build/contracts/MinimumViableMultisig.json"; -import RegistryJson from "@counterfactual/contracts/build/contracts/Registry.json"; -import { ethers } from "ethers"; - -import { Transaction } from "../../src/middleware/protocol-operation/types"; -import { - A_ADDRESS, - A_PRIVATE_KEY, - B_ADDRESS, - B_PRIVATE_KEY, - UNUSED_FUNDED_ACCOUNT_PRIVATE_KEY -} from "../utils/environment"; - -import { TestResponseSink } from "./test-response-sink"; - -// FIXME: Remove this dependency! -// https://github.com/counterfactual/monorepo/issues/103 -const ganache = new ethers.providers.JsonRpcProvider("http://127.0.0.1:9545"); - -const { abi } = cf.utils; - -describe("Setup Protocol", async () => { - jest.setTimeout(30000); - - let devEnvNetworkContext7777777: cf.legacy.network.NetworkContext; - - beforeAll(() => { - // This `require` statement is explicitly in side the `beforeAll` and not at the file - // scope because we need it to do the file lookup at runtime and not buildtime otherwise - // the build process will fail if the contracts haven't been migrated yet. - - // TODO: The linter can't handle files with numbers as names. This should be fixed - // when we have a different method for retrieving the network addresses anyway. - // https://github.com/counterfactual/monorepo/issues/114 - // tslint:disable-next-line - const networkFile = require("@counterfactual/contracts/networks/7777777.json"); - - const addressOfContract = new Map( - networkFile.map(entry => [entry.contractName, entry.address])); - - devEnvNetworkContext7777777 = new cf.legacy.network.NetworkContext( - addressOfContract.get("Registry")!, - addressOfContract.get("PaymentApp")!, - addressOfContract.get("ConditionalTransaction")!, - addressOfContract.get("MultiSend")!, - addressOfContract.get("NonceRegistry")!, - addressOfContract.get("Signatures")!, - addressOfContract.get("StaticCall")!, - addressOfContract.get("ETHBalanceRefundApp")! - ); - }); - - /** - * The following happens in this test: - * 1. Two users set up a state channel between them - * 2. Both users deposit money into the channel - * 3. One user unilaterally closes the channel by deploying commitments made during setup and deposit - * 4. The test checks whether everyone got back the money they deposited into the channel - */ - it("should have the correct funds on chain", async () => { - const depositAmount = ethers.utils.parseEther("0.0005"); - - const walletA = new TestResponseSink( - A_PRIVATE_KEY, - devEnvNetworkContext7777777 - ); - const walletB = new TestResponseSink( - B_PRIVATE_KEY, - devEnvNetworkContext7777777 - ); - - const startingBalanceA = await ganache.getBalance(A_ADDRESS); - const startingBalanceB = await ganache.getBalance(B_ADDRESS); - - const ethersMasterWallet = new ethers.Wallet( - UNUSED_FUNDED_ACCOUNT_PRIVATE_KEY, - ganache - ); - - walletA.io.peers.set(B_ADDRESS, walletB); - walletB.io.peers.set(A_ADDRESS, walletA); - - const peerBalances = cf.legacy.utils.PeerBalance.balances( - A_ADDRESS, - ethers.utils.bigNumberify(0), - B_ADDRESS, - ethers.utils.bigNumberify(0) - ); - - const signingKeys = [ - peerBalances.peerA.address, - peerBalances.peerB.address - ]; - - const registry = await new ethers.Contract( - devEnvNetworkContext7777777.registryAddr, - RegistryJson.abi, - ethersMasterWallet - ); - - // TODO: Truffle migrate does not auto-link the bytecode in the build folder, - // so we have to do it manually. Will fix later of course :) - // https://github.com/counterfactual/monorepo/issues/113 - const multisig = await new ethers.ContractFactory( - MinimumViableMultisigJson.abi, - devEnvNetworkContext7777777.linkedBytecode( - MinimumViableMultisigJson.bytecode - ), - ethersMasterWallet - ).deploy(); - - await multisig.functions.setup(signingKeys); - - await setup(multisig.address, walletA, walletB); - - const { - cfAddr: balanceRefundAppId, - txFeeA: depositTxFeeA, - txFeeB: depositTxFeeB - } = await makeDeposits(multisig.address, walletA, walletB, depositAmount); - - const app = cf.legacy.utils.FreeBalance.contractInterface( - devEnvNetworkContext7777777 - ); - - const terms = cf.legacy.utils.FreeBalance.terms(); - - const initcode = new ethers.utils.Interface( - AppInstanceJson.abi - ).deployFunction.encode( - devEnvNetworkContext7777777.linkedBytecode(AppInstanceJson.bytecode), - [ - multisig.address, - signingKeys, - app.hash(), - terms.hash(), - // TODO: Don't hard-code the timeout, make it dependant on some - // function(blockchain) to in the future check for congestion... :) - // https://github.com/counterfactual/monorepo/issues/112 - 100 - ] - ); - - // TODO: Figure out how to not have to put the insanely high gasLimit here - // https://github.com/counterfactual/monorepo/issues/146 - await registry.functions.deploy(initcode, 0, { gasLimit: 6e9 }); - - const uninstallTx: Transaction = await walletA.store.getTransaction( - balanceRefundAppId, - cf.legacy.node.ActionName.UNINSTALL - ); - - await ethersMasterWallet.sendTransaction({ - to: uninstallTx.to, - value: `0x${uninstallTx.value.toString(16)}`, - data: uninstallTx.data, - gasLimit: 6e9 - }); - - await cf.legacy.utils.mineBlocks( - 100, - ethersMasterWallet.provider as ethers.providers.JsonRpcProvider - ); - - const freeBalance = walletA.instructionExecutor.node.freeBalanceFromMultisigAddress( - multisig.address - ); - - const values = [ - freeBalance.alice, - freeBalance.bob, - freeBalance.aliceBalance, - freeBalance.bobBalance - ]; - - const freeBalanceFinalState = abi.encode( - ["address", "address", "uint256", "uint256"], - values - ); - - const appInstance = new cf.legacy.app.AppInstance( - devEnvNetworkContext7777777, - multisig.address, - signingKeys, - app, - terms, - 100, - 0 - ); - - const appInstanceCfAddr = appInstance.cfAddress(); - - const appInstanceAddr = await registry.functions.resolver( - appInstanceCfAddr - ); - - const stateChannel = new ethers.Contract( - appInstanceAddr, - AppInstanceJson.abi, - ethersMasterWallet - ); - - const appData = [ - app.address, - app.applyAction, - app.resolve, - app.getTurnTaker, - app.isStateTerminal - ]; - - const termsData = abi.encode( - ["bytes1", "uint8", "uint256", "address"], - ["0x19", terms.assetType, terms.limit, terms.token] - ); - - await stateChannel.functions.setResolution( - appData, - freeBalanceFinalState, - termsData - ); - - const setupTx: Transaction = await walletA.store.getTransaction( - multisig.address, - cf.legacy.node.ActionName.SETUP - ); - - await ethersMasterWallet.sendTransaction({ - to: setupTx.to, - value: `0x${setupTx.value.toString(16)}`, - data: setupTx.data - }); - - const endBalanceA = await ganache.getBalance(A_ADDRESS); - - const endBalanceB = await ganache.getBalance(B_ADDRESS); - - expect(endBalanceA.sub(startingBalanceA).toNumber()).toEqual( - depositTxFeeA.toNumber() - ); - - expect(endBalanceB.sub(startingBalanceB).toNumber()).toEqual( - depositTxFeeB.toNumber() - ); - }); -}); - -async function setup( - multisigAddr: string, - walletA: TestResponseSink, - walletB: TestResponseSink -) { - validatePresetup(walletA, walletB); - const response = await walletA.runSetupProtocol( - walletA.signingKey.address, - walletB.signingKey.address, - multisigAddr - ); - expect(response.status).toEqual(cf.legacy.node.ResponseStatus.COMPLETED); - validateSetup(multisigAddr, walletA, walletB); -} - -function validatePresetup( - walletA: TestResponseSink, - walletB: TestResponseSink -) { - expect(walletA.instructionExecutor.node.channelStates).toEqual({}); - expect(walletB.instructionExecutor.node.channelStates).toEqual({}); -} - -function validateSetup( - multisigAddr: string, - walletA: TestResponseSink, - walletB: TestResponseSink -) { - validateNoAppsAndFreeBalance( - multisigAddr, - walletA, - walletB, - ethers.utils.bigNumberify(0), - ethers.utils.bigNumberify(0) - ); - validateNoAppsAndFreeBalance( - multisigAddr, - walletB, - walletA, - ethers.utils.bigNumberify(0), - ethers.utils.bigNumberify(0) - ); -} - -/** - * Validates the correctness of walletAs free balance *not* walletBs. - */ -function validateNoAppsAndFreeBalance( - multisigAddr: string, - walletA: TestResponseSink, - walletB: TestResponseSink, - amountAgiven: ethers.utils.BigNumber, - amountBgiven: ethers.utils.BigNumber -) { - // todo: add nonce and uniqueId params and check them - // https://github.com/counterfactual/monorepo/issues/111 - const state = walletA.instructionExecutor.node; - - let peerA = walletA.signingKey.address; - let peerB = walletB.signingKey.address; - - let amountA = amountAgiven; - let amountB = amountBgiven; - - if (peerB.localeCompare(peerA) < 0) { - const tmp = peerA; - peerA = peerB; - peerB = tmp; - const tmpAmount = amountA; - amountA = amountB; - amountB = tmpAmount; - } - - const channel = - walletA.instructionExecutor.node.channelStates[multisigAddr]; - expect(Object.keys(state.channelStates).length).toEqual(1); - expect(channel.counterParty).toEqual(walletB.signingKey.address); - expect(channel.me).toEqual(walletA.signingKey.address); - expect(channel.multisigAddress).toEqual(multisigAddr); - expect(channel.appInstances).toEqual({}); - expect(channel.freeBalance.alice).toEqual(peerA); - expect(channel.freeBalance.bob).toEqual(peerB); - expect(channel.freeBalance.aliceBalance.toNumber()).toEqual( - amountA.toNumber() - ); - expect(channel.freeBalance.bobBalance.toNumber()).toEqual(amountB.toNumber()); -} - -async function makeDeposits( - multisigAddr: string, - walletA: TestResponseSink, - walletB: TestResponseSink, - depositAmount: ethers.utils.BigNumber -): Promise<{ - cfAddr: string; - txFeeA: ethers.utils.BigNumber; - txFeeB: ethers.utils.BigNumber; -}> { - const { txFee: txFeeA } = await deposit( - multisigAddr, - walletA, // depositor - walletB, // counterparty - depositAmount, // amountToDeposit - ethers.utils.bigNumberify(0) // counterpartyBalance - ); - const { cfAddr, txFee: txFeeB } = await deposit( - multisigAddr, - walletB, // depositor - walletA, // counterparty - depositAmount, // amountToDeposit - depositAmount // counterpartyBalance - ); - return { cfAddr, txFeeA, txFeeB }; -} - -async function deposit( - multisigAddr: string, - depositor: TestResponseSink, - counterparty: TestResponseSink, - amountToDeposit: ethers.utils.BigNumber, - counterpartyBalance: ethers.utils.BigNumber -): Promise<{ cfAddr: string; txFee: ethers.utils.BigNumber }> { - const cfAddr = await installBalanceRefund( - multisigAddr, - depositor, - counterparty, - counterpartyBalance - ); - const txFee = await depositOnChain(multisigAddr, depositor, amountToDeposit); - await uninstallBalanceRefund( - multisigAddr, - cfAddr, - depositor, - counterparty, - amountToDeposit, - counterpartyBalance - ); - return { cfAddr, txFee }; -} - -async function installBalanceRefund( - multisigAddr: string, - depositor: TestResponseSink, - counterparty: TestResponseSink, - threshold: ethers.utils.BigNumber -) { - const msg = startInstallBalanceRefundMsg( - multisigAddr, - depositor.signingKey.address, - counterparty.signingKey.address, - threshold - ); - const response = await depositor.runInstallProtocol( - depositor.signingKey.address, - counterparty.signingKey.address, - multisigAddr, - msg - ); - expect(response.status).toEqual(cf.legacy.node.ResponseStatus.COMPLETED); - // since the machine is async, we need to wait for walletB to finish up its - // side of the protocol before inspecting it's state - - await new Promise(resolve => setTimeout(resolve, 50)); - // check B's client - validateInstalledBalanceRefund(multisigAddr, counterparty, threshold); - // check A's client and return the newly created cf address - return validateInstalledBalanceRefund(multisigAddr, depositor, threshold); -} - -async function depositOnChain( - multisigAddress: string, - wallet: TestResponseSink, - value: ethers.utils.BigNumber -): Promise { - const address = wallet.signingKey.address; - const balanceBefore = await ganache.getBalance(address); - - await new ethers.Wallet( - wallet.signingKey.privateKey, - ganache - ).sendTransaction({ - value, - to: multisigAddress - }); - - const balanceAfter = await ganache.getBalance(address); - // Calculate transaction fee - return balanceAfter.sub(balanceBefore).add(value); -} - -async function uninstallBalanceRefund( - multisigAddr: string, - cfAddr: string, - walletA: TestResponseSink, - walletB: TestResponseSink, - amountA: ethers.utils.BigNumber, - amountB: ethers.utils.BigNumber -) { - const response = await walletA.runUninstallProtocol( - walletA.signingKey.address, - walletB.signingKey.address, - multisigAddr, - [ - new cf.legacy.utils.PeerBalance(walletA.signingKey.address, amountA), - new cf.legacy.utils.PeerBalance(walletB.signingKey.address, 0) - ], - cfAddr - ); - expect(response.status).toEqual(cf.legacy.node.ResponseStatus.COMPLETED); - // validate walletA - validateUninstalledAndFreeBalance( - multisigAddr, - cfAddr, - walletA, - walletB, - amountA, - amountB - ); - // validate walletB - validateUninstalledAndFreeBalance( - multisigAddr, - cfAddr, - walletB, - walletA, - amountB, - amountA - ); -} - -function startInstallBalanceRefundMsg( - multisigAddr: string, - from: string, - to: string, - threshold: ethers.utils.BigNumber -): cf.legacy.app.InstallData { - let peerA = from; - let peerB = to; - if (peerB.localeCompare(peerA) < 0) { - const tmp = peerA; - peerA = peerB; - peerB = tmp; - } - const terms = new cf.legacy.app.Terms( - 0, - ethers.utils.bigNumberify(10), - ethers.constants.AddressZero - ); // todo - - const app = new cf.legacy.app.AppInterface( - "0x0", - "0x00000000", - "0x00000000", - "0x00000000", - "0x00000000", - "" - ); // todo - const timeout = 100; - return { - terms, - app, - timeout, - peerA: new cf.legacy.utils.PeerBalance(peerA, 0), - peerB: new cf.legacy.utils.PeerBalance(peerB, 0), - keyA: peerA, - keyB: peerB, - encodedAppState: "0x1234" - }; -} - -function validateInstalledBalanceRefund( - multisigAddr: string, - wallet: TestResponseSink, - amount: ethers.utils.BigNumber -) { - const stateChannel = - wallet.instructionExecutor.node.channelStates[multisigAddr]; - const appInstances = stateChannel.appInstances; - const cfAddrs = Object.keys(appInstances); - expect(cfAddrs.length).toEqual(1); - - const cfAddr = cfAddrs[0]; - - expect(appInstances[cfAddr].peerA.balance.toNumber()).toEqual(0); - expect(appInstances[cfAddr].peerA.address).toEqual( - stateChannel.freeBalance.alice - ); - expect(appInstances[cfAddr].peerA.balance.toNumber()).toEqual(0); - - expect(appInstances[cfAddr].peerB.balance.toNumber()).toEqual(0); - expect(appInstances[cfAddr].peerB.address).toEqual( - stateChannel.freeBalance.bob - ); - expect(appInstances[cfAddr].peerB.balance.toNumber()).toEqual(0); - - return cfAddr; -} - -/** - * Validates the correctness of walletA's free balance *not* walletB's. - */ -function validateUninstalledAndFreeBalance( - multisigAddr: string, - cfAddr: string, - walletA: TestResponseSink, - walletB: TestResponseSink, - amountAgiven: ethers.utils.BigNumber, - amountBgiven: ethers.utils.BigNumber -) { - // TODO: add nonce and uniqueId params and check them - // https://github.com/counterfactual/monorepo/issues/111 - const state = walletA.instructionExecutor.node; - - let peerA = walletA.signingKey.address; - let peerB = walletB.signingKey.address; - - let amountA = amountAgiven; - let amountB = amountBgiven; - - if (peerB.localeCompare(peerA) < 0) { - const tmp = peerA; - peerA = peerB; - peerB = tmp; - const tmpAmount = amountA; - amountA = amountB; - amountB = tmpAmount; - } - - const channel = - walletA.instructionExecutor.node.channelStates[multisigAddr]; - const app = channel.appInstances[cfAddr]; - - expect(Object.keys(state.channelStates).length).toEqual(1); - expect(channel.counterParty).toEqual(walletB.signingKey.address); - expect(channel.me).toEqual(walletA.signingKey.address); - expect(channel.multisigAddress).toEqual(multisigAddr); - expect(channel.freeBalance.alice).toEqual(peerA); - expect(channel.freeBalance.bob).toEqual(peerB); - expect(channel.freeBalance.aliceBalance.toNumber()).toEqual( - amountA.toNumber() - ); - expect(channel.freeBalance.bobBalance.toNumber()).toEqual(amountB.toNumber()); - - expect(app.dependencyNonce.nonceValue).toEqual(1); -} - diff --git a/packages/machine/test/integration/lifecycle/depositor.ts b/packages/machine/test/integration/lifecycle/depositor.ts deleted file mode 100644 index 464fb0d5c..000000000 --- a/packages/machine/test/integration/lifecycle/depositor.ts +++ /dev/null @@ -1,229 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import { TestResponseSink } from "../test-response-sink"; -import { ethers } from "ethers"; -import { - UNUSED_FUNDED_ACCOUNT -} from "../../utils/environment"; - - -/** - * A collection of staic methods responsible for "depositing", i.e., running - * the intsall protocol with "balance refund/withdraw" app, and ensuring - * the machine state was correctly modified. - */ -export class Depositor { - public static async makeDeposits( - peerA: TestResponseSink, - peerB: TestResponseSink - ): Promise { - await Depositor.deposit( - peerA, - peerB, - ethers.utils.bigNumberify(10), - ethers.utils.bigNumberify(0) - ); - await Depositor.deposit( - peerB, - peerA, - ethers.utils.bigNumberify(5), - ethers.utils.bigNumberify(10) - ); - } - - /** - * @param amountA is the amount wallet A wants to deposit into the channel. - * @param amountBCumulative is the amount wallet B already has in the channel, - * i.e., the threshold for the balance refund. - */ - public static async deposit( - peerA: TestResponseSink, - peerB: TestResponseSink, - amountA: ethers.utils.BigNumber, - amountBCumulative: ethers.utils.BigNumber - ) { - const cfAddr = await Depositor.installBalanceRefund( - peerA, - peerB, - amountBCumulative - ); - await Depositor.uninstallBalanceRefund( - cfAddr, - peerA, - peerB, - amountA, - amountBCumulative - ); - } - - public static async installBalanceRefund( - peerA: TestResponseSink, - peerB: TestResponseSink, - threshold: ethers.utils.BigNumber - ) { - const msg = Depositor.startInstallBalanceRefundMsg( - peerA.signingKey.address!, - peerB.signingKey.address!, - threshold - ); - const response = await peerA.runInstallProtocol( - peerA.signingKey.address!, - peerB.signingKey.address!, - UNUSED_FUNDED_ACCOUNT, - msg - ) - expect(response.status).toEqual(cf.legacy.node.ResponseStatus.COMPLETED); - // since the machine is async, we need to wait for peerB to finish up its - // side of the protocol before inspecting its state - await new Promise(resolve => setTimeout(resolve, 1)); - // check B's client - Depositor.validateInstalledBalanceRefund(peerA, peerB, threshold); - // check A's client and return the newly created cf.legacy.signingKey.address - return Depositor.validateInstalledBalanceRefund(peerA, peerB, threshold); - } - - public static startInstallBalanceRefundMsg( - from: string, - to: string, - threshold: ethers.utils.BigNumber - ): cf.legacy.app.InstallData { - const canon = cf.legacy.utils.PeerBalance.balances( - from, - ethers.utils.bigNumberify(0), - to, - ethers.utils.bigNumberify(0) - ); - const terms = new cf.legacy.app.Terms( - 0, - new ethers.utils.BigNumber(10), - ethers.constants.AddressZero - ); // TODO: - const app = new cf.legacy.app.AppInterface( - "0x0", - "0x11111111", - "0x11111111", - "0x11111111", - "0x11111111", - "" - ); // TODO: - const timeout = 100; - return { - terms, - app, - timeout, - peerA: canon.peerA, - peerB: canon.peerB, - keyA: from, - keyB: to, - encodedAppState: "0x1234" - }; - } - - public static validateInstalledBalanceRefund( - peerA: TestResponseSink, - peerB: TestResponseSink, - amount: ethers.utils.BigNumber - ) { - const stateChannel = - peerA.instructionExecutor.node.channelStates[UNUSED_FUNDED_ACCOUNT]; - expect(stateChannel.me).toEqual(peerA.signingKey.address); - expect(stateChannel.counterParty).toEqual(peerB.signingKey.address); - - const appInstances = stateChannel.appInstances; - const cfAddrs = Object.keys(appInstances); - expect(cfAddrs.length).toEqual(1); - - const cfAddr = cfAddrs[0]; - expect(appInstances[cfAddr].peerA.balance.toNumber()).toEqual(0); - expect(appInstances[cfAddr].peerA.address).toEqual( - stateChannel.freeBalance.alice - ); - expect(appInstances[cfAddr].peerA.balance.toNumber()).toEqual(0); - expect(appInstances[cfAddr].peerB.balance.toNumber()).toEqual(0); - expect(appInstances[cfAddr].peerB.address).toEqual( - stateChannel.freeBalance.bob - ); - expect(appInstances[cfAddr].peerB.balance.toNumber()).toEqual(0); - - return cfAddr; - } - - public static async uninstallBalanceRefund( - cfAddr: string, - peerA: TestResponseSink, - peerB: TestResponseSink, - amountA: ethers.utils.BigNumber, - amountB: ethers.utils.BigNumber - ) { - const response = await peerA.runUninstallProtocol( - peerA.signingKey.address!, - peerB.signingKey.address!, - UNUSED_FUNDED_ACCOUNT, - [ - new cf.legacy.utils.PeerBalance(peerA.signingKey.address!, amountA), - new cf.legacy.utils.PeerBalance(peerB.signingKey.address!, 0) - ], - cfAddr - ) - expect(response.status).toEqual(cf.legacy.node.ResponseStatus.COMPLETED); - // validate peerA - Depositor.validateUninstall(cfAddr, peerA, peerB, amountA, amountB); - // validate peerB - Depositor.validateUninstall(cfAddr, peerB, peerA, amountB, amountA); - } - - public static validateUninstall( - cfAddr: string, - peerA: TestResponseSink, - peerB: TestResponseSink, - amountA: ethers.utils.BigNumber, - amountB: ethers.utils.BigNumber - ) { - // TODO: add nonce and uniqueId params and check them - // https://github.com/counterfactual/monorepo/issues/111 - const state = peerA.instructionExecutor.node; - const canon = cf.legacy.utils.PeerBalance.balances( - peerA.signingKey.address!, - amountA, - peerB.signingKey.address!, - amountB - ); - - const channel = - peerA.instructionExecutor.node.channelStates[UNUSED_FUNDED_ACCOUNT]; - const app = channel.appInstances[cfAddr]; - - expect(Object.keys(state.channelStates).length).toEqual(1); - expect(channel.me).toEqual(peerA.signingKey.address); - expect(channel.counterParty).toEqual(peerB.signingKey.address); - expect(channel.multisigAddress).toEqual(UNUSED_FUNDED_ACCOUNT); - expect(channel.freeBalance.alice).toEqual(canon.peerA.address); - expect(channel.freeBalance.bob).toEqual(canon.peerB.address); - expect(channel.freeBalance.aliceBalance).toEqual(canon.peerA.balance); - expect(channel.freeBalance.bobBalance).toEqual(canon.peerB.balance); - expect(channel.freeBalance.uniqueId).toEqual(0); - expect(app.dependencyNonce.nonceValue).toEqual(1); - } - - public static startUninstallBalanceRefundMsg( - appInstanceId: string, - from: string, - to: string, - amount: ethers.utils.BigNumber - ): cf.legacy.node.ClientActionMessage { - const uninstallData = { - peerAmounts: [ - new cf.legacy.utils.PeerBalance(from, amount), - new cf.legacy.utils.PeerBalance(to, 0) - ] - }; - return { - appInstanceId, - action: cf.legacy.node.ActionName.UNINSTALL, - data: uninstallData, - multisigAddress: UNUSED_FUNDED_ACCOUNT, - fromAddress: from, - toAddress: to, - seq: 0 - }; - } -} diff --git a/packages/machine/test/integration/lifecycle/setup-protocol.ts b/packages/machine/test/integration/lifecycle/setup-protocol.ts deleted file mode 100644 index 088c4bae1..000000000 --- a/packages/machine/test/integration/lifecycle/setup-protocol.ts +++ /dev/null @@ -1,110 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import { ethers } from "ethers"; - -import { UNUSED_FUNDED_ACCOUNT } from "../../utils/environment"; - -import { TestResponseSink } from "../test-response-sink"; - -/** - * A collection of static methods responsible for running the setup potocol - * and asserting the internally stored state was correctly modified. - * - * Note: this expects peerA and peerB to be linked correctly! - */ -export class SetupProtocol { - public static async validateAndRun( - peerA: TestResponseSink, - peerB: TestResponseSink - ) { - SetupProtocol.validatePresetup(peerA, peerB); - await SetupProtocol.run(peerA, peerB); - SetupProtocol.validatePostsetup(peerA, peerB); - } - - /** - * Asserts the state of the given wallets is empty. - */ - public static validatePresetup( - peerA: TestResponseSink, - peerB: TestResponseSink - ) { - expect(peerA.instructionExecutor.node.channelStates).toEqual({}); - expect(peerB.instructionExecutor.node.channelStates).toEqual({}); - } - - public static setupStartMsg( - from: string, - to: string - ): cf.legacy.node.ClientActionMessage { - return { - appInstanceId: "", - action: cf.legacy.node.ActionName.SETUP, - data: {}, - multisigAddress: UNUSED_FUNDED_ACCOUNT, - toAddress: to, - fromAddress: from, - seq: 0 - }; - } - - /** - * Asserts the setup protocol modifies the internally stored state correctly. - */ - public static validatePostsetup( - peerA: TestResponseSink, - peerB: TestResponseSink - ) { - SetupProtocol.validateWallet( - peerA, - peerB, - ethers.utils.bigNumberify(0), - ethers.utils.bigNumberify(0) - ); - SetupProtocol.validateWallet( - peerB, - peerA, - ethers.utils.bigNumberify(0), - ethers.utils.bigNumberify(0) - ); - } - - /** - * Validates the correctness of walletAs free balance *not* walletBs. - */ - public static validateWallet( - peerA: TestResponseSink, - peerB: TestResponseSink, - amountA: ethers.utils.BigNumber, - amountB: ethers.utils.BigNumber - ) { - // TODO: add nonce and uniqueId params and check them - // https://github.com/counterfactual/monorepo/issues/111 - const state = peerA.instructionExecutor.node; - const canon = cf.legacy.utils.PeerBalance.balances( - peerA.signingKey.address, - amountA, - peerB.signingKey.address, - amountB - ); - const channel = - peerA.instructionExecutor.node.channelStates[UNUSED_FUNDED_ACCOUNT]; - expect(Object.keys(state.channelStates).length).toEqual(1); - expect(channel.counterParty).toEqual(peerB.signingKey.address); - expect(channel.me).toEqual(peerA.signingKey.address); - expect(channel.multisigAddress).toEqual(UNUSED_FUNDED_ACCOUNT); - expect(channel.appInstances).toEqual({}); - expect(channel.freeBalance.alice).toEqual(canon.peerA.address); - expect(channel.freeBalance.bob).toEqual(canon.peerB.address); - expect(channel.freeBalance.aliceBalance).toEqual(canon.peerA.balance); - expect(channel.freeBalance.bobBalance).toEqual(canon.peerB.balance); - } - - public static async run(peerA: TestResponseSink, peerB: TestResponseSink) { - const response = await peerA.runSetupProtocol( - peerA.signingKey.address, - peerB.signingKey.address, - UNUSED_FUNDED_ACCOUNT - ); - expect(response.status).toEqual(cf.legacy.node.ResponseStatus.COMPLETED); - } -} diff --git a/packages/machine/test/integration/lifecycle/three-player-lifecycle.spec.ts b/packages/machine/test/integration/lifecycle/three-player-lifecycle.spec.ts deleted file mode 100644 index 688e753d5..000000000 --- a/packages/machine/test/integration/lifecycle/three-player-lifecycle.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - A_PRIVATE_KEY, - B_PRIVATE_KEY, - I_PRIVATE_KEY, - A_ADDRESS, - B_ADDRESS, - I_ADDRESS, - UNUSED_FUNDED_ACCOUNT -} from "../../utils/environment"; - -import { TestResponseSink } from "../test-response-sink"; -import { SetupProtocol } from "./setup-protocol"; -import { Depositor } from "./depositor"; - -/** - * Tests that the machine's State is correctly modified during the lifecycle - * of a state channel application, TicTacToeSimulator, running the setup, install, update, - * and uninstall protocols. - */ -describe("Machine State Lifecycle", async () => { - // extending the timeout to allow the async machines to finish - // and give time to `recoverAddress` to order signing keys right - // for setting commitments - jest.setTimeout(50000); - - it.only("should modify machine state during the lifecycle of TicTacToeSimulator", async () => { - const [peerA, peerB, peerI ]: TestResponseSink[] = getCommunicatingPeers(); - await SetupProtocol.run(peerA, peerI); - await Depositor.makeDeposits(peerA, peerI); - await SetupProtocol.run(peerI, peerB); - await Depositor.makeDeposits(peerI, peerB); - await peerA.runInstallVirtualAppProtocol( - A_ADDRESS, B_ADDRESS, I_ADDRESS, UNUSED_FUNDED_ACCOUNT - ); - }); -}); - -/** - * @returns the wallets containing the machines that will be used for the test. - */ -function getCommunicatingPeers(): TestResponseSink[] { - // TODO: Document somewhere that the .signingKey.address" *must* be a hex otherwise - // machine/src/middleware/node-transition/install-proposer.ts:98:14 - // will throw an error when doing BigNumber.gt check. - // https://github.com/counterfactual/monorepo/issues/110 - - // TODO: Furthermore document that these will eventually be used to generate - // the `signingKeys` in any proposals e.g., InstallProposer, thus the proposal - // will fail if they are not valid Ethereum addresses - // https://github.com/counterfactual/monorepo/issues/109 - const peerA = new TestResponseSink(A_PRIVATE_KEY); - const peerB = new TestResponseSink(B_PRIVATE_KEY); - const peerI = new TestResponseSink(I_PRIVATE_KEY); - - peerA.io.peers.set(B_ADDRESS, peerB); - peerA.io.peers.set(I_ADDRESS, peerI); - - peerB.io.peers.set(A_ADDRESS, peerA); - peerB.io.peers.set(I_ADDRESS, peerI); - - peerI.io.peers.set(A_ADDRESS, peerA); - peerI.io.peers.set(B_ADDRESS, peerB); - - return [peerA, peerB, peerI]; -} - diff --git a/packages/machine/test/integration/lifecycle/tic-tac-toe-simulator.ts b/packages/machine/test/integration/lifecycle/tic-tac-toe-simulator.ts deleted file mode 100644 index 75b8ea3e6..000000000 --- a/packages/machine/test/integration/lifecycle/tic-tac-toe-simulator.ts +++ /dev/null @@ -1,252 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import { ethers } from "ethers"; - -import { - UNUSED_FUNDED_ACCOUNT -} from "../../utils/environment"; - -import { TestResponseSink } from "../test-response-sink"; - -export class TicTacToeSimulator { - public static async simulatePlayingGame( - peerA: TestResponseSink, - peerB: TestResponseSink - ) { - const cfAddr = await TicTacToeSimulator.installTtt(peerA, peerB); - await TicTacToeSimulator.makeMoves(peerA, peerB, cfAddr); - await TicTacToeSimulator.uninstall(peerA, peerB, cfAddr); - return cfAddr; - } - - public static async installTtt( - peerA: TestResponseSink, - peerB: TestResponseSink - ) { - const msg = TicTacToeSimulator.installMsg( - peerA.signingKey.address!, - peerB.signingKey.address! - ); - const response = await peerA.runInstallProtocol( - peerA.signingKey.address!, - peerB.signingKey.address!, - UNUSED_FUNDED_ACCOUNT, - msg - ) - expect(response.status).toEqual(cf.legacy.node.ResponseStatus.COMPLETED); - return TicTacToeSimulator.validateInstall(peerA, peerB); - } - - public static installMsg( - to: string, - from: string - ): cf.legacy.app.InstallData { - let peerA = from; - let peerB = to; - if (peerB.localeCompare(peerA) < 0) { - const tmp = peerA; - peerA = peerB; - peerB = tmp; - } - const terms = new cf.legacy.app.Terms( - 0, - new ethers.utils.BigNumber(10), - ethers.constants.AddressZero - ); // TODO: - const app = new cf.legacy.app.AppInterface( - "0x0", - "0x11111111", - "0x11111111", - "0x11111111", - "0x11111111", - "" - ); // TODO: - const timeout = 100; - return { - terms, - app, - timeout, - peerA: new cf.legacy.utils.PeerBalance(peerA, 2), - peerB: new cf.legacy.utils.PeerBalance(peerB, 2), - keyA: peerA, - keyB: peerB, - encodedAppState: "0x1234" - }; - } - - public static async validateInstall( - peerA: TestResponseSink, - peerB: TestResponseSink - ): Promise { - await new Promise(resolve => setTimeout(resolve, 50)); - const stateChannel = - peerA.instructionExecutor.node.channelStates[UNUSED_FUNDED_ACCOUNT]; - const appInstances = stateChannel.appInstances; - const cfAddrs = Object.keys(appInstances); - expect(cfAddrs.length).toEqual(1); - - // first validate the app - const cfAddr = cfAddrs[0]; - expect(appInstances[cfAddr].peerA.balance.toNumber()).toEqual(2); - expect(appInstances[cfAddr].peerB.balance.toNumber()).toEqual(2); - - TicTacToeSimulator.validateInstallFreeBalance( - peerA.instructionExecutor.node.channelStates[UNUSED_FUNDED_ACCOUNT].freeBalance, - peerA, - peerB - ); - TicTacToeSimulator.validateInstallFreeBalance( - peerB.instructionExecutor.node.channelStates[UNUSED_FUNDED_ACCOUNT].freeBalance, - peerA, - peerB - ); - return cfAddr; - } - - public static validateInstallFreeBalance( - freeBalance: cf.legacy.utils.FreeBalance, - peerA: TestResponseSink, - peerB: TestResponseSink - ) { - // start with 10, 5 and both parties deposit 2 into TicTacToeSimulator. - expect( - freeBalance.balanceOfAddress(peerA.signingKey.address).toNumber() - ).toEqual(8); - expect( - freeBalance.balanceOfAddress(peerB.signingKey.address).toNumber() - ).toEqual(3); - } - - /** - * Game is over at the end of this functon call and is ready to be uninstalled. - */ - public static async makeMoves( - peerA: TestResponseSink, - peerB: TestResponseSink, - cfAddr: string - ) { - const state = [0, 0, 0, 0, 0, 0, 0, 0, 0]; - const X = 1; - const O = 2; - - await TicTacToeSimulator.makeMove(peerA, peerB, cfAddr, state, 0, X, 1); - await TicTacToeSimulator.makeMove(peerB, peerA, cfAddr, state, 4, O, 2); - await TicTacToeSimulator.makeMove(peerA, peerB, cfAddr, state, 1, X, 3); - await TicTacToeSimulator.makeMove(peerB, peerA, cfAddr, state, 5, O, 4); - await TicTacToeSimulator.makeMove(peerA, peerB, cfAddr, state, 2, X, 5); - } - - public static async makeMove( - peerA: TestResponseSink, - peerB: TestResponseSink, - cfAddr: string, - appState: number[], - cell: number, - side: number, - moveNumber: number - ) { - appState[cell] = side; - const state = appState.toString(); - const response = await peerA.runUpdateProtocol( - peerA.signingKey.address!, - peerB.signingKey.address!, - UNUSED_FUNDED_ACCOUNT, - cfAddr, - state, - ethers.constants.HashZero - ) - expect(response.status).toEqual(cf.legacy.node.ResponseStatus.COMPLETED); - TicTacToeSimulator.validateMakeMove( - peerA, - peerB, - cfAddr, - state, - moveNumber - ); - await new Promise(resolve => setTimeout(resolve, 50)); - TicTacToeSimulator.validateMakeMove( - peerB, - peerA, - cfAddr, - state, - moveNumber - ); - } - - public static validateMakeMove( - peerA: TestResponseSink, - peerB: TestResponseSink, - cfAddr, - appState: string, - moveNumber: number - ) { - const appA = - peerA.instructionExecutor.node.channelStates[UNUSED_FUNDED_ACCOUNT] - .appInstances[cfAddr]; - const appB = - peerB.instructionExecutor.node.channelStates[UNUSED_FUNDED_ACCOUNT] - .appInstances[cfAddr]; - - expect(appA.encodedState).toEqual(appState); - expect(appA.localNonce).toEqual(moveNumber + 1); - expect(appB.encodedState).toEqual(appState); - expect(appB.localNonce).toEqual(moveNumber + 1); - } - - public static async uninstall( - peerA: TestResponseSink, - peerB: TestResponseSink, - cfAddr: string - ) { - const response = await peerA.runUninstallProtocol( - peerA.signingKey.address!, - peerB.signingKey.address!, - UNUSED_FUNDED_ACCOUNT, - [ - new cf.legacy.utils.PeerBalance(peerA.signingKey.address!, ethers.utils.bigNumberify(4)), - new cf.legacy.utils.PeerBalance(peerB.signingKey.address!, ethers.utils.bigNumberify(0)) - ], - cfAddr - ) - expect(response.status).toEqual(cf.legacy.node.ResponseStatus.COMPLETED); - // A wins so give him 2 and subtract 2 from B - TicTacToeSimulator.validateUninstall( - cfAddr, - peerA, - ethers.utils.bigNumberify(12), - peerB, - ethers.utils.bigNumberify(3) - ); - } - - public static validateUninstall( - cfAddr: string, - walletA: TestResponseSink, - amountA: ethers.utils.BigNumber, - walletB: TestResponseSink, - amountB: ethers.utils.BigNumber - ) { - TicTacToeSimulator.validateUninstallChannelInfo( - walletA.instructionExecutor.node.channelStates[UNUSED_FUNDED_ACCOUNT], - cfAddr, - walletA, - amountA, - walletB, - amountB - ); - } - - public static validateUninstallChannelInfo( - channel: cf.legacy.channel.StateChannelInfo, - cfAddr: string, - walletA: TestResponseSink, - amountA: ethers.utils.BigNumber, - walletB: TestResponseSink, - amountB: ethers.utils.BigNumber - ) { - const app = channel.appInstances[cfAddr]; - expect(channel.freeBalance.balanceOfAddress(walletA.signingKey.address)).toEqual(amountA); - expect(channel.freeBalance.balanceOfAddress(walletB.signingKey.address)).toEqual(amountB); - expect(channel.freeBalance.uniqueId).toEqual(0); - expect(app.dependencyNonce.nonceValue).toEqual(1); - } -} diff --git a/packages/machine/test/integration/lifecycle/two-player-lifecycle.spec.ts b/packages/machine/test/integration/lifecycle/two-player-lifecycle.spec.ts deleted file mode 100644 index d9ab0980f..000000000 --- a/packages/machine/test/integration/lifecycle/two-player-lifecycle.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { - A_PRIVATE_KEY, - B_PRIVATE_KEY, - A_ADDRESS, - B_ADDRESS, -} from "../../utils/environment"; - -import { TestResponseSink } from "../test-response-sink"; -import { SetupProtocol } from "./setup-protocol"; -import { Depositor } from "./depositor"; -import { TicTacToeSimulator } from "./tic-tac-toe-simulator"; - -/** - * Tests that the machine's State is correctly modified during the lifecycle - * of a state channel application, TicTacToeSimulator, running the setup, install, update, - * and uninstall protocols. - */ -describe("Machine State Lifecycle", async () => { - // extending the timeout to allow the async machines to finish - // and give time to `recoverAddress` to order signing keys right - // for setting commitments - jest.setTimeout(50000); - - it.only("should modify machine state during the lifecycle of TicTacToeSimulator", async () => { - const [peerA, peerB]: TestResponseSink[] = getCommunicatingPeers(); - await SetupProtocol.validateAndRun(peerA, peerB); - await Depositor.makeDeposits(peerA, peerB); - await TicTacToeSimulator.simulatePlayingGame(peerA, peerB); - }); -}); - -/** - * @returns the wallets containing the machines that will be used for the test. - */ -function getCommunicatingPeers(): TestResponseSink[] { - // TODO: Document somewhere that the .signingKey.address" *must* be a hex otherwise - // machine/src/middleware/node-transition/install-proposer.ts:98:14 - // will throw an error when doing BigNumber.gt check. - // https://github.com/counterfactual/monorepo/issues/110 - - // TODO: Furthermore document that these will eventually be used to generate - // the `signingKeys` in any proposals e.g., InstallProposer, thus the proposal - // will fail if they are not valid Ethereum addresses - // https://github.com/counterfactual/monorepo/issues/109 - const peerA = new TestResponseSink(A_PRIVATE_KEY); - const peerB = new TestResponseSink(B_PRIVATE_KEY); - - peerA.io.peers.set(B_ADDRESS, peerB); - peerB.io.peers.set(A_ADDRESS, peerA); - - return [peerA, peerB]; -} - diff --git a/packages/machine/test/integration/setup-then-set-state.spec.ts b/packages/machine/test/integration/setup-then-set-state.spec.ts new file mode 100644 index 000000000..60f7dc736 --- /dev/null +++ b/packages/machine/test/integration/setup-then-set-state.spec.ts @@ -0,0 +1,204 @@ +import AppRegistry from "@counterfactual/contracts/build/contracts/AppRegistry.json"; +import ETHBucket from "@counterfactual/contracts/build/contracts/ETHBucket.json"; +import MinimumViableMultisig from "@counterfactual/contracts/build/contracts/MinimumViableMultisig.json"; +import NonceRegistry from "@counterfactual/contracts/build/contracts/NonceRegistry.json"; +import ProxyFactory from "@counterfactual/contracts/build/contracts/ProxyFactory.json"; +import StateChannelTransaction from "@counterfactual/contracts/build/contracts/StateChannelTransaction.json"; +import { AssetType, NetworkContext } from "@counterfactual/types"; +import dotenv from "dotenv-safe"; +import { Contract, Wallet } from "ethers"; +import { AddressZero, WeiPerEther, Zero } from "ethers/constants"; +import { JsonRpcProvider } from "ethers/providers"; +import { + BigNumber, + hexlify, + Interface, + randomBytes, + SigningKey +} from "ethers/utils"; + +import { + SetStateCommitment, + SetupCommitment +} from "../../src/middleware/protocol-operation"; +import { StateChannel } from "../../src/models"; + +// To be honest, 30000 is an arbitrary large number that has never failed +// to reach the done() call in the test case, not intelligency chosen +const JEST_TEST_WAIT_TIME = 30000; + +// ProxyFactory.createProxy uses assembly `call` so we can't estimate +// gas needed, so we hard-code this number to ensure the tx completes +const CREATE_PROXY_AND_SETUP_GAS = 6e9; + +// Similarly, the SetupCommitment is a `delegatecall`, so we estimate +const SETUP_COMMITMENT_GAS = 6e9; + +// The AppRegistry.setState call _could_ be estimated but we haven't +// written this test to do that yet +const SETSTATE_COMMITMENT_GAS = 6e9; + +let networkId: number; +let provider: JsonRpcProvider; +let wallet: Wallet; + +// TODO: When we add a second use case of this custom mathcer, +// move it and its typigns into somewhere re-usable +declare global { + namespace jest { + interface Matchers { + toBeEq(expected: BigNumber): BigNumber; + } + } +} +expect.extend({ + toBeEq(received: BigNumber, argument: BigNumber) { + return { + pass: received.eq(argument), + message: () => `expected ${received} not to be equal to ${argument}` + }; + } +}); + +// TODO: This will be re-used for all integration tests, so +// move it somewhere re-usable when we add a new test +beforeAll(async () => { + dotenv.config(); + + // Can use ! because dotenv-safe ensures value is set + const host = process.env.DEV_GANACHE_HOST!; + const port = process.env.DEV_GANACHE_PORT!; + const mnemonic = process.env.DEV_GANACHE_MNEMONIC!; + + provider = new JsonRpcProvider(`http://${host}:${port}`); + wallet = Wallet.fromMnemonic(mnemonic).connect(provider); + networkId = (await provider.getNetwork()).chainId; +}); + +/** + * This test suite tests submitting a generated Set State Commitment + * to the blockchain and observing the result + */ +describe("Scenario - open channel, update free balance, on-chain resolution", () => { + jest.setTimeout(JEST_TEST_WAIT_TIME); + + it("should distribute resolution returned from ETH Free Balance App", async done => { + const relevantArtifacts = [ + AppRegistry, + ETHBucket, + NonceRegistry, + StateChannelTransaction + ]; + + const network = { + // Fetches the values from build artifacts of the contracts needed + // for this test and sets the ones we don't care about to 0x0 + MultiSend: AddressZero, + ETHBalanceRefund: AddressZero, + ...relevantArtifacts.reduce( + (accumulator, artifact) => ({ + ...accumulator, + [artifact.contractName]: artifact.networks[networkId].address + }), + {} + ) + } as NetworkContext; + + const signingKeys = [ + new SigningKey(hexlify(randomBytes(32))), + new SigningKey(hexlify(randomBytes(32))) + ].sort((a, b) => + parseInt(a.address, 16) < parseInt(b.address, 16) ? -1 : 1 + ); + + const users = signingKeys.map(x => x.address); + + const proxyFactory = new Contract( + ProxyFactory.networks[networkId].address, + ProxyFactory.abi, + wallet + ); + + proxyFactory.on("ProxyCreation", async proxy => { + let stateChannel = new StateChannel(proxy, users).setupChannel(network); + let freeBalanceETH = stateChannel.getFreeBalanceFor(AssetType.ETH); + + const state = { + alice: stateChannel.multisigOwners[0], + bob: stateChannel.multisigOwners[1], + aliceBalance: WeiPerEther, + bobBalance: WeiPerEther + }; + + stateChannel = stateChannel.setState(freeBalanceETH.id, state); + freeBalanceETH = stateChannel.getFreeBalanceFor(AssetType.ETH); + + const setStateCommitment = new SetStateCommitment( + network, + freeBalanceETH.identity, + freeBalanceETH.encodedLatestState, + freeBalanceETH.nonce, + freeBalanceETH.timeout + ); + + const setStateTx = setStateCommitment.transaction([ + signingKeys[0].signDigest(setStateCommitment.hashToSign()), + signingKeys[1].signDigest(setStateCommitment.hashToSign()) + ]); + + await wallet.sendTransaction({ + ...setStateTx, + gasLimit: SETSTATE_COMMITMENT_GAS + }); + + for (const _ of Array(freeBalanceETH.timeout)) { + await provider.send("evm_mine", []); + } + + const appRegistry = new Contract( + AppRegistry.networks[networkId].address, + AppRegistry.abi, + wallet + ); + + await appRegistry.functions.setResolution( + freeBalanceETH.identity, + freeBalanceETH.appInterface, + freeBalanceETH.encodedLatestState, + freeBalanceETH.encodedTerms + ); + + const setupCommitment = new SetupCommitment( + network, + stateChannel.multisigAddress, + stateChannel.multisigOwners, + stateChannel.getFreeBalanceFor(AssetType.ETH).identity, + stateChannel.getFreeBalanceFor(AssetType.ETH).terms + ); + + const setupTx = setupCommitment.transaction([ + signingKeys[0].signDigest(setupCommitment.hashToSign()), + signingKeys[1].signDigest(setupCommitment.hashToSign()) + ]); + + await wallet.sendTransaction({ to: proxy, value: WeiPerEther.mul(2) }); + + await wallet.sendTransaction({ + ...setupTx, + gasLimit: SETUP_COMMITMENT_GAS + }); + + expect(await provider.getBalance(proxy)).toBeEq(Zero); + expect(await provider.getBalance(users[0])).toBeEq(WeiPerEther); + expect(await provider.getBalance(users[1])).toBeEq(WeiPerEther); + + done(); + }); + + await proxyFactory.functions.createProxy( + MinimumViableMultisig.networks[networkId].address, + new Interface(MinimumViableMultisig.abi).functions.setup.encode([users]), + { gasLimit: CREATE_PROXY_AND_SETUP_GAS } + ); + }); +}); diff --git a/packages/machine/test/integration/test-commitment-store.ts b/packages/machine/test/integration/test-commitment-store.ts deleted file mode 100644 index 7bf5dff37..000000000 --- a/packages/machine/test/integration/test-commitment-store.ts +++ /dev/null @@ -1,255 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import { ethers } from "ethers"; - -import { Context } from "../../src/instruction-executor"; -import { - ProtocolOperation, - Transaction -} from "../../src/middleware/protocol-operation/types"; -import { InternalMessage } from "../../src/types"; - -import { - InMemoryKeyValueStore, - InMemoryKeyValueStoreImpl -} from "./test-key-value-store"; - -interface Commitments { - appId: string; - commitments: Map; - - addCommitment( - action: cf.legacy.node.ActionName, - protocolOperation: ProtocolOperation, - signatures: ethers.utils.Signature[] - ); - - hasCommitment(action: cf.legacy.node.ActionName); - - getTransaction(action: cf.legacy.node.ActionName); -} - -/** - * AppCommitment holds the Commitments for install, update, and uninstall of apps - * which by definition includes the signatures over the operation - * Refer to: https://github.com/counterfactual/machine/blob/master/specs/counterfactual-protocols.md#commitments - */ -export class AppCommitments implements Commitments { - public static deserialize( - appId: string, - serializedCommitments: string - ): AppCommitments { - const commitments = new Map(); - const commitmentObjects = new Map(JSON.parse(serializedCommitments)); - commitmentObjects.forEach((commitment: any, action) => { - commitments.set( - action as cf.legacy.node.ActionName, - new Transaction(commitment.to, commitment.value, commitment.data) - ); - }); - return new AppCommitments(appId, commitments); - } - - public readonly appId: string; - public readonly commitments: Map; - - constructor( - appId: string, - commitments: Map = new Map() - ) { - this.appId = appId; - this.commitments = commitments; - } - - /** - * Adds a commitment for some action on this app. - * @param action - * @param protocolOperation - * @param signatures - */ - public async addCommitment( - action: cf.legacy.node.ActionName, - protocolOperation: ProtocolOperation, - signatures: ethers.utils.Signature[] - ) { - const commitment = protocolOperation.transaction(signatures); - if ( - action !== cf.legacy.node.ActionName.UPDATE && - this.commitments.has(action) - ) { - return; - // FIXME: we should never non-maliciously get to this state - // https://github.com/counterfactual/monorepo/issues/101 - throw Error("Can't reset setup/install/uninstall commitments"); - } - this.commitments.set(action, commitment); - } - - /** - * Determines whether a given action's commitment has been set - * @param action - */ - public async hasCommitment( - action: cf.legacy.node.ActionName - ): Promise { - return this.commitments.has(action); - } - - /** - * Gets an action's commitment for this app - * @param action - */ - public async getTransaction( - action: cf.legacy.node.ActionName - ): Promise { - if (this.commitments.has(action)) { - return this.commitments.get(action)!; - } - throw Error(`App ID: ${this.appId} has no ${action} commitment`); - } - - public serialize(): string { - // FIXME: This is absurd, we shouldn't even be using a Map for this use case - // considering that the keys are all strings anyway. - // https://stackoverflow.com/a/29085474/2680092 - // https://github.com/counterfactual/monorepo/issues/100 - const pairs: [cf.legacy.node.ActionName, Transaction][] = []; - this.commitments.forEach((v, k) => { - pairs.push([k, v]); - }); - return JSON.stringify(pairs); - } -} - -/** - * The store is a mapping of appId to the three types of actions for an app: - * - install - * - update - * - uninstall - * Each action has a cf operation which encapsulates both the actual - * operation and the data that's being operated on. - */ -export class TestCommitmentStore { - public store: InMemoryKeyValueStore; - private appCount: number; - - constructor() { - this.appCount = 0; - this.store = new InMemoryKeyValueStoreImpl(); - } - - /** - * Sets the commitment at the end of a protocol's execution. - * @param internalMessage - * @param next - * @param context - * @throws Error if the counterparty's signature is not set - */ - public async setCommitment( - internalMessage: InternalMessage, - next: Function, - context: Context - ) { - let appId; - const action: cf.legacy.node.ActionName = internalMessage.actionName; - const operation = context.intermediateResults.operation!; - let appCommitments: AppCommitments; - - const incomingMessage = this.incomingMessage(internalMessage, context); - - if (action === cf.legacy.node.ActionName.SETUP) { - appId = internalMessage.clientMessage.multisigAddress; - } else if (action === cf.legacy.node.ActionName.INSTALL) { - const proposal = context.intermediateResults.proposedStateTransition!; - appId = proposal.cfAddr; - } else { - appId = internalMessage.clientMessage.appInstanceId; - } - - if (this.store.has(appId)) { - appCommitments = AppCommitments.deserialize(appId, this.store.get(appId)); - } else { - appCommitments = new AppCommitments(appId); - this.appCount += 1; - this.store.put(appId, Object(appCommitments.serialize())); - } - - const signature = context.intermediateResults.signature!; - - const counterpartySignature = incomingMessage!.signature!; - if ( - counterpartySignature === undefined || - cf.utils.signaturesToBytes(signature) === - cf.utils.signaturesToBytes(counterpartySignature) - ) { - // FIXME: these errors should be handled more gracefully - // https://github.com/counterfactual/monorepo/issues/99 - throw Error( - `Cannot make commitment for operation ${action}. - The counterparty hasn't signed the commitment.` - ); - } - - await appCommitments.addCommitment(action, operation, [ - signature, - counterpartySignature - ]); - this.store.put(appId, Object(appCommitments.serialize())); - next(); - } - - /** - * Returns the last message sent from my peer. - */ - public incomingMessage( - internalMessage: InternalMessage, - context: Context - ): cf.legacy.node.ClientActionMessage | null { - const incomingMessageResult = context.intermediateResults.inbox[0]; - if (incomingMessageResult === undefined) { - return internalMessage.clientMessage; - } - return incomingMessageResult; - } - - /** - * Given an app ID, returns the signed transaction representing the action - * operating over the specified app. - * @param appId - * @param action - * @throws Error If appId doesn't exist in the store - * @throws Error if action doesn't exist for the app - */ - public async getTransaction( - appId: string, - action: cf.legacy.node.ActionName - ): Promise { - if (!this.store.has(appId)) { - throw Error("Invalid app id"); - } - const appCommitments = AppCommitments.deserialize( - appId, - this.store.get(appId) - ); - - return appCommitments.getTransaction(action); - } - - public getAppCount(): number { - return this.appCount; - } - - public appExists(appId: string): boolean { - return this.store.has(appId); - } - - public async appHasCommitment( - appId: string, - action: cf.legacy.node.ActionName - ): Promise { - const appCommitments = AppCommitments.deserialize( - appId, - this.store.get(appId) - ); - return this.store.has(appId) && appCommitments.hasCommitment(action); - } -} diff --git a/packages/machine/test/integration/test-io-provider.ts b/packages/machine/test/integration/test-io-provider.ts deleted file mode 100644 index 9545007a3..000000000 --- a/packages/machine/test/integration/test-io-provider.ts +++ /dev/null @@ -1,101 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; - -import { Context } from "../../src/instruction-executor"; - -import { InternalMessage } from "../../src/types"; - -import { TestResponseSink } from "./test-response-sink"; - -type ClientActionMessageConsumer = (arg: cf.legacy.node.ClientActionMessage) => void; - -export class TestIOProvider { - public messages: cf.legacy.node.ClientActionMessage[]; - - public peers = new Map(); - - // when TestIOProvider::waitForIo is called without pending messages, - // the returned promise's resolve function is captured and stored here - private waitForIoContinuation?: ClientActionMessageConsumer; - - public ackMethod: Function = Object.create(null); - - private parent: TestResponseSink; - - constructor(parent: TestResponseSink) { - this.messages = []; - this.parent = parent; - } - - public receiveMessageFromPeer( - serializedMessage: cf.legacy.node.ClientActionMessage - ) { - const message = serializedMessage; - - if (this.waitForIoContinuation !== undefined) { - this.waitForIoContinuation(message); - // mark this.waitForIoContinuation as resolved - this.waitForIoContinuation = undefined; - } else if (!this.parent.active) { - // if the parent test response sink is not running a protocol - this.parent.active = true; - this.ackMethod(message); - } else { - this.messages.push(message); - } - - } - - public listenOnce( - continuation: ClientActionMessageConsumer, - ) { - - const message = this.messages.pop(); - - if (message !== undefined) { - continuation(message); - } else { - this.waitForIoContinuation = continuation; - } - - } - - public async ioSendMessage( - internalMessage: InternalMessage, - next: Function, - context: Context - ) { - if (context.intermediateResults.outbox.length === 0) { - throw Error("ioSendMessage called with empty outbox"); - } - for (const message of context.intermediateResults.outbox) { - const recipientAddr = message.toAddress; - const recipient = this.peers.get(recipientAddr); - if (recipient === undefined) { - throw Error(`cannot route to ${recipientAddr}`); - } - recipient.receiveMessageFromPeer(message); - } - } - - public async waitForIo( - message: InternalMessage, - next: Function, - context: Context - ): Promise { - - let resolve: ClientActionMessageConsumer; - const promise = new Promise( - r => (resolve = r) - ); - - this.listenOnce( - msg => { - context.intermediateResults.inbox.push(msg); - resolve(msg); - }, - ); - - - return promise; - } -} diff --git a/packages/machine/test/integration/test-key-value-store.ts b/packages/machine/test/integration/test-key-value-store.ts deleted file mode 100644 index 7eeddcb15..000000000 --- a/packages/machine/test/integration/test-key-value-store.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; - -export class InMemoryKeyValueStorePolyfill { - public inMemoryStorage: object; - - constructor() { - this.inMemoryStorage = {}; - } - - public setItem(key: string, value: any) { - this.inMemoryStorage[key] = value; - } - - public getItem(key: string) { - return this.inMemoryStorage[key]; - } -} - -export interface InMemoryKeyValueStore { - get(key: string); - - put(key: string, value: object); - - has(key: string); -} - -export function getInMemoryKeyValueStore() { - try { - // localStorage is not available in Node - // @ts-ignore - return window.localStorage; - } catch (e) { - return new InMemoryKeyValueStorePolyfill(); - } -} - -export class InMemoryKeyValueStoreImpl implements InMemoryKeyValueStore { - public store; - - constructor() { - this.store = getInMemoryKeyValueStore(); - } - - public get(key: string) { - return cf.legacy.utils.serializer.deserialize( - JSON.parse(this.store.getItem(key) || "") - ); - } - - public put(key: string, value: object) { - return this.store.setItem(key, JSON.stringify(value)); - } - - public has(key: string): boolean { - return this.store.getItem(key) !== null; - } -} diff --git a/packages/machine/test/integration/test-response-sink.ts b/packages/machine/test/integration/test-response-sink.ts deleted file mode 100644 index 876503759..000000000 --- a/packages/machine/test/integration/test-response-sink.ts +++ /dev/null @@ -1,227 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import { ethers } from "ethers"; - -import { - Context, - InstructionExecutor, - InstructionExecutorConfig -} from "../../src/instruction-executor"; -import { Opcode } from "../../src/opcodes"; -import { InternalMessage } from "../../src/types"; - -import { TestCommitmentStore } from "./test-commitment-store"; -import { TestIOProvider } from "./test-io-provider"; - -type ResponseConsumer = (arg: cf.legacy.node.Response) => void; - -export class TestResponseSink implements cf.legacy.node.ResponseSink { - public instructionExecutor: InstructionExecutor; - public io: TestIOProvider; - public store: TestCommitmentStore; - public signingKey: ethers.utils.SigningKey; - - public active: Boolean; - - // when TestResponseSink::run(.*)Protocol is called, the returned promise's - // resolve function is captured and stored here - private runProtocolContinuation?: ResponseConsumer; - - constructor( - readonly privateKey: string, - networkContext?: cf.legacy.network.NetworkContext - ) { - this.active = false; - - this.store = new TestCommitmentStore(); - - this.signingKey = new ethers.utils.SigningKey(privateKey); - - if (networkContext === undefined) { - console.warn( - `Defaulting network context to ${ - ethers.constants.AddressZero - } for all contracts.` - ); - } - - // An instance of a InstructionExecutor that will execute protocols. - this.instructionExecutor = new InstructionExecutor( - new InstructionExecutorConfig( - this, - networkContext || cf.legacy.network.EMPTY_NETWORK_CONTEXT - ) - ); - - this.io = new TestIOProvider(this); - - // TODO: Document why this is needed. - // https://github.com/counterfactual/monorepo/issues/108 - this.io.ackMethod = this.instructionExecutor.dispatchReceivedMessage.bind( - this.instructionExecutor - ); - - this.instructionExecutor.register( - Opcode.OP_SIGN, - (message, next, context) => { - const signature = this.signMyUpdate(context); - context.intermediateResults.signature = signature; - } - ); - - this.instructionExecutor.register( - Opcode.OP_SIGN_VALIDATE, - async (message: InternalMessage, next: Function, context: Context) => { - return this.validateSignatures(message, next, context); - } - ); - - this.instructionExecutor.register( - Opcode.IO_SEND, - this.io.ioSendMessage.bind(this.io) - ); - - this.instructionExecutor.register( - Opcode.IO_WAIT, - this.io.waitForIo.bind(this.io) - ); - - this.instructionExecutor.register( - Opcode.STATE_TRANSITION_COMMIT, - this.store.setCommitment.bind(this.store) - ); - } - - public async runUninstallProtocol( - fromAddress: string, - toAddress: string, - multisigAddress: string, - peerAmounts: cf.legacy.utils.PeerBalance[], - appId: string - ): Promise { - this.active = true; - this.instructionExecutor.runUninstallProtocol( - fromAddress, toAddress, multisigAddress, peerAmounts, appId); - return new Promise((resolve) => { - this.runProtocolContinuation = resolve; - }); - } - - public async runUpdateProtocol( - fromAddress: string, - toAddress: string, - multisigAddress: string, - appId: string, - encodedAppState: string, - appStateHash: cf.legacy.utils.H256 - ): Promise { - this.active = true; - const promise = new Promise((resolve, reject) => { - this.runProtocolContinuation = resolve; - }); - this.instructionExecutor.runUpdateProtocol( - fromAddress, toAddress, multisigAddress, appId, encodedAppState, appStateHash); - return promise; - } - - public async runSetupProtocol( - fromAddress: string, - toAddress: string, - multisigAddress: string - ): Promise { - this.active = true; - const promise = new Promise((resolve, reject) => { - this.runProtocolContinuation = resolve; - }); - this.instructionExecutor.runSetupProtocol(fromAddress, toAddress, multisigAddress); - return promise; - } - - public async runInstallVirtualAppProtocol( - fromAddress: string, - toAddress: string, - intermediary: string, - multisigAddress: string - ): Promise { - this.active = true; - const promise = new Promise((resolve, reject) => { - this.runProtocolContinuation = resolve; - }); - this.instructionExecutor.runInstallVirtualAppProtocol( - fromAddress, toAddress, intermediary, multisigAddress); - return promise; - } - - public runInstallProtocol( - fromAddress: string, - toAddress: string, - multisigAddress: string, - installData: cf.legacy.app.InstallData - ): Promise { - this.active = true; - const promise = new Promise((resolve, reject) => { - this.runProtocolContinuation = resolve; - }); - this.instructionExecutor.runInstallProtocol(fromAddress, toAddress, multisigAddress, installData); - return promise; - } - - /** - * Resolves the registered promise so the test can continue. - */ - public sendResponse(res: cf.legacy.node.Response) { - this.active = false; - if (this.runProtocolContinuation) { - this.runProtocolContinuation(res); - this.runProtocolContinuation = undefined; - } - } - - /** - * Called When a peer wants to send an io messge to this wallet. - */ - public receiveMessageFromPeer(incoming: cf.legacy.node.ClientActionMessage) { - this.io.receiveMessageFromPeer(incoming); - } - - private signMyUpdate( - context: Context - ): ethers.utils.Signature { - const operation = context.intermediateResults.operation!; - const digest = operation.hashToSign(); - const { recoveryParam, r, s } = this.signingKey.signDigest(digest); - return { r, s, v: recoveryParam! + 27 }; - } - - private validateSignatures( - message: InternalMessage, - next: Function, - context: Context - ) { - const operation = context.intermediateResults.operation!; - const digest = operation.hashToSign(); - let sig; - const expectedSigningAddress = - message.clientMessage.toAddress === this.signingKey.address - ? message.clientMessage.fromAddress - : message.clientMessage.toAddress; - if (message.clientMessage.signature === undefined) { - // initiator - const incomingMessage = context.intermediateResults.inbox[0]; - sig = incomingMessage.signature; - } else { - // receiver - sig = message.clientMessage.signature; - } - - const recoveredAddress = ethers.utils.recoverAddress(digest, { - v: sig.v, - r: sig.r, - s: sig.s - }); - if (recoveredAddress !== expectedSigningAddress) { - // FIXME: handle this more gracefully - // https://github.com/counterfactual/monorepo/issues/93 - throw Error("Invalid signature"); - } - } -} diff --git a/packages/machine/test/mocks.ts b/packages/machine/test/mocks.ts new file mode 100644 index 000000000..b6739c2cd --- /dev/null +++ b/packages/machine/test/mocks.ts @@ -0,0 +1,3 @@ +export class MockResponseSink { + sendResponse = () => {}; +} diff --git a/packages/machine/test/unit/cf-operations.spec.ts b/packages/machine/test/unit/cf-operations.spec.ts deleted file mode 100644 index b130e91fd..000000000 --- a/packages/machine/test/unit/cf-operations.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import MinimumViableMultisigJson from "@counterfactual/contracts/build/contracts/MinimumViableMultisig.json"; -import { AssetType } from "@counterfactual/contracts/dist/utils"; -import { ethers } from "ethers"; - -import { OpSetup } from "../../src/middleware/protocol-operation"; -import "../../src/middleware/protocol-operation/types"; - -const fakeCtx = new cf.legacy.network.NetworkContext( - "0x1111111111111111111111111111111111111111", - "0x2222222222222222222222222222222222222222", - "0x3333333333333333333333333333333333333333", - "0x4444444444444444444444444444444444444444", - "0x5555555555555555555555555555555555555555", - "0x6666666666666666666666666666666666666666", - "0x7777777777777777777777777777777777777777", - "0x8888888888888888888888888888888888888888" -); -const alice = "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; -const bob = "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; -const multisig = "0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; - -describe("ProtocolOperation subclasses", async () => { - describe("OpSetup", async () => { - it("generates a correct transaction", async () => { - const appInterface = new cf.legacy.app.AppInterface( - "0xDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", - "0x00000000", - "0x00000000", - "0x00000000", - "0x00000000", - "tuple()" - ); - const terms = new cf.legacy.app.Terms( - AssetType.ETH, - ethers.utils.parseEther("1"), - ethers.constants.AddressZero - ); - const cfApp = new cf.legacy.app.AppInstance( - fakeCtx, - multisig, - [alice, bob], - appInterface, - terms, - 100, - 0 - ); - const fakeNonce = new cf.legacy.utils.Nonce(false, 0, 0); - const freeBal = new cf.legacy.utils.FreeBalance( - alice, - ethers.utils.parseEther("0.5"), - bob, - ethers.utils.parseEther("0.5"), - 1, - 0, - 100, - fakeNonce - ); - const op = new OpSetup(fakeCtx, multisig, cfApp, freeBal, fakeNonce); - const transaction = op.transaction([]); - expect(transaction.to).toBe(multisig); - expect(transaction.value).toBe(0); - const txSighash = transaction.data.slice(0, 10); - const expectedSighash = new ethers.utils.Interface( - MinimumViableMultisigJson.abi - ).functions.execTransaction.sighash; - expect(txSighash).toBe(expectedSighash); - }); - }); -}); diff --git a/packages/machine/test/unit/cf-operations/fixture.ts b/packages/machine/test/unit/cf-operations/fixture.ts deleted file mode 100644 index 0689d6a23..000000000 --- a/packages/machine/test/unit/cf-operations/fixture.ts +++ /dev/null @@ -1,152 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import { ethers } from "ethers"; - -export const TEST_NETWORK_CONTEXT = new cf.legacy.network.NetworkContext( - "0x1111111111111111111111111111111111111111", - "0x2222222222222222222222222222222222222222", - "0x3333333333333333333333333333333333333333", - "0x4444444444444444444444444444444444444444", - "0x5555555555555555555555555555555555555555", - "0x6666666666666666666666666666666666666666", - "0x7777777777777777777777777777777777777777", - "0x8888888888888888888888888888888888888888" -); -export const TEST_MULTISIG_ADDRESS = ethers.utils.hexlify( - "0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" -); -export const TEST_SIGNING_KEYS = [ - // 0xaeF082d339D227646DB914f0cA9fF02c8544F30b - new ethers.utils.SigningKey( - "0x3570f77380e22f8dc2274d8fd33e7830cc2d29cf76804e8c21f4f7a6cc571d27" - ), - // 0xb37e49bFC97A948617bF3B63BC6942BB15285715 - new ethers.utils.SigningKey( - "0x4ccac8b1e81fb18a98bbaf29b9bfe307885561f71b76bd4680d7aec9d0ddfcfd" - ) -]; -export const TEST_PARTICIPANTS = [ - TEST_SIGNING_KEYS[0].address, - TEST_SIGNING_KEYS[1].address -]; -export const TEST_APP_UNIQUE_ID = 13; -export const TEST_TERMS = new cf.legacy.app.Terms( - 0, - ethers.utils.parseEther("1.0"), - ethers.constants.AddressZero -); -export const TEST_APP_INTERFACE = new cf.legacy.app.AppInterface( - ethers.constants.AddressZero, - "0x00000000", - "0x00000000", - "0x00000000", - "0x00000000", - "tuple(address,uint256)" -); -export const TEST_APP_STATE_HASH = ethers.utils.hexlify( - "0x9999999999999999999999999999999999999999999999999999999999999999" -); -export const TEST_LOCAL_NONCE = 0; -export const TEST_TIMEOUT = 100; - -export const TEST_FREE_BALANCE = new cf.legacy.utils.FreeBalance( - TEST_SIGNING_KEYS[0].address, - ethers.utils.parseEther("0.5"), - TEST_SIGNING_KEYS[1].address, - ethers.utils.parseEther("0.5"), - 0, - 10, - 100, - new cf.legacy.utils.Nonce(true, 0, 5) -); - -export const TEST_FREE_BALANCE_APP_INTERFACE = cf.legacy.utils.FreeBalance.contractInterface( - TEST_NETWORK_CONTEXT -); - -export const TEST_FREE_BALANCE_APP_INSTANCE = new cf.legacy.app.AppInstance( - TEST_NETWORK_CONTEXT, - TEST_MULTISIG_ADDRESS, - TEST_PARTICIPANTS, - TEST_FREE_BALANCE_APP_INTERFACE, - cf.legacy.utils.FreeBalance.terms(), - TEST_FREE_BALANCE.timeout, - TEST_FREE_BALANCE.uniqueId -); - -export const TEST_APP_INSTANCE = new cf.legacy.app.AppInstance( - TEST_NETWORK_CONTEXT, - TEST_MULTISIG_ADDRESS, - TEST_PARTICIPANTS, - TEST_APP_INTERFACE, - TEST_TERMS, - TEST_TIMEOUT, - TEST_APP_UNIQUE_ID -); - -/** - * Construct contract call data - * @param funcSig Function signature ABI encoding e.g. "execTransaction(address, uint256, bytes, uint8, bytes)" - * @param args Arguments to pass to contract call - */ -export function constructContractCall(funcSig: string, ...args: any[]): string { - const [funcName] = funcSig.split("("); - return new ethers.utils.Interface([funcSig]).functions[funcName].encode(args); -} - -/** - * Concatenate calls into multiSend() call - * @param subcalls Transaction data of subcalls - */ -export function constructMultiSend(subcalls: string[]): string { - return constructContractCall( - "multiSend(bytes)", - `0x${subcalls.map(call => call.substr(2)).join("")}` - ); -} - -/** - * Construct contract call to be passed to multiSend(). - * @param operation Type of operation to execute: call or delegatecall - * @param to Address of contract to call - * @param value Value to send - * @param funcSig Function signature ABI encoding e.g. "setState(bytes32, uint256, uint256, bytes)" - * @param args Arguments to pass to contract call - */ -export function constructMultiSendSubCall( - operation: "delegatecall" | "call", - to: string, - value: string | number, - funcSig: string, - args: any[] -): string { - const data = constructContractCall(funcSig, ...args); - return ethers.utils.defaultAbiCoder.encode( - ["uint8", "address", "uint256", "bytes"], - [operation === "delegatecall" ? 1 : 0, to, value, data] - ); -} - -/** - * Construct call to Multisig execTransaction() proxy function - * @param operation Type of operation to execute: call or delegatecall - * @param to Address of contract to call - * @param value Value to send - * @param transactionData Transaction data of call - * @param signatures Signatures of multisig owners for this call - */ -export function constructMultisigExecTransaction( - operation: "delegatecall" | "call", - to: string, - value: string | number, - transactionData: string, - signatures: ethers.utils.Signature[] -): string { - return constructContractCall( - "execTransaction(address, uint256, bytes, uint8, bytes)", - to, - value, - transactionData, - operation === "delegatecall" ? 1 : 0, - cf.utils.signaturesToBytes(...signatures) - ); -} diff --git a/packages/machine/test/unit/cf-operations/install.spec.ts b/packages/machine/test/unit/cf-operations/install.spec.ts deleted file mode 100644 index e4337bf22..000000000 --- a/packages/machine/test/unit/cf-operations/install.spec.ts +++ /dev/null @@ -1,121 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import { keccak256 } from "ethers/utils"; - -import { OpInstall } from "../../../src/middleware/protocol-operation"; - -import { - constructContractCall, - constructMultiSend, - constructMultiSendSubCall, - constructMultisigExecTransaction, - TEST_APP_INSTANCE, - TEST_FREE_BALANCE, - TEST_FREE_BALANCE_APP_INSTANCE, - TEST_MULTISIG_ADDRESS, - TEST_NETWORK_CONTEXT, - TEST_SIGNING_KEYS, - TEST_TERMS -} from "./fixture"; - -function constructFreeBalanceInput() { - const { - alice, - bob, - aliceBalance, - bobBalance, - localNonce, - timeout - } = TEST_FREE_BALANCE; - const appStateHash = keccak256( - cf.utils.abi.encode( - ["address", "address", "uint256", "uint256"], - [alice, bob, aliceBalance, bobBalance] - ) - ); - return constructMultiSendSubCall( - "delegatecall", - TEST_NETWORK_CONTEXT.registryAddr, - 0, - "proxyCall(address,bytes32,bytes)", - [ - TEST_NETWORK_CONTEXT.registryAddr, - TEST_FREE_BALANCE_APP_INSTANCE.cfAddress(), - constructContractCall( - "setState(bytes32,uint256,uint256,bytes)", - appStateHash, - localNonce, - timeout, - "0x0" - ) - ] - ); -} - -function constructConditionalTransferInput(nonceUniqueId: number) { - const salt = keccak256( - cf.utils.abi.encodePacked(["uint256"], [nonceUniqueId]) - ); - const uninstallKey = keccak256( - cf.utils.abi.encodePacked( - ["address", "uint256", "uint256"], - [TEST_MULTISIG_ADDRESS, 0, salt] - ) - ); - const { assetType, limit, token } = TEST_TERMS; - return constructMultiSendSubCall( - "delegatecall", - TEST_NETWORK_CONTEXT.conditionalTransactionAddr, - 0, - "executeAppConditionalTransaction(address,address,bytes32,bytes32,tuple(uint8,uint256,address))", - [ - TEST_NETWORK_CONTEXT.registryAddr, - TEST_NETWORK_CONTEXT.nonceRegistryAddr, - uninstallKey, - TEST_APP_INSTANCE.cfAddress(), - [assetType, limit, token] - ] - ); -} - -function constructInstallMultiSendData(nonceUniqueId: number) { - const freeBalanceInput = constructFreeBalanceInput(); - const conditionalTransactionInput = constructConditionalTransferInput( - nonceUniqueId - ); - return constructMultiSend([freeBalanceInput, conditionalTransactionInput]); -} - -describe("OpInstall", () => { - const TEST_NONCE_UNIQUE_ID = 1; - let operation: OpInstall; - - beforeEach(() => { - operation = new OpInstall( - TEST_NETWORK_CONTEXT, - TEST_MULTISIG_ADDRESS, - TEST_APP_INSTANCE, - TEST_FREE_BALANCE, - new cf.legacy.utils.Nonce(true, TEST_NONCE_UNIQUE_ID, 0) - ); - }); - - // https://specs.counterfactual.com/04-setup-protocol#commitments - it("Should be able to compute the correct tx to submit on-chain", () => { - const digest = operation.hashToSign(); - const signatures = TEST_SIGNING_KEYS.map(key => key.signDigest(digest)); - - const multiSendData = constructInstallMultiSendData(TEST_NONCE_UNIQUE_ID); - const expectedTxData = constructMultisigExecTransaction( - "delegatecall", - TEST_NETWORK_CONTEXT.multiSendAddr, - 0, // value - multiSendData, - signatures - ); - - const tx = operation.transaction(signatures); - expect(tx.to).toBe(TEST_MULTISIG_ADDRESS); - expect(tx.value).toBe(0); - expect(tx.data).toBe(expectedTxData); - }); -}); diff --git a/packages/machine/test/unit/cf-operations/set-state.spec.ts b/packages/machine/test/unit/cf-operations/set-state.spec.ts deleted file mode 100644 index a003274e8..000000000 --- a/packages/machine/test/unit/cf-operations/set-state.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import { ethers } from "ethers"; - -import { OpSetState } from "../../../src/middleware/protocol-operation"; - -import { - constructContractCall, - TEST_APP_INSTANCE, - TEST_APP_INTERFACE, - TEST_APP_STATE_HASH, - TEST_APP_UNIQUE_ID, - TEST_LOCAL_NONCE, - TEST_MULTISIG_ADDRESS, - TEST_NETWORK_CONTEXT, - TEST_PARTICIPANTS, - TEST_SIGNING_KEYS, - TEST_TERMS, - TEST_TIMEOUT -} from "./fixture"; - -function constructSetStateData(signatures: ethers.utils.Signature[]): string { - return constructContractCall( - "proxyCall(address,bytes32,bytes)", - TEST_NETWORK_CONTEXT.registryAddr, - TEST_APP_INSTANCE.cfAddress(), - constructContractCall( - "setState(bytes32,uint256,uint256,bytes)", - TEST_APP_STATE_HASH, - TEST_LOCAL_NONCE, - TEST_TIMEOUT, - cf.utils.signaturesToBytes(...signatures) - ) - ); -} - -describe("OpSetState", () => { - let operation: OpSetState; - - beforeEach(() => { - operation = new OpSetState( - TEST_NETWORK_CONTEXT, - TEST_MULTISIG_ADDRESS, - TEST_PARTICIPANTS, - TEST_APP_STATE_HASH, - TEST_APP_UNIQUE_ID, - TEST_TERMS, - TEST_APP_INTERFACE, - TEST_LOCAL_NONCE, - TEST_TIMEOUT - ); - }); - - it("Should be able to compute the correct tx to submit on-chain", () => { - const digest = operation.hashToSign(); - const signatures = TEST_SIGNING_KEYS.map(key => key.signDigest(digest)); - - const expectedTxData = constructSetStateData(signatures); - - const tx = operation.transaction(signatures); - expect(tx.to).toBe(TEST_NETWORK_CONTEXT.registryAddr); - expect(tx.value).toBe(0); - expect(tx.data).toBe(expectedTxData); - }); - - // https://specs.counterfactual.com/06-update-protocol#commitments - it("Should compute the correct hash to sign", () => { - expect(operation.hashToSign()).toBe( - ethers.utils.keccak256( - cf.utils.abi.encodePacked( - ["bytes1", "address[]", "uint256", "uint256", "bytes32"], - [ - "0x19", - TEST_PARTICIPANTS, - TEST_LOCAL_NONCE, - TEST_TIMEOUT, - TEST_APP_STATE_HASH - ] - ) - ) - ); - }); -}); diff --git a/packages/machine/test/unit/cf-operations/setup.spec.ts b/packages/machine/test/unit/cf-operations/setup.spec.ts deleted file mode 100644 index e67269684..000000000 --- a/packages/machine/test/unit/cf-operations/setup.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import { ethers } from "ethers"; - -import { OpSetup } from "../../../src/middleware/protocol-operation"; - -import { - constructContractCall, - constructMultisigExecTransaction, - TEST_FREE_BALANCE, - TEST_FREE_BALANCE_APP_INSTANCE, - TEST_MULTISIG_ADDRESS, - TEST_NETWORK_CONTEXT, - TEST_SIGNING_KEYS -} from "./fixture"; - -const { keccak256 } = ethers.utils; - -function constructConditionalTransactionData(nonceUniqueId: number) { - const salt = keccak256( - cf.utils.abi.encodePacked(["uint256"], [nonceUniqueId]) - ); - const uninstallKey = keccak256( - cf.utils.abi.encodePacked( - ["address", "uint256", "uint256"], - [TEST_MULTISIG_ADDRESS, 0, salt] - ) - ); - - const { terms } = TEST_FREE_BALANCE_APP_INSTANCE; - return constructContractCall( - "executeAppConditionalTransaction(address,address,bytes32,bytes32,tuple(uint8,uint256,address))", - TEST_NETWORK_CONTEXT.registryAddr, - TEST_NETWORK_CONTEXT.nonceRegistryAddr, - uninstallKey, - TEST_FREE_BALANCE_APP_INSTANCE.cfAddress(), - [terms.assetType, terms.limit, terms.token] - ); -} - -describe("OpSetup", () => { - const TEST_NONCE_UNIQUE_ID = 1; - let operation: OpSetup; - - beforeEach(() => { - operation = new OpSetup( - TEST_NETWORK_CONTEXT, - TEST_MULTISIG_ADDRESS, - TEST_FREE_BALANCE_APP_INSTANCE, - TEST_FREE_BALANCE, - new cf.legacy.utils.Nonce(true, TEST_NONCE_UNIQUE_ID, 0) - ); - }); - - // https://specs.counterfactual.com/04-setup-protocol#commitments - it("Should be able to compute the correct tx to submit on-chain", () => { - const conditionalTransactionData = constructConditionalTransactionData( - TEST_NONCE_UNIQUE_ID - ); - - const digest = operation.hashToSign(); - const signatures = TEST_SIGNING_KEYS.map(key => key.signDigest(digest)); - const expectedTxData = constructMultisigExecTransaction( - "delegatecall", - TEST_NETWORK_CONTEXT.conditionalTransactionAddr, - 0, - conditionalTransactionData, - signatures - ); - - const tx = operation.transaction(signatures); - expect(tx.to).toBe(TEST_MULTISIG_ADDRESS); - expect(tx.value).toBe(0); - expect(tx.data).toBe(expectedTxData); - }); -}); diff --git a/packages/machine/test/unit/ethereum/install.spec.ts b/packages/machine/test/unit/ethereum/install.spec.ts new file mode 100644 index 000000000..1f6c7adc5 --- /dev/null +++ b/packages/machine/test/unit/ethereum/install.spec.ts @@ -0,0 +1,244 @@ +import AppRegistry from "@counterfactual/contracts/build/contracts/AppRegistry.json"; +import MultiSend from "@counterfactual/contracts/build/contracts/MultiSend.json"; +import StateChannelTransaction from "@counterfactual/contracts/build/contracts/StateChannelTransaction.json"; +import { AssetType, NetworkContext } from "@counterfactual/types"; +import { AddressZero, HashZero, WeiPerEther, Zero } from "ethers/constants"; +import { + bigNumberify, + getAddress, + hexlify, + Interface, + randomBytes, + TransactionDescription +} from "ethers/utils"; + +import { InstallCommitment } from "../../../src/middleware/protocol-operation"; +import { MultisigTransaction } from "../../../src/middleware/protocol-operation/types"; +import { appIdentityToHash } from "../../../src/middleware/protocol-operation/utils/app-identity"; +import { decodeMultisendCalldata } from "../../../src/middleware/protocol-operation/utils/multisend-decoder"; +import { AppInstance, StateChannel } from "../../../src/models"; + +/** + * This test suite decodes a constructed OpInstall transaction object according + * to the specifications defined by Counterfactual as can be found here: + * https://specs.counterfactual.com/05-install-protocol#commitments + */ +describe("InstallCommitment", () => { + let tx: MultisigTransaction; + + // Test network context + const networkContext: NetworkContext = { + ETHBucket: getAddress(hexlify(randomBytes(20))), + StateChannelTransaction: getAddress(hexlify(randomBytes(20))), + MultiSend: getAddress(hexlify(randomBytes(20))), + NonceRegistry: getAddress(hexlify(randomBytes(20))), + AppRegistry: getAddress(hexlify(randomBytes(20))), + ETHBalanceRefund: getAddress(hexlify(randomBytes(20))) + }; + + // General interaction testing values + const interaction = { + sender: getAddress(hexlify(randomBytes(20))), + receiver: getAddress(hexlify(randomBytes(20))) + }; + + // State channel testing values + let stateChannel = new StateChannel( + getAddress(hexlify(randomBytes(20))), + [interaction.sender, interaction.receiver].sort((a, b) => + parseInt(a, 16) < parseInt(b, 16) ? -1 : 1 + ) + ).setupChannel(networkContext); + + // Set the state to some test values + stateChannel = stateChannel.setState( + stateChannel.getFreeBalanceFor(AssetType.ETH).id, + { + alice: stateChannel.multisigOwners[0], + bob: stateChannel.multisigOwners[1], + aliceBalance: WeiPerEther, + bobBalance: WeiPerEther + } + ); + + const freeBalanceETH = stateChannel.getFreeBalanceFor(AssetType.ETH); + + const app = new AppInstance( + stateChannel.multisigAddress, + [ + getAddress(hexlify(randomBytes(20))), + getAddress(hexlify(randomBytes(20))) + ], + Math.ceil(1000 * Math.random()), + { + addr: getAddress(hexlify(randomBytes(20))), + applyAction: hexlify(randomBytes(4)), + resolve: hexlify(randomBytes(4)), + isStateTerminal: hexlify(randomBytes(4)), + getTurnTaker: hexlify(randomBytes(4)), + stateEncoding: "tuple(address foo, uint256 bar)", + actionEncoding: undefined + }, + { + assetType: AssetType.ETH, + limit: bigNumberify(2), + token: AddressZero + }, + false, + stateChannel.numInstalledApps + 1, + { foo: AddressZero, bar: 0 }, + 0, + Math.ceil(1000 * Math.random()) + ); + + beforeAll(() => { + tx = new InstallCommitment( + networkContext, + stateChannel.multisigAddress, + stateChannel.multisigOwners, + app.identity, + app.terms, + freeBalanceETH.identity, + freeBalanceETH.terms, + freeBalanceETH.hashOfLatestState, + freeBalanceETH.nonce, + freeBalanceETH.timeout, + stateChannel.numInstalledApps + 1 + ).getTransactionDetails(); + }); + + it("should be to MultiSend", () => { + expect(tx.to).toBe(networkContext.MultiSend); + }); + + it("should have no value", () => { + expect(tx.value).toBe(0); + }); + + describe("the calldata of the multisend transaction", () => { + let transactions: [number, string, number, string][]; + + beforeAll(() => { + const { data } = tx; + const desc = new Interface(MultiSend.abi).parseTransaction({ data }); + transactions = decodeMultisendCalldata(desc.args[0]); + }); + + it("should contain two transactions", () => { + expect(transactions.length).toBe(2); + }); + + describe("the transaction to update the free balance", () => { + let to: string; + let val: number; + let data: string; + let op: number; + + beforeAll(() => { + [op, to, val, data] = transactions[0]; + }); + + it("should be to the AppRegistry", () => { + expect(to).toBe(networkContext.AppRegistry); + }); + + it("should be of value 0", () => { + expect(val).toEqual(Zero); + }); + + it("should be a Call", () => { + expect(op).toBe(0); + }); + + describe("the calldata", () => { + let iface: Interface; + let calldata: TransactionDescription; + + beforeAll(() => { + iface = new Interface(AppRegistry.abi); + calldata = iface.parseTransaction({ data }); + }); + + it("should be directed at the setState method", () => { + expect(calldata.sighash).toBe(iface.functions.setState.sighash); + }); + + it("should build the expected AppIdentity argument", () => { + const [ + [owner, signingKeys, appInterfaceHash, termsHash, defaultTimeout] + ] = calldata.args; + const expected = freeBalanceETH.identity; + expect(owner).toBe(expected.owner); + expect(signingKeys).toEqual(expected.signingKeys); + expect(appInterfaceHash).toBe(expected.appInterfaceHash); + expect(termsHash).toBe(expected.termsHash); + expect(defaultTimeout).toEqual(bigNumberify(expected.defaultTimeout)); + }); + + it("should build the expected SignedStateHashUpdate argument", () => { + const [, [stateHash, nonce, timeout, signatures]] = calldata.args; + expect(stateHash).toBe(freeBalanceETH.hashOfLatestState); + expect(nonce).toEqual(bigNumberify(freeBalanceETH.nonce)); + expect(timeout).toEqual(bigNumberify(freeBalanceETH.timeout)); + expect(signatures).toBe(HashZero); + }); + }); + }); + + describe("the transaction to execute the conditional transaction", () => { + let to: string; + let val: number; + let data: string; + let op: number; + + beforeAll(() => { + [op, to, val, data] = transactions[1]; + }); + + it("should be to the StateChannelTransaction", () => { + expect(to).toBe(networkContext.StateChannelTransaction); + }); + + it("should be of value 0", () => { + expect(val).toEqual(Zero); + }); + + it("should be a DelegateCall", () => { + expect(op).toBe(1); + }); + + describe("the calldata", () => { + let iface: Interface; + let calldata: TransactionDescription; + + beforeAll(() => { + iface = new Interface(StateChannelTransaction.abi); + calldata = iface.parseTransaction({ data }); + }); + + it("should be directed at the executeAppConditionalTransaction method", () => { + expect(calldata.sighash).toBe( + iface.functions.executeAppConditionalTransaction.sighash + ); + }); + + it("should have correctly constructed arguments", () => { + const [ + appRegistryAddress, + nonceRegistryAddress, + uninstallKey, + appInstanceId, + terms + ] = calldata.args; + expect(appRegistryAddress).toBe(networkContext.AppRegistry); + expect(nonceRegistryAddress).toBe(networkContext.NonceRegistry); + expect(uninstallKey).toBe(app.uninstallKey); + expect(appInstanceId).toBe(appIdentityToHash(app.identity)); + expect(terms[0]).toBe(app.terms.assetType); + expect(terms[1]).toEqual(app.terms.limit); + expect(terms[2]).toBe(app.terms.token); + }); + }); + }); + }); +}); diff --git a/packages/machine/test/unit/ethereum/set-state.spec.ts b/packages/machine/test/unit/ethereum/set-state.spec.ts new file mode 100644 index 000000000..6c805c6bc --- /dev/null +++ b/packages/machine/test/unit/ethereum/set-state.spec.ts @@ -0,0 +1,148 @@ +import AppRegistry from "@counterfactual/contracts/build/contracts/AppRegistry.json"; +import { AssetType, NetworkContext } from "@counterfactual/types"; +import { AddressZero } from "ethers/constants"; +import { + bigNumberify, + getAddress, + hexlify, + Interface, + keccak256, + randomBytes, + solidityPack, + TransactionDescription +} from "ethers/utils"; + +import { SetStateCommitment } from "../../../src/middleware/protocol-operation"; +import { Transaction } from "../../../src/middleware/protocol-operation/types"; +import { appIdentityToHash } from "../../../src/middleware/protocol-operation/utils/app-identity"; +import { AppInstance } from "../../../src/models"; + +/** + * This test suite decodes a constructed SetState Commitment transaction object + * to the specifications defined by Counterfactual as can be found here: + * https://specs.counterfactual.com/06-update-protocol#commitments + */ +describe("Set State Commitment", () => { + let commitment: SetStateCommitment; + let tx: Transaction; + + // Test network context + const networkContext: NetworkContext = { + ETHBucket: getAddress(hexlify(randomBytes(20))), + StateChannelTransaction: getAddress(hexlify(randomBytes(20))), + MultiSend: getAddress(hexlify(randomBytes(20))), + NonceRegistry: getAddress(hexlify(randomBytes(20))), + AppRegistry: getAddress(hexlify(randomBytes(20))), + ETHBalanceRefund: getAddress(hexlify(randomBytes(20))) + }; + + const app = new AppInstance( + getAddress(hexlify(randomBytes(20))), + [ + getAddress(hexlify(randomBytes(20))), + getAddress(hexlify(randomBytes(20))) + ], + Math.ceil(1000 * Math.random()), + { + addr: getAddress(hexlify(randomBytes(20))), + applyAction: hexlify(randomBytes(4)), + resolve: hexlify(randomBytes(4)), + isStateTerminal: hexlify(randomBytes(4)), + getTurnTaker: hexlify(randomBytes(4)), + stateEncoding: "tuple(address foo, uint256 bar)", + actionEncoding: undefined + }, + { + assetType: AssetType.ETH, + limit: bigNumberify(2), + token: AddressZero + }, + false, + Math.ceil(1000 * Math.random()), + { foo: AddressZero, bar: 0 }, + 0, + Math.ceil(1000 * Math.random()) + ); + + beforeAll(() => { + commitment = new SetStateCommitment( + networkContext, + app.identity, + app.encodedLatestState, + app.nonce, + app.timeout + ); + // TODO: (question) Should there be a way to retrieve the version + // of this transaction sent to the multisig vs sent + // directly to the app registry? + tx = commitment.transaction([ + /* NOTE: Passing in no signatures for test only */ + ]); + }); + + it("should be to AppRegistry", () => { + expect(tx.to).toBe(networkContext.AppRegistry); + }); + + it("should have no value", () => { + expect(tx.value).toBe(0); + }); + + describe("the calldata", () => { + const iface = new Interface(AppRegistry.abi); + let desc: TransactionDescription; + + beforeAll(() => { + const { data } = tx; + desc = iface.parseTransaction({ data }); + }); + + it("should be to the setState method", () => { + expect(desc.sighash).toBe(iface.functions.setState.sighash); + }); + + it("should contain expected AppIdentity argument", () => { + const [ + owner, + signingKeys, + appInterfaceHash, + termsHash, + defaultTimeout + ] = desc.args[0]; + expect(owner).toBe(app.identity.owner); + expect(signingKeys).toEqual(app.identity.signingKeys); + expect(appInterfaceHash).toBe(app.identity.appInterfaceHash); + expect(termsHash).toBe(app.identity.termsHash); + expect(defaultTimeout).toEqual(bigNumberify(app.identity.defaultTimeout)); + }); + + it("should contain expected SignedStateHashUpdate argument", () => { + const [stateHash, nonce, timeout, []] = desc.args[1]; + expect(stateHash).toBe(app.hashOfLatestState); + expect(nonce).toEqual(bigNumberify(app.nonce)); + expect(timeout).toEqual(bigNumberify(app.timeout)); + }); + }); + + it("should produce the correct hash to sign", () => { + const hashToSign = commitment.hashToSign(); + + // Based on MAppRegistryCore::computeStateHash + // TODO: Probably should be able to compute this from some helper + // function ... maybe an AppRegistry class or something + const expectedHashToSign = keccak256( + solidityPack( + ["bytes1", "bytes32", "uint256", "uint256", "bytes32"], + [ + "0x19", + appIdentityToHash(app.identity), + app.nonce, + app.timeout, + app.hashOfLatestState + ] + ) + ); + + expect(hashToSign).toBe(expectedHashToSign); + }); +}); diff --git a/packages/machine/test/unit/ethereum/setup.spec.ts b/packages/machine/test/unit/ethereum/setup.spec.ts new file mode 100644 index 000000000..5537cea32 --- /dev/null +++ b/packages/machine/test/unit/ethereum/setup.spec.ts @@ -0,0 +1,104 @@ +import StateChannelTransaction from "@counterfactual/contracts/build/contracts/StateChannelTransaction.json"; +import { AssetType, NetworkContext } from "@counterfactual/types"; +import { + getAddress, + hexlify, + Interface, + randomBytes, + TransactionDescription +} from "ethers/utils"; + +import { SetupCommitment } from "../../../src/middleware/protocol-operation"; +import { MultisigTransaction } from "../../../src/middleware/protocol-operation/types"; +import { appIdentityToHash } from "../../../src/middleware/protocol-operation/utils/app-identity"; +import { StateChannel } from "../../../src/models"; + +/** + * This test suite decodes a constructed SetupCommitment transaction object according + * to the specifications defined by Counterfactual as can be found here: + * https://specs.counterfactual.com/04-setup-protocol#commitments + * + * TODO: This test suite _only_ covers the conditional transaction from the specs + * above. This is because the root nonce setNonce transaction has not been + * implemented in OpSetuptup yet. + */ +describe("SetupCommitment", () => { + let tx: MultisigTransaction; + + // Test network context + const networkContext: NetworkContext = { + ETHBucket: getAddress(hexlify(randomBytes(20))), + StateChannelTransaction: getAddress(hexlify(randomBytes(20))), + MultiSend: getAddress(hexlify(randomBytes(20))), + NonceRegistry: getAddress(hexlify(randomBytes(20))), + AppRegistry: getAddress(hexlify(randomBytes(20))), + ETHBalanceRefund: getAddress(hexlify(randomBytes(20))) + }; + + // General interaction testing values + const interaction = { + sender: getAddress(hexlify(randomBytes(20))), + receiver: getAddress(hexlify(randomBytes(20))) + }; + + // State channel testing values + const stateChannel = new StateChannel( + getAddress(hexlify(randomBytes(20))), + [interaction.sender, interaction.receiver].sort((a, b) => + parseInt(a, 16) < parseInt(b, 16) ? -1 : 1 + ) + ).setupChannel(networkContext); + + const freeBalanceETH = stateChannel.getFreeBalanceFor(AssetType.ETH); + + beforeAll(() => { + tx = new SetupCommitment( + networkContext, + stateChannel.multisigAddress, + stateChannel.multisigOwners, + freeBalanceETH.identity, + freeBalanceETH.terms + ).getTransactionDetails(); + }); + + it("should be to StateChannelTransaction", () => { + expect(tx.to).toBe(networkContext.StateChannelTransaction); + }); + + it("should have no value", () => { + expect(tx.value).toBe(0); + }); + + describe("the calldata", () => { + const iface = new Interface(StateChannelTransaction.abi); + let desc: TransactionDescription; + + beforeAll(() => { + const { data } = tx; + desc = iface.parseTransaction({ data }); + }); + + it("should be to the executeAppConditionalTransaction method", () => { + expect(desc.sighash).toBe( + iface.functions.executeAppConditionalTransaction.sighash + ); + }); + + it("should contain expected arguments", () => { + const [ + appRegistry, + nonceRegistry, + uninstallKey, + appInstanceId, + [assetType, limit, token] + ] = desc.args; + expect(appRegistry).toBe(networkContext.AppRegistry); + expect(nonceRegistry).toEqual(networkContext.NonceRegistry); + expect(uninstallKey).toBe(freeBalanceETH.uninstallKey); + expect(appInstanceId).toBe(appIdentityToHash(freeBalanceETH.identity)); + expect(assetType).toBe(freeBalanceETH.terms.assetType); + expect(limit).toEqual(freeBalanceETH.terms.limit); + expect(token).toBe(freeBalanceETH.terms.token); + }); + }); +}); diff --git a/packages/machine/test/unit/ethereum/uninstall.spec.ts b/packages/machine/test/unit/ethereum/uninstall.spec.ts new file mode 100644 index 000000000..9df4bcd33 --- /dev/null +++ b/packages/machine/test/unit/ethereum/uninstall.spec.ts @@ -0,0 +1,217 @@ +import AppRegistry from "@counterfactual/contracts/build/contracts/AppRegistry.json"; +import MultiSend from "@counterfactual/contracts/build/contracts/MultiSend.json"; +import NonceRegistry from "@counterfactual/contracts/build/contracts/NonceRegistry.json"; +import { + AssetType, + ETHBucketAppState, + NetworkContext +} from "@counterfactual/types"; +import { HashZero, One, WeiPerEther, Zero } from "ethers/constants"; +import { + bigNumberify, + defaultAbiCoder, + getAddress, + hexlify, + Interface, + keccak256, + randomBytes, + TransactionDescription +} from "ethers/utils"; + +import { UninstallCommitment } from "../../../src/middleware/protocol-operation"; +import { MultisigTransaction } from "../../../src/middleware/protocol-operation/types"; +import { decodeMultisendCalldata } from "../../../src/middleware/protocol-operation/utils/multisend-decoder"; +import { StateChannel } from "../../../src/models"; + +/** + * This test suite decodes a constructed Uninstall Commitment transaction object + * to the specifications defined by Counterfactual as can be found here: + * https://specs.counterfactual.com/07-uninstall-protocol#commitments + */ +describe("Uninstall Commitment", () => { + let tx: MultisigTransaction; + + // Test network context + const networkContext: NetworkContext = { + ETHBucket: getAddress(hexlify(randomBytes(20))), + StateChannelTransaction: getAddress(hexlify(randomBytes(20))), + MultiSend: getAddress(hexlify(randomBytes(20))), + NonceRegistry: getAddress(hexlify(randomBytes(20))), + AppRegistry: getAddress(hexlify(randomBytes(20))), + ETHBalanceRefund: getAddress(hexlify(randomBytes(20))) + }; + + // General interaction testing values + const interaction = { + sender: getAddress(hexlify(randomBytes(20))), + receiver: getAddress(hexlify(randomBytes(20))) + }; + + // State channel testing values + let stateChannel = new StateChannel( + getAddress(hexlify(randomBytes(20))), + [interaction.sender, interaction.receiver].sort((a, b) => + parseInt(a, 16) < parseInt(b, 16) ? -1 : 1 + ) + ).setupChannel(networkContext); + + // Set the state to some test values + stateChannel = stateChannel.setState( + stateChannel.getFreeBalanceFor(AssetType.ETH).id, + { + alice: stateChannel.multisigOwners[0], + bob: stateChannel.multisigOwners[1], + aliceBalance: WeiPerEther, + bobBalance: WeiPerEther + } + ); + + const freeBalanceETH = stateChannel.getFreeBalanceFor(AssetType.ETH); + + const appBeingUninstalledSeqNo = Math.ceil(1000 * Math.random()); + + beforeAll(() => { + tx = new UninstallCommitment( + networkContext, + stateChannel.multisigAddress, + stateChannel.multisigOwners, + freeBalanceETH.identity, + freeBalanceETH.terms, + freeBalanceETH.state as ETHBucketAppState, + freeBalanceETH.nonce, + freeBalanceETH.timeout, + appBeingUninstalledSeqNo + ).getTransactionDetails(); + }); + + it("should be to MultiSend", () => { + expect(tx.to).toBe(networkContext.MultiSend); + }); + + it("should have no value", () => { + expect(tx.value).toBe(0); + }); + + describe("the calldata of the multisend transaction", () => { + let transactions: [number, string, number, string][]; + + beforeAll(() => { + const { data } = tx; + const desc = new Interface(MultiSend.abi).parseTransaction({ data }); + transactions = decodeMultisendCalldata(desc.args[0]); + }); + + it("should contain two transactions", () => { + expect(transactions.length).toBe(2); + }); + + describe("the transaction to update the free balance", () => { + let to: string; + let val: number; + let data: string; + let op: number; + + beforeAll(() => { + [op, to, val, data] = transactions[0]; + }); + + it("should be to the AppRegistry", () => { + expect(to).toBe(networkContext.AppRegistry); + }); + + it("should be of value 0", () => { + expect(val).toEqual(Zero); + }); + + it("should be a Call", () => { + expect(op).toBe(0); + }); + + describe("the calldata", () => { + let iface: Interface; + let calldata: TransactionDescription; + + beforeAll(() => { + iface = new Interface(AppRegistry.abi); + calldata = iface.parseTransaction({ data }); + }); + + it("should be directed at the setState method", () => { + expect(calldata.sighash).toEqual(iface.functions.setState.sighash); + }); + + it("should build the expected AppIdentity argument", () => { + const [ + [owner, signingKeys, appInterfaceHash, termsHash, defaultTimeout] + ] = calldata.args; + + const expected = freeBalanceETH.identity; + + expect(owner).toBe(expected.owner); + expect(signingKeys).toEqual(expected.signingKeys); + expect(appInterfaceHash).toBe(expected.appInterfaceHash); + expect(termsHash).toBe(expected.termsHash); + expect(defaultTimeout).toEqual(bigNumberify(expected.defaultTimeout)); + }); + + it("should build the expected SignedStateHashUpdate argument", () => { + const [, [stateHash, nonce, timeout, signatures]] = calldata.args; + + expect(stateHash).toBe(freeBalanceETH.hashOfLatestState); + expect(nonce).toEqual(bigNumberify(freeBalanceETH.nonce)); + expect(timeout).toEqual(bigNumberify(freeBalanceETH.timeout)); + expect(signatures).toBe(HashZero); + }); + }); + }); + + describe("the transaction to update the dependency nonce", () => { + let to: string; + let val: number; + let data: string; + let op: number; + + beforeAll(() => { + [op, to, val, data] = transactions[1]; + }); + + it("should be to the NonceRegistry", () => { + expect(to).toBe(networkContext.NonceRegistry); + }); + + it("should be of value 0", () => { + expect(val).toEqual(Zero); + }); + + it("should be a Call", () => { + expect(op).toBe(0); + }); + + describe("the calldata", () => { + let iface: Interface; + let calldata: TransactionDescription; + + beforeAll(() => { + iface = new Interface(NonceRegistry.abi); + calldata = iface.parseTransaction({ data }); + }); + + it("should be directed at the setNonce method", () => { + expect(calldata.sighash).toEqual(iface.functions.setNonce.sighash); + }); + + it("should build set the nonce to 1 (uninstalled)", () => { + const [timeout, salt, nonceValue] = calldata.args; + + expect(timeout).toEqual(Zero); + expect(salt).toEqual( + keccak256( + defaultAbiCoder.encode(["uint256"], [appBeingUninstalledSeqNo]) + ) + ); + expect(nonceValue).toEqual(One); + }); + }); + }); + }); +}); diff --git a/packages/machine/test/unit/instruction-executor.spec.ts b/packages/machine/test/unit/instruction-executor.spec.ts new file mode 100644 index 000000000..cd0e62854 --- /dev/null +++ b/packages/machine/test/unit/instruction-executor.spec.ts @@ -0,0 +1,112 @@ +import { AssetType } from "@counterfactual/types"; +import { getAddress, hexlify, randomBytes } from "ethers/utils"; + +import { InstructionExecutor } from "../../src/instruction-executor"; +import { SetupCommitment } from "../../src/middleware/protocol-operation"; +import { AppInstance, StateChannel } from "../../src/models"; +import { Opcode } from "../../src/opcodes"; +import { Context } from "../../src/types"; + +describe("InstructionExecutor", () => { + // Test network context + const networkContext = { + ETHBucket: getAddress(hexlify(randomBytes(20))), + StateChannelTransaction: getAddress(hexlify(randomBytes(20))), + MultiSend: getAddress(hexlify(randomBytes(20))), + NonceRegistry: getAddress(hexlify(randomBytes(20))), + AppRegistry: getAddress(hexlify(randomBytes(20))), + ETHBalanceRefund: getAddress(hexlify(randomBytes(20))) + }; + + // General interaction testing values + const interaction = { + sender: getAddress(hexlify(randomBytes(20))), + receiver: getAddress(hexlify(randomBytes(20))) + }; + + // State channel testing values + const stateChannel = new StateChannel( + getAddress(hexlify(randomBytes(20))), + [interaction.sender, interaction.receiver].sort((a, b) => + parseInt(a, 16) < parseInt(b, 16) ? -1 : 1 + ) + ); + + let instructionExecutor: InstructionExecutor; + + beforeAll(() => { + instructionExecutor = new InstructionExecutor(networkContext); + + // We must define _some_ methods for each opcode or the machine + // will fail with an error like "While executing op number at seq + // of protocol setup, execution failed with the following error. + // TypeError: Cannot read property 'method' of undefined" + const { middleware } = instructionExecutor; + middleware.add(Opcode.OP_SIGN_VALIDATE, () => {}); + middleware.add(Opcode.IO_SEND, () => {}); + middleware.add(Opcode.IO_WAIT, () => {}); + middleware.add(Opcode.STATE_TRANSITION_COMMIT, () => {}); + }); + + describe("the result of STATE_TRANSITION_PROPOSE for the Setup Protocol", () => { + let commitment: SetupCommitment; + let channel: StateChannel; + let fb: AppInstance; + + beforeAll(() => { + instructionExecutor.middleware.add( + Opcode.OP_SIGN, + (_, __, context: Context) => { + commitment = context.operation as SetupCommitment; + channel = context.stateChannel; + } + ); + + instructionExecutor.runSetupProtocol(stateChannel); + + fb = channel.getFreeBalanceFor(AssetType.ETH); + }); + + describe("the proposed state transition of the channel", () => { + it("should have the right metadatas", () => { + expect(channel.multisigAddress).toBe(stateChannel.multisigAddress); + expect(channel.multisigOwners).toContain(interaction.sender); + expect(channel.multisigOwners).toContain(interaction.receiver); + }); + }); + + describe("the computed SetupCommitment", () => { + it("should use the network context passed into instruction executor", () => { + expect(commitment.networkContext).toEqual(networkContext); + }); + + it("should use the default free balance terms", () => { + expect(commitment.freeBalanceTerms).toEqual(fb.terms); + }); + + it("should use the default free balance app identity", () => { + expect(commitment.freeBalanceAppIdentity).toEqual(fb.identity); + }); + + it("should be based on the multisig passed into the protocol executor", () => { + expect(commitment.multisigAddress).toBe(stateChannel.multisigAddress); + }); + + it("should assume from and to as the owners of the multisig, sorted", () => { + expect(commitment.multisigOwners).toEqual(stateChannel.multisigOwners); + }); + + it("should construct the correct hash digest to sign", () => { + expect(commitment.hashToSign()).toBe( + new SetupCommitment( + networkContext, + stateChannel.multisigAddress, + stateChannel.multisigOwners, + fb.identity, + fb.terms + ).hashToSign() + ); + }); + }); + }); +}); diff --git a/packages/machine/test/unit/models/app-instance/app-instance.spec.ts b/packages/machine/test/unit/models/app-instance/app-instance.spec.ts new file mode 100644 index 000000000..5c72f1c65 --- /dev/null +++ b/packages/machine/test/unit/models/app-instance/app-instance.spec.ts @@ -0,0 +1,47 @@ +import { AssetType } from "@counterfactual/types"; +import { AddressZero } from "ethers/constants"; +import { bigNumberify, getAddress, hexlify, randomBytes } from "ethers/utils"; + +import { AppInstance } from "../../../../src/models"; + +describe("AppInstance", () => { + it("should be able to instantiate", () => { + const multisigAddress = getAddress(hexlify(randomBytes(20))); + const signingKeys = [ + getAddress(hexlify(randomBytes(20))), + getAddress(hexlify(randomBytes(20))) + ]; + + const app = new AppInstance( + multisigAddress, + signingKeys, + Math.ceil(Math.random() * 2e10), + { + addr: getAddress(hexlify(randomBytes(20))), + applyAction: hexlify(randomBytes(4)), + resolve: hexlify(randomBytes(4)), + isStateTerminal: hexlify(randomBytes(4)), + getTurnTaker: hexlify(randomBytes(4)), + stateEncoding: "tuple(address foo, uint256 bar)", + actionEncoding: undefined + }, + { + assetType: AssetType.ETH, + limit: bigNumberify(Math.ceil(Math.random() * 2e10)), + token: AddressZero + }, + false, + Math.ceil(Math.random() * 2e10), + { foo: getAddress(hexlify(randomBytes(20))), bar: 0 }, + 999, // <------ nonce + Math.ceil(1000 * Math.random()) + ); + + expect(app).not.toBe(null); + expect(app).not.toBe(undefined); + expect(app.multisigAddress).toBe(multisigAddress); + expect(app.signingKeys).toBe(signingKeys); + + // TODO: moar tests pl0x + }); +}); diff --git a/packages/machine/test/unit/models/state-channel/install.spec.ts b/packages/machine/test/unit/models/state-channel/install.spec.ts new file mode 100644 index 000000000..f526f5b43 --- /dev/null +++ b/packages/machine/test/unit/models/state-channel/install.spec.ts @@ -0,0 +1,115 @@ +import { AssetType, ETHBucketAppState } from "@counterfactual/types"; +import { AddressZero, WeiPerEther, Zero } from "ethers/constants"; +import { bigNumberify, getAddress, hexlify, randomBytes } from "ethers/utils"; + +import { AppInstance, StateChannel } from "../../../../src/models"; + +describe("StateChannel::uninstallApp", () => { + const networkContext = { + ETHBucket: getAddress(hexlify(randomBytes(20))), + StateChannelTransaction: getAddress(hexlify(randomBytes(20))), + MultiSend: getAddress(hexlify(randomBytes(20))), + NonceRegistry: getAddress(hexlify(randomBytes(20))), + AppRegistry: getAddress(hexlify(randomBytes(20))), + ETHBalanceRefund: getAddress(hexlify(randomBytes(20))) + }; + + let sc1: StateChannel; + let sc2: StateChannel; + + let appInstanceId: string; + + beforeAll(() => { + const multisigAddress = getAddress(hexlify(randomBytes(20))); + const multisigOwners = [ + getAddress(hexlify(randomBytes(20))), + getAddress(hexlify(randomBytes(20))) + ]; + + sc1 = new StateChannel(multisigAddress, multisigOwners).setupChannel( + networkContext + ); + + const app = new AppInstance( + getAddress(hexlify(randomBytes(20))), + [ + getAddress(hexlify(randomBytes(20))), + getAddress(hexlify(randomBytes(20))) + ], + Math.ceil(Math.random() * 2e10), + { + addr: getAddress(hexlify(randomBytes(20))), + applyAction: hexlify(randomBytes(4)), + resolve: hexlify(randomBytes(4)), + isStateTerminal: hexlify(randomBytes(4)), + getTurnTaker: hexlify(randomBytes(4)), + stateEncoding: "tuple(address foo, uint256 bar)", + actionEncoding: undefined + }, + { + assetType: AssetType.ETH, + limit: bigNumberify(Math.ceil(Math.random() * 2e10)), + token: AddressZero + }, + false, + Math.ceil(Math.random() * 2e10), + { foo: getAddress(hexlify(randomBytes(20))), bar: 0 }, + 999, // <------ nonce + Math.ceil(1000 * Math.random()) + ); + + appInstanceId = app.id; + + // Give 1 ETH to Alice and to Bob so they can spend it on the new app + const fb = sc1.getFreeBalanceFor(AssetType.ETH); + + sc1 = sc1.setState(fb.id, { + ...fb.state, + aliceBalance: WeiPerEther, + bobBalance: WeiPerEther + }); + + sc2 = sc1.installApp(app, WeiPerEther, WeiPerEther); + }); + + it("should not alter any of the base properties", () => { + expect(sc2.multisigAddress).toBe(sc1.multisigAddress); + expect(sc2.multisigOwners).toBe(sc1.multisigOwners); + }); + + it("should have bumped the sequence number", () => { + expect(sc2.numInstalledApps).toBe(sc1.numInstalledApps + 1); + }); + + it("should have added something at the id of thew new app", () => { + expect(sc2.getAppInstance(appInstanceId)).not.toBe(undefined); + }); + + describe("the updated ETH Free Balance", () => { + let fb: AppInstance; + + beforeAll(() => { + fb = sc2.getFreeBalanceFor(AssetType.ETH); + }); + + it("should have updated balances for Alice and Bob", () => { + const { aliceBalance, bobBalance } = fb.state as ETHBucketAppState; + expect(aliceBalance).toEqual(Zero); + expect(bobBalance).toEqual(Zero); + }); + }); + + describe("the newly installed app", () => { + let app: AppInstance; + + beforeAll(() => { + app = sc2.getAppInstance(appInstanceId)!; + }); + + it("should not be a metachannel app", () => { + expect(app.isMetachannelApp).toBe(false); + }); + + // TODO: moar tests pl0x + }); +}); diff --git a/packages/machine/test/unit/models/state-channel/set-state.spec.ts b/packages/machine/test/unit/models/state-channel/set-state.spec.ts new file mode 100644 index 000000000..b4c1ec24a --- /dev/null +++ b/packages/machine/test/unit/models/state-channel/set-state.spec.ts @@ -0,0 +1,95 @@ +import { AssetType } from "@counterfactual/types"; +import { AddressZero, Zero } from "ethers/constants"; +import { bigNumberify, getAddress, hexlify, randomBytes } from "ethers/utils"; + +import { AppInstance, StateChannel } from "../../../../src/models"; + +describe("StateChannel::setState", () => { + const networkContext = { + ETHBucket: getAddress(hexlify(randomBytes(20))), + StateChannelTransaction: getAddress(hexlify(randomBytes(20))), + MultiSend: getAddress(hexlify(randomBytes(20))), + NonceRegistry: getAddress(hexlify(randomBytes(20))), + AppRegistry: getAddress(hexlify(randomBytes(20))), + ETHBalanceRefund: getAddress(hexlify(randomBytes(20))) + }; + + let sc1: StateChannel; + let sc2: StateChannel; + let testApp: AppInstance; + + beforeAll(() => { + const multisigAddress = getAddress(hexlify(randomBytes(20))); + const multisigOwners = [ + getAddress(hexlify(randomBytes(20))), + getAddress(hexlify(randomBytes(20))) + ]; + + testApp = new AppInstance( + getAddress(hexlify(randomBytes(20))), + [ + getAddress(hexlify(randomBytes(20))), + getAddress(hexlify(randomBytes(20))) + ], + Math.ceil(Math.random() * 2e10), + { + addr: getAddress(hexlify(randomBytes(20))), + applyAction: hexlify(randomBytes(4)), + resolve: hexlify(randomBytes(4)), + isStateTerminal: hexlify(randomBytes(4)), + getTurnTaker: hexlify(randomBytes(4)), + stateEncoding: "tuple(address foo, uint256 bar)", + actionEncoding: undefined + }, + { + assetType: AssetType.ETH, + limit: bigNumberify(Math.ceil(Math.random() * 2e10)), + token: AddressZero + }, + false, + Math.ceil(Math.random() * 2e10), + { foo: getAddress(hexlify(randomBytes(20))), bar: 0 }, + 999, // <------ nonce + Math.ceil(1000 * Math.random()) + ); + + sc1 = new StateChannel(multisigAddress, multisigOwners) + .setupChannel(networkContext) + .installApp(testApp, Zero, Zero); + + sc2 = sc1.setState(testApp.id, { foo: AddressZero, bar: 1337 }); + }); + + it("should not alter any of the base properties", () => { + expect(sc2.multisigAddress).toBe(sc1.multisigAddress); + expect(sc2.multisigOwners).toBe(sc1.multisigOwners); + }); + + it("should not have bumped the sequence number", () => { + expect(sc2.numInstalledApps).toBe(sc1.numInstalledApps); + }); + + describe("the updated app", () => { + let app: AppInstance; + + beforeAll(() => { + app = sc2.getAppInstance(testApp.id)!; + }); + + it("should have the new state", () => { + expect(app.state).toEqual({ + foo: AddressZero, + bar: 1337 + }); + }); + + it("should have bumped the nonce", () => { + // TODO: make 999 a const var in the test + expect(app.nonce).toBe(999 + 1); + }); + + it("should have used the default timeout", () => { + expect(app.timeout).toBe(app.timeout); + }); + }); +}); diff --git a/packages/machine/test/unit/models/state-channel/setup-channel.spec.ts b/packages/machine/test/unit/models/state-channel/setup-channel.spec.ts new file mode 100644 index 000000000..f814f8fad --- /dev/null +++ b/packages/machine/test/unit/models/state-channel/setup-channel.spec.ts @@ -0,0 +1,115 @@ +import ETHBucket from "@counterfactual/contracts/build/contracts/ETHBucket.json"; +import { AssetType, ETHBucketAppState } from "@counterfactual/types"; +import { Zero } from "ethers/constants"; +import { + // formatParamType, + getAddress, + hexlify, + Interface, + randomBytes +} from "ethers/utils"; + +import { AppInstance, StateChannel } from "../../../../src/models"; + +describe("StateChannel::setupChannel", () => { + let sc1: StateChannel; + let sc2: StateChannel; + + const networkContext = { + ETHBucket: getAddress(hexlify(randomBytes(20))), + StateChannelTransaction: getAddress(hexlify(randomBytes(20))), + MultiSend: getAddress(hexlify(randomBytes(20))), + NonceRegistry: getAddress(hexlify(randomBytes(20))), + AppRegistry: getAddress(hexlify(randomBytes(20))), + ETHBalanceRefund: getAddress(hexlify(randomBytes(20))) + }; + + beforeAll(() => { + const multisigAddress = getAddress(hexlify(randomBytes(20))); + const multisigOwners = [ + getAddress(hexlify(randomBytes(20))), + getAddress(hexlify(randomBytes(20))) + ]; + + sc1 = new StateChannel(multisigAddress, multisigOwners); + + sc2 = sc1.setupChannel(networkContext); + }); + + it("should not alter any of the base properties", () => { + expect(sc2.multisigAddress).toBe(sc1.multisigAddress); + expect(sc2.multisigOwners).toBe(sc1.multisigOwners); + }); + + it("should have added an ETH Free Balance", () => { + const fb = sc2.getFreeBalanceFor(AssetType.ETH); + expect(fb).not.toBe(undefined); + }); + + it("should have bumped the sequence number", () => { + expect(sc2.numInstalledApps).toBe(sc1.numInstalledApps + 1); + }); + + describe("the installed ETH Free Balance", () => { + let fb: AppInstance; + + beforeAll(() => { + fb = sc2.getFreeBalanceFor(AssetType.ETH); + }); + + it("should be owned by the multisig", () => { + expect(fb.multisigAddress).toBe(sc2.multisigAddress); + }); + + it("should not be a metachannel app", () => { + expect(fb.isMetachannelApp).toBe(false); + }); + + it("should have nonce 0 to start", () => { + expect(fb.nonce).toBe(0); + }); + + it("should have a default timeout defined by the hard-coded assumption", () => { + // See HARD_CODED_ASSUMPTIONS in state-channel.ts + expect(fb.timeout).toBe(10); + }); + + it("should use the default timeout for the initial timeout", () => { + expect(fb.timeout).toBe(fb.defaultTimeout); + }); + + it("should use the multisig owners as the signing keys", () => { + expect(fb.signingKeys).toBe(sc2.multisigOwners); + }); + + it("should use the ETHBucketApp as the app target", () => { + const iface = new Interface(ETHBucket.abi); + expect(fb.appInterface.addr).toBe(networkContext.ETHBucket); + // Have to wait for formatParamType to include names + // expect(fb.appInterface.stateEncoding).toBe( + // formatParamType(iface.functions.resolve.inputs[0]) + // ); + expect(fb.appInterface.applyAction).toBe("0x00000000"); + expect(fb.appInterface.isStateTerminal).toBe("0x00000000"); + expect(fb.appInterface.getTurnTaker).toBe("0x00000000"); + expect(fb.appInterface.resolve).toBe(iface.functions.resolve.sighash); + expect(fb.appInterface.actionEncoding).toBe(undefined); + }); + + it("should have seqNo of 0 (b/c it is the first ever app)", () => { + expect(fb.appSeqNo).toBe(0); + }); + + it("should set the signingKeys as the multisigOwners", () => { + const { alice, bob } = fb.state as ETHBucketAppState; + expect(alice).toBe(sc1.multisigOwners[0]); + expect(bob).toBe(sc1.multisigOwners[1]); + }); + + it("should have 0 balances for Alice and Bob", () => { + const { aliceBalance, bobBalance } = fb.state as ETHBucketAppState; + expect(aliceBalance).toBe(Zero); + expect(bobBalance).toBe(Zero); + }); + }); +}); diff --git a/packages/machine/test/unit/models/state-channel/state-channel.spec.ts b/packages/machine/test/unit/models/state-channel/state-channel.spec.ts new file mode 100644 index 000000000..f1dec3610 --- /dev/null +++ b/packages/machine/test/unit/models/state-channel/state-channel.spec.ts @@ -0,0 +1,22 @@ +import { getAddress, hexlify, randomBytes } from "ethers/utils"; + +import { StateChannel } from "../../../../src/models"; + +describe("StateChannel", () => { + it("should be able to instantiate", () => { + const multisigAddress = getAddress(hexlify(randomBytes(20))); + const multisigOwners = [ + getAddress(hexlify(randomBytes(20))), + getAddress(hexlify(randomBytes(20))) + ]; + + const sc = new StateChannel(multisigAddress, multisigOwners); + + expect(sc).not.toBe(null); + expect(sc).not.toBe(undefined); + expect(sc.multisigAddress).toBe(multisigAddress); + expect(sc.multisigOwners).toBe(multisigOwners); + expect(sc.numActiveApps).toBe(0); + expect(sc.numInstalledApps).toBe(0); + }); +}); diff --git a/packages/machine/test/unit/models/state-channel/uninstall-app.spec.ts b/packages/machine/test/unit/models/state-channel/uninstall-app.spec.ts new file mode 100644 index 000000000..51000eff1 --- /dev/null +++ b/packages/machine/test/unit/models/state-channel/uninstall-app.spec.ts @@ -0,0 +1,89 @@ +import { AssetType, ETHBucketAppState } from "@counterfactual/types"; +import { AddressZero, Zero } from "ethers/constants"; +import { bigNumberify, getAddress, hexlify, randomBytes } from "ethers/utils"; + +import { AppInstance, StateChannel } from "../../../../src/models"; + +describe("StateChannel::uninstallApp", () => { + const networkContext = { + ETHBucket: getAddress(hexlify(randomBytes(20))), + StateChannelTransaction: getAddress(hexlify(randomBytes(20))), + MultiSend: getAddress(hexlify(randomBytes(20))), + NonceRegistry: getAddress(hexlify(randomBytes(20))), + AppRegistry: getAddress(hexlify(randomBytes(20))), + ETHBalanceRefund: getAddress(hexlify(randomBytes(20))) + }; + + let sc1: StateChannel; + let sc2: StateChannel; + let testApp: AppInstance; + + beforeAll(() => { + const multisigAddress = getAddress(hexlify(randomBytes(20))); + const multisigOwners = [ + getAddress(hexlify(randomBytes(20))), + getAddress(hexlify(randomBytes(20))) + ]; + + testApp = new AppInstance( + getAddress(hexlify(randomBytes(20))), + [ + getAddress(hexlify(randomBytes(20))), + getAddress(hexlify(randomBytes(20))) + ], + Math.ceil(Math.random() * 2e10), + { + addr: getAddress(hexlify(randomBytes(20))), + applyAction: hexlify(randomBytes(4)), + resolve: hexlify(randomBytes(4)), + isStateTerminal: hexlify(randomBytes(4)), + getTurnTaker: hexlify(randomBytes(4)), + stateEncoding: "tuple(address foo, uint256 bar)", + actionEncoding: undefined + }, + { + assetType: AssetType.ETH, + limit: bigNumberify(Math.ceil(Math.random() * 2e10)), + token: AddressZero + }, + false, + Math.ceil(Math.random() * 2e10), + { foo: getAddress(hexlify(randomBytes(20))), bar: 0 }, + 999, // <------ nonce + Math.ceil(1000 * Math.random()) + ); + + sc1 = new StateChannel(multisigAddress, multisigOwners) + .setupChannel(networkContext) + .installApp(testApp, Zero, Zero); + + sc2 = sc1.uninstallApp(testApp.id, Zero, Zero); + }); + + it("should not alter any of the base properties", () => { + expect(sc2.multisigAddress).toBe(sc1.multisigAddress); + expect(sc2.multisigOwners).toBe(sc1.multisigOwners); + }); + + it("should not have bumped the sequence number", () => { + expect(sc2.numInstalledApps).toBe(sc1.numInstalledApps); + }); + + it("should have deleted the app being uninstalled", () => { + expect(sc2.isAppInstanceInstalled(testApp.id)).toBe(false); + }); + + describe("the updated ETH Free Balance", () => { + let fb: AppInstance; + + beforeAll(() => { + fb = sc2.getFreeBalanceFor(AssetType.ETH); + }); + + it("should have updated balances for Alice and Bob", () => { + const { aliceBalance, bobBalance } = fb.state as ETHBucketAppState; + expect(aliceBalance).toEqual(Zero); + expect(bobBalance).toEqual(Zero); + }); + }); +}); diff --git a/packages/machine/test/unit/state-transition.spec.ts b/packages/machine/test/unit/state-transition.spec.ts deleted file mode 100644 index 7f12b79ee..000000000 --- a/packages/machine/test/unit/state-transition.spec.ts +++ /dev/null @@ -1,184 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import { ethers } from "ethers"; - -import { Opcode } from "../../src/opcodes"; -import { InstallProposer } from "../../src/middleware/state-transition/install-proposer"; -import { SetupProposer } from "../../src/middleware/state-transition/setup-proposer"; -import { Node, StateChannelInfoImpl } from "../../src/node"; -import { InternalMessage } from "../../src/types"; - -import { - A_ADDRESS, - B_ADDRESS, - UNUSED_FUNDED_ACCOUNT -} from "../utils/environment"; - -// install params -const KEY_A = "0x9e5d9691ad19e3b8c48cb9b531465ffa73ee8dd3"; -const KEY_B = "0x9e5d9691ad19e3b8c48cb9b531465ffa73ee8dd4"; -const TOKEN_ADDRESS = "0x9e5d9691ad19e3b8c48cb9b531465ffa73ee8dd5"; -const APP_ADDRESS = "0x9e5d9691ad19e3b8c48cb9b531465ffa73ee8dd6"; -const APPLY_ACTION = "0x00000001"; -const RESOLVE = "0x00000002"; -const TURN = "0x00000003"; -const IS_STATE_TERMINAL = "0x00000004"; -const ABI_ENCODING = ""; - -describe("State transition", () => { - it("should propose a new setup state", () => { - const message = new InternalMessage( - cf.legacy.node.ActionName.SETUP, - Opcode.STATE_TRANSITION_PROPOSE, - setupClientMsg(), - ); - const proposal = SetupProposer.propose(message); - validateSetupInfos(proposal.state); - }); - it("should propose a new install state", () => { - const message = new InternalMessage( - cf.legacy.node.ActionName.INSTALL, - Opcode.STATE_TRANSITION_PROPOSE, - installClientMsg(), - ); - const expectedCfAddr = new cf.legacy.app.AppInstance( - cf.legacy.network.EMPTY_NETWORK_CONTEXT, - message.clientMessage.multisigAddress, - [KEY_A, KEY_B], - message.clientMessage.data.app, - message.clientMessage.data.terms, - message.clientMessage.data.timeout, - 1 - ).cfAddress(); - const proposal = InstallProposer.propose( - message, - { - intermediateResults: { - inbox: [], - outbox: [] - }, - instructionExecutor: Object.create(null) - }, - setupInstallState() - ); - validateInstallInfos(proposal.state, expectedCfAddr); - }); -}); - -function setupClientMsg(): cf.legacy.node.ClientActionMessage { - return { - appInstanceId: "0", - action: cf.legacy.node.ActionName.SETUP, - data: {}, - multisigAddress: UNUSED_FUNDED_ACCOUNT, - fromAddress: A_ADDRESS, - toAddress: B_ADDRESS, - seq: 0 - }; -} - -function setupInstallState(): Node { - const freeBalance = new cf.legacy.utils.FreeBalance( - A_ADDRESS, - ethers.utils.bigNumberify(20), - B_ADDRESS, - ethers.utils.bigNumberify(20), - 0, // local nonce - 0, // uniqueId - 100, // timeout - new cf.legacy.utils.Nonce(true, 0, 0) // nonce - ); - const info = new StateChannelInfoImpl( - B_ADDRESS, - A_ADDRESS, - UNUSED_FUNDED_ACCOUNT, - {}, - freeBalance - ); - const channelStates: cf.legacy.channel.StateChannelInfos = { - [UNUSED_FUNDED_ACCOUNT]: info - }; - return new Node(channelStates, cf.legacy.network.EMPTY_NETWORK_CONTEXT); -} - -function validateSetupInfos(infos: cf.legacy.channel.StateChannelInfos) { - expect(Object.keys(infos).length).toEqual(1); - const info = infos[UNUSED_FUNDED_ACCOUNT]; - expect(info.counterParty).toEqual(B_ADDRESS); - expect(info.me).toEqual(A_ADDRESS); - expect(Object.keys(info.appInstances).length).toEqual(0); - expect(info.freeBalance.balanceOfAddress(A_ADDRESS).toNumber()).toEqual(0); - expect(info.freeBalance.balanceOfAddress(B_ADDRESS).toNumber()).toEqual(0); - expect(info.freeBalance.localNonce).toEqual(0); - expect(info.freeBalance.uniqueId).toEqual(0); - - const expectedSalt = ethers.utils.solidityKeccak256(["uint256"], [0]); - - expect(info.freeBalance.dependencyNonce.nonceValue).toEqual(0); - expect(info.freeBalance.dependencyNonce.salt).toEqual(expectedSalt); -} - -function installClientMsg(): cf.legacy.node.ClientActionMessage { - return { - appInstanceId: "0", - action: cf.legacy.node.ActionName.INSTALL, - data: { - peerA: new cf.legacy.utils.PeerBalance(A_ADDRESS, 5), - peerB: new cf.legacy.utils.PeerBalance(B_ADDRESS, 3), - keyA: KEY_A, - keyB: KEY_B, - encodedAppState: "0x0", - terms: new cf.legacy.app.Terms( - 0, - ethers.utils.bigNumberify(8), - TOKEN_ADDRESS - ), - app: new cf.legacy.app.AppInterface( - APP_ADDRESS, - APPLY_ACTION, - RESOLVE, - TURN, - IS_STATE_TERMINAL, - ABI_ENCODING - ), - timeout: 100 - }, - multisigAddress: UNUSED_FUNDED_ACCOUNT, - fromAddress: B_ADDRESS, - toAddress: A_ADDRESS, - seq: 0 - }; -} - -function validateInstallInfos( - infos: cf.legacy.channel.StateChannelInfos, - expectedCfAddr: cf.legacy.utils.H256 -) { - const stateChannel = infos[UNUSED_FUNDED_ACCOUNT]; - - expect(stateChannel.freeBalance.aliceBalance.toNumber()).toEqual(15); - expect(stateChannel.freeBalance.bobBalance.toNumber()).toEqual(17); - - const app = infos[UNUSED_FUNDED_ACCOUNT].appInstances[expectedCfAddr]; - const expectedSalt = - "0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6"; - - expect(app.id).toEqual(expectedCfAddr); - expect(app.peerA.address).toEqual(A_ADDRESS); - expect(app.peerA.balance.toNumber()).toEqual(5); - expect(app.peerB.address).toEqual(B_ADDRESS); - expect(app.keyA).toEqual(KEY_A); - expect(app.keyB).toEqual(KEY_B); - expect(app.encodedState).toEqual("0x0"); - expect(app.localNonce).toEqual(1); - expect(app.timeout).toEqual(100); - expect(app.terms.assetType).toEqual(0); - expect(app.terms.limit).toEqual(ethers.utils.bigNumberify(8)); - expect(app.terms.token).toEqual(TOKEN_ADDRESS); - expect(app.cfApp.address).toEqual(APP_ADDRESS); - expect(app.cfApp.applyAction).toEqual(APPLY_ACTION); - expect(app.cfApp.resolve).toEqual(RESOLVE); - expect(app.cfApp.getTurnTaker).toEqual(TURN); - expect(app.cfApp.isStateTerminal).toEqual(IS_STATE_TERMINAL); - expect(app.dependencyNonce.salt).toEqual(expectedSalt); - expect(app.dependencyNonce.nonceValue).toEqual(0); -} diff --git a/packages/machine/test/utils/environment.ts b/packages/machine/test/utils/environment.ts deleted file mode 100644 index 283003666..000000000 --- a/packages/machine/test/utils/environment.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ethers } from "ethers"; - -export const UNUSED_FUNDED_ACCOUNT_PRIVATE_KEY: string = process.env - .npm_package_config_unlockedAccount0!; // never null via test runtime -export const UNUSED_FUNDED_ACCOUNT = new ethers.Wallet( - UNUSED_FUNDED_ACCOUNT_PRIVATE_KEY -).address; - -export const A_PRIVATE_KEY: string = process.env - .npm_package_config_unlockedAccount1!; -export const A_ADDRESS = new ethers.Wallet(A_PRIVATE_KEY).address; - -export const B_PRIVATE_KEY: string = process.env - .npm_package_config_unlockedAccount2!; -export const B_ADDRESS = new ethers.Wallet(B_PRIVATE_KEY).address; - -export const I_PRIVATE_KEY: string = process.env - .npm_package_config_unlockedAccount3!; -export const I_ADDRESS = new ethers.Wallet(I_PRIVATE_KEY).address; diff --git a/packages/machine/truffle-config.js b/packages/machine/truffle-config.js new file mode 100644 index 000000000..0c7e25d5b --- /dev/null +++ b/packages/machine/truffle-config.js @@ -0,0 +1,19 @@ +const HDWalletProvider = require("truffle-hdwallet-provider"); + +require("dotenv-safe").config(); + +module.exports = { + contracts_directory: "../contracts", + migrations_directory: "../contracts/migrations", + networks: { + machine: { + network_id: process.env.DEV_GANACHE_NETWORK_ID, + provider: function() { + return new HDWalletProvider( + process.env.DEV_GANACHE_MNEMONIC, + `http://${process.env.DEV_GANACHE_HOST}:${process.env.DEV_GANACHE_PORT}/` + ); + } + } + } +}; diff --git a/packages/machine/tsconfig.json b/packages/machine/tsconfig.json index 674ee65dd..af2dacc45 100644 --- a/packages/machine/tsconfig.json +++ b/packages/machine/tsconfig.json @@ -1,14 +1,9 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "rootDir": ".", - "sourceRoot": ".", - // TODO: Document why we need declarationDir here - "declarationDir": "dist/", + "resolveJsonModule": true, "outDir": "dist/" }, - // TODO: Figure out how to add "test" here so they get linted by tslint when using - // the -p flag but *also* such that rollup doesn't include tests in the build. - "include": ["src"], + "include": ["src", "test"], "exclude": ["dist"] } diff --git a/packages/machine/tsconfig.test.json b/packages/machine/tsconfig.test.json deleted file mode 100644 index 09e13f54f..000000000 --- a/packages/machine/tsconfig.test.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "rootDir": ".", - "sourceRoot": ".", - // TODO: Document why we need declarationDir here - "declarationDir": "dist/", - "outDir": "dist/" - }, - "include": ["src", "test"], - "exclude": ["dist"] -} diff --git a/packages/node-provider/package.json b/packages/node-provider/package.json index 3fbfee413..85038abd9 100644 --- a/packages/node-provider/package.json +++ b/packages/node-provider/package.json @@ -9,7 +9,6 @@ ], "license": "MIT", "scripts": { - "clean": "rm -rf .rpt2_cache jest-cache build dist", "build": "tsc -p tsconfig.json && rollup -c", "test": "tsc -b && jest --runInBand --detectOpenHandles --bail --forceExit", "test-debug": "node --inspect-brk jest --runInBand", diff --git a/packages/node/package.json b/packages/node/package.json index c988ff815..91f78a561 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -5,7 +5,6 @@ "iife": "dist/index.iife.js", "license": "MIT", "scripts": { - "clean": "rm -rf .rpt2_cache jest-cache build dist", "build": "tsc -b && rollup -c", "test": "tsc -b && jest --runInBand --detectOpenHandles --bail", "test:coverage": "tsc -b && jest --runInBand --detectOpenHandles --bail --coverage", @@ -18,7 +17,7 @@ "@types/node": "^10.9.3", "cuid": "^2.1.4", "dotenv": "^6.1.0", - "ethers": "^4.0.17", + "ethers": "4.0.20", "firebase": "^5.6.0", "firebase-server": "^1.0.1", "jest": "^23.6.0", diff --git a/packages/node/src/channels.ts b/packages/node/src/channels.ts index c80607251..33689697c 100644 --- a/packages/node/src/channels.ts +++ b/packages/node/src/channels.ts @@ -1,5 +1,5 @@ import { Address, AppInstanceInfo, Node } from "@counterfactual/common-types"; -import { ethers } from "ethers"; +import { Wallet } from "ethers"; import { v4 as generateUUID } from "uuid"; import { APP_INSTANCE_STATUS, Channel } from "./models"; @@ -447,6 +447,6 @@ export class Channels { private generateNewMultisigAddress(owners: Address[]): Address { // TODO: implement this using CREATE2 - return ethers.Wallet.createRandom().address; + return Wallet.createRandom().address; } } diff --git a/packages/node/src/models.ts b/packages/node/src/models.ts index 5f86e2040..0ecf15c5b 100644 --- a/packages/node/src/models.ts +++ b/packages/node/src/models.ts @@ -4,8 +4,7 @@ import { AppInstanceInfo, AssetType } from "@counterfactual/common-types"; - -import { zeroBalance } from "./utils"; +import { Zero } from "ethers/constants"; import Nonce = legacy.utils.Nonce; import FreeBalance = legacy.utils.FreeBalance; @@ -93,9 +92,9 @@ export class Channel { // TODO: extend to all asset types const ethFreeBalance = new FreeBalance( multisigOwners[0], - zeroBalance, + Zero, multisigOwners[1], - zeroBalance, + Zero, 0, 0, 0, diff --git a/packages/node/src/node.ts b/packages/node/src/node.ts index 601cefbb9..89dd5d8a3 100644 --- a/packages/node/src/node.ts +++ b/packages/node/src/node.ts @@ -1,5 +1,5 @@ import { Address, Node as NodeTypes } from "@counterfactual/common-types"; -import { ethers } from "ethers"; +import { SigningKey } from "ethers/utils"; import EventEmitter from "eventemitter3"; import { Channels } from "./channels"; @@ -22,7 +22,7 @@ export class Node { private readonly outgoing: EventEmitter; private readonly channels: Channels; - private readonly signer: ethers.utils.SigningKey; + private readonly signer: SigningKey; protected readonly requestHandler: RequestHandler; /** @@ -35,7 +35,7 @@ export class Node { private readonly storeService: IStoreService, nodeConfig: NodeConfig ) { - this.signer = new ethers.utils.SigningKey(privateKey); + this.signer = new SigningKey(privateKey); this.incoming = new EventEmitter(); this.outgoing = new EventEmitter(); this.channels = new Channels( diff --git a/packages/node/src/utils.ts b/packages/node/src/utils.ts index 5fb36c7d0..cc01d5198 100644 --- a/packages/node/src/utils.ts +++ b/packages/node/src/utils.ts @@ -1,11 +1,9 @@ import { Address } from "@counterfactual/common-types"; -import { ethers } from "ethers"; - -export const zeroBalance = ethers.utils.bigNumberify("0"); +import { BigNumber, hashMessage } from "ethers/utils"; export function orderedAddressesHash(addresses: Address[]): string { addresses.sort((addrA: Address, addrB: Address) => { - return new ethers.utils.BigNumber(addrA).lt(addrB) ? -1 : 1; + return new BigNumber(addrA).lt(addrB) ? -1 : 1; }); - return ethers.utils.hashMessage(addresses.join("")); + return hashMessage(addresses.join("")); } diff --git a/packages/node/test/integration/get-app-instances.spec.ts b/packages/node/test/integration/get-app-instances.spec.ts index 24d990980..2519e74a8 100644 --- a/packages/node/test/integration/get-app-instances.spec.ts +++ b/packages/node/test/integration/get-app-instances.spec.ts @@ -4,7 +4,7 @@ import { } from "@counterfactual/common-types"; import cuid from "cuid"; import dotenv from "dotenv"; -import { ethers } from "ethers"; +import { Wallet } from "ethers"; import FirebaseServer from "firebase-server"; import { IStoreService, Node, NodeConfig } from "../../src"; @@ -62,7 +62,7 @@ describe("Node method follows spec - getAppInstances", () => { it("can accept a valid call to get non-empty list of app instances", async done => { // the peer with whom an installation proposal is being made - const peerAddress = new ethers.Wallet(B_PRIVATE_KEY).address; + const peerAddress = new Wallet(B_PRIVATE_KEY).address; // first, a channel must be opened for it to have an app instance const multisigAddress = await getNewMultisig(node, [ diff --git a/packages/node/test/integration/node-communication.spec.ts b/packages/node/test/integration/node-communication.spec.ts index 7f1311385..fc5b276e0 100644 --- a/packages/node/test/integration/node-communication.spec.ts +++ b/packages/node/test/integration/node-communication.spec.ts @@ -1,5 +1,5 @@ import dotenv from "dotenv"; -import { ethers } from "ethers"; +import { AddressZero } from "ethers/constants"; import FirebaseServer from "firebase-server"; import { IMessagingService } from "../../src"; @@ -28,7 +28,7 @@ describe("Two nodes can communicate with each other", () => { }); it("can setup listeners for events through messaging service", done => { - const address = ethers.constants.AddressZero; + const address = AddressZero; const testMsg = { event: "testEvent", data: { diff --git a/packages/node/test/integration/node-store.spec.ts b/packages/node/test/integration/node-store.spec.ts index dc9a24c6b..a524f31e1 100644 --- a/packages/node/test/integration/node-store.spec.ts +++ b/packages/node/test/integration/node-store.spec.ts @@ -1,5 +1,6 @@ import dotenv from "dotenv"; -import { ethers } from "ethers"; +import { Wallet } from "ethers"; +import { AddressZero } from "ethers/constants"; import FirebaseServer from "firebase-server"; import { IStoreService, Node, NodeConfig } from "../../src"; @@ -41,12 +42,9 @@ describe("Node can use storage service", () => { }); it("can save multiple channels under respective multisig indeces and query for all channels", async () => { - const channelA = { owners: [node.address, ethers.constants.AddressZero] }; + const channelA = { owners: [node.address, AddressZero] }; const channelB = { - owners: [ - new ethers.Wallet(B_PRIVATE_KEY).address, - ethers.constants.AddressZero - ] + owners: [new Wallet(B_PRIVATE_KEY).address, AddressZero] }; await storeService.set([{ key: "multisigAddress/0x111", value: channelA }]); await storeService.set([{ key: "multisigAddress/0x222", value: channelB }]); @@ -61,12 +59,9 @@ describe("Node can use storage service", () => { }); it("can save multiple channels under respective multisig indeces in one call and query for all channels", async () => { - const channelA = { owners: [node.address, ethers.constants.AddressZero] }; + const channelA = { owners: [node.address, AddressZero] }; const channelB = { - owners: [ - new ethers.Wallet(B_PRIVATE_KEY).address, - ethers.constants.AddressZero - ] + owners: [new Wallet(B_PRIVATE_KEY).address, AddressZero] }; await storeService.set([ { key: "multisigAddress/0x111", value: channelA }, diff --git a/packages/node/test/integration/utils.ts b/packages/node/test/integration/utils.ts index c88fdb08c..77c21da16 100644 --- a/packages/node/test/integration/utils.ts +++ b/packages/node/test/integration/utils.ts @@ -5,7 +5,7 @@ import { Node as NodeTypes } from "@counterfactual/common-types"; import cuid from "cuid"; -import { ethers } from "ethers"; +import { One } from "ethers/constants"; import { Node } from "../../src"; import { APP_INSTANCE_STATUS } from "../../src/models"; @@ -95,9 +95,9 @@ export function makeProposalRequest( asset: { assetType: AssetType.ETH }, - myDeposit: ethers.utils.bigNumberify("1"), - peerDeposit: ethers.utils.bigNumberify("1"), - timeout: ethers.utils.bigNumberify("1"), + myDeposit: One, + peerDeposit: One, + timeout: One, initialState: { propertyA: "A", propertyB: "B" diff --git a/packages/playground/tsconfig.json b/packages/playground/tsconfig.json index 114fa9477..362400c02 100644 --- a/packages/playground/tsconfig.json +++ b/packages/playground/tsconfig.json @@ -7,7 +7,9 @@ "moduleResolution": "node", "module": "esnext", "jsx": "react", - "jsxFactory": "h" + "jsxFactory": "h", + // @types/mocha has a duplicate conflict, this is the suggested temp fix. + "skipLibCheck": true, }, "include": ["src"], "exclude": ["__mocks__"] diff --git a/packages/types/.gitignore b/packages/types/.gitignore new file mode 100644 index 000000000..9d9bb3df7 --- /dev/null +++ b/packages/types/.gitignore @@ -0,0 +1 @@ +src/*[.js, .js.map, .d.ts] diff --git a/packages/types/package.json b/packages/types/package.json new file mode 100644 index 000000000..8b047247b --- /dev/null +++ b/packages/types/package.json @@ -0,0 +1,14 @@ +{ + "name": "@counterfactual/types", + "version": "0.0.1", + "description": "TypeScript typings for common Counterfactual types", + "main": "src/index.ts", + "types": "src/index.d.ts", + "license": "MIT", + "scripts": { + "build": "tsc -p ." + }, + "devDependencies": { + "ethers": "4.0.20" + } +} diff --git a/packages/types/src/globals.d.ts b/packages/types/src/globals.d.ts new file mode 100644 index 000000000..2318f638a --- /dev/null +++ b/packages/types/src/globals.d.ts @@ -0,0 +1,6 @@ +declare module '*.json' { + const json: any; + /* tslint:disable */ + export default json; + /* tslint:enable */ +} \ No newline at end of file diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts new file mode 100644 index 000000000..57a501232 --- /dev/null +++ b/packages/types/src/index.ts @@ -0,0 +1,61 @@ +import { ethers } from "ethers"; + +export enum AssetType { + ETH = 0, + ERC20 = 1 +} + +export interface Terms { + assetType: AssetType; + limit: ethers.utils.BigNumber; + token: string; +} + +export interface AppIdentity { + owner: string; + signingKeys: string[]; + appInterfaceHash: string; + termsHash: string; + defaultTimeout: number; +} + +export interface AppInterface { + addr: string; + applyAction: string; + resolve: string; + getTurnTaker: string; + isStateTerminal: string; + stateEncoding: string; + actionEncoding: string | undefined; +} + +export interface AppInstance { + owner: string; + signingKeys: string[]; + appInterface: AppInterface; + terms: Terms; + defaultTimeout: number; +} + +export interface SignedStateHashUpdate { + stateHash: string; + nonce: number; + timeout: number; + signatures: string; +} + +export interface ETHBucketAppState { + alice: string; + bob: string; + aliceBalance: ethers.utils.BigNumber; + bobBalance: ethers.utils.BigNumber; +} + +export interface NetworkContext { + AppRegistry: string; + ETHBalanceRefund: string; + ETHBucket: string; + MultiSend: string; + NonceRegistry: string; + StateChannelTransaction: string; +} diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json new file mode 100644 index 000000000..3cb514556 --- /dev/null +++ b/packages/types/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/*"] +} diff --git a/packages/types/tslint.json b/packages/types/tslint.json new file mode 100644 index 000000000..394d8a239 --- /dev/null +++ b/packages/types/tslint.json @@ -0,0 +1,3 @@ +{ + "extends": ["../../tslint.json"] +} diff --git a/packages/typescript-typings/package.json b/packages/typescript-typings/package.json index c26edf3bd..697ca952d 100644 --- a/packages/typescript-typings/package.json +++ b/packages/typescript-typings/package.json @@ -12,6 +12,8 @@ }, "devDependencies": { "copyfiles": "^2.0.0", + "ethereum-waffle": "1.2.0", + "ethers": "4.0.20", "tslint": "5.11.0", "typescript": "^3.1.2" }, diff --git a/packages/typescript-typings/types/counterfactual/index.d.ts b/packages/typescript-typings/types/counterfactual/index.d.ts deleted file mode 100644 index 08f74657b..000000000 --- a/packages/typescript-typings/types/counterfactual/index.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -declare interface OffChainContractEntry { - contractName: string; - address: string; -} - -declare interface OffChainNetworkJson { - migrationVersion: number; - contracts: OffChainContractEntry[]; -} - -declare interface NetworkMapping { - [networkId: number]: { address: string }; -} - -declare interface BuildArtifact { - readonly contractName: string; - readonly abi: any[]; - readonly bytecode: string; - readonly networks: NetworkMapping; -} - -declare interface ArtifactsLoader { - require(name: string): BuildArtifact; -} - -declare interface TruffleDeployer { - deploy(artifact: BuildArtifact); - - link(artifact: BuildArtifact, libraries: BuildArtifact[]); -} \ No newline at end of file diff --git a/packages/typescript-typings/types/ethereum-waffle/index.d.ts b/packages/typescript-typings/types/ethereum-waffle/index.d.ts new file mode 100644 index 000000000..dedae1231 --- /dev/null +++ b/packages/typescript-typings/types/ethereum-waffle/index.d.ts @@ -0,0 +1,40 @@ +declare module "ethereum-waffle" { + import { Web3Provider } from "ethers/providers"; + import { Contract, Wallet } from "ethers"; + import * as waffle from "ethereum-waffle"; + + type ContractJSON = { + interface: any; + bytecode: string; + }; + + export function solidity(chai, utils): void; + + export function createMockProvider(ganacheOptions = {}): Web3Provider; + + export async function getWallets(provider: Web3Provider): Promise; + + export async function deployContract( + wallet: Wallet, + contractJSON: ContractJSON, + args?: any[], + overrideOptions?: object + ): Promise; + + export const link: ( + contract: Contract, + libraryName: string, + libraryAddress: string + ) => void; +} + +declare namespace Chai { + interface Assertion + extends LanguageChains, + NumericComparison, + TypeComparison { + reverted: Assertion; + revertedWith(revertReason: string): Assertion; + emit(contract: Contract, eventName: string): Assertion; + } +} diff --git a/tslint.json b/tslint.json index 001dc619a..43d674db1 100644 --- a/tslint.json +++ b/tslint.json @@ -1,17 +1,23 @@ { + "extends": [ "tslint-config-airbnb", "tslint-plugin-prettier", "tslint-config-prettier" ], + "rules": { + "prettier": true, + // NOTE: Added by Liam. I just prefer this rule to be on. Looks clean. "ordered-imports": [ true, {"grouped-imports": true} ], + // See https://github.com/counterfactual/monorepo/pull/231#discussion_r233860710 "import-name": false + } } diff --git a/yarn.lock b/yarn.lock index 754cdd106..3dafd890f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1433,21 +1433,7 @@ "@types/connect" "*" "@types/node" "*" -"@types/chai-as-promised@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.0.tgz#010b04cde78eacfb6e72bfddb3e58fe23c2e78b9" - integrity sha512-MFiW54UOSt+f2bRw8J7LgQeIvE/9b4oGvwU7XW30S9QGAiHGnU/fmiOprsyMkdmH2rl8xSPc0/yrQw8juXU6bQ== - dependencies: - "@types/chai" "*" - -"@types/chai-string@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@types/chai-string/-/chai-string-1.4.1.tgz#3a9d22716c27f2759bf272a4dbbdb593f18399e3" - integrity sha512-aRNMs6TKgjgPlCHwDfq/YNy5VtRR2hJ4AUWByddrT0TRVVD8eX4MiHW6/iHvmQHRlVuuPZcwnTUE7b4yFt7bEA== - dependencies: - "@types/chai" "*" - -"@types/chai@*": +"@types/chai@^4.1.7": version "4.1.7" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.1.7.tgz#1b8e33b61a8c09cbe1f85133071baa0dbf9fa71a" integrity sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA== @@ -1536,6 +1522,11 @@ resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.10.tgz#4897974cc317bf99d4fe6af1efa15957fa9c94de" integrity sha512-DC8xTuW/6TYgvEg3HEXS7cu9OijFqprVDXXiOcdOKZCU/5PJNLZU37VVvmZHdtMiGOa8wAA/We+JzbdxFzQTRQ== +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + "@types/keygrip@*": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.1.tgz#ff540462d2fb4d0a88441ceaf27d287b01c3d878" @@ -1577,6 +1568,11 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b" integrity sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA== +"@types/mocha@^5.2.5": + version "5.2.5" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.5.tgz#8a4accfc403c124a0bafe8a9fc61a05ec1032073" + integrity sha512-lAVp+Kj54ui/vLUFxsJTMtWvZraZxum3w3Nwkble2dNuV5VnPA+Mi2oGX9XYJAaIvZi3tn3cbjS/qcJXRb6Bww== + "@types/node@*", "@types/node@^10.3.2", "@types/node@^10.9.3": version "10.12.18" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67" @@ -1805,6 +1801,41 @@ abi-decoder@^1.0.8: dependencies: web3 "^0.18.4" +abstract-leveldown@0.12.3: + version "0.12.3" + resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-0.12.3.tgz#116b1ec5c7710ef7a2d5706768bbdb4440be1070" + integrity sha1-EWsexcdxDvei1XBnaLvbREC+EHA= + dependencies: + xtend "~3.0.0" + +abstract-leveldown@^2.4.1, abstract-leveldown@~2.7.1: + version "2.7.2" + resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-2.7.2.tgz#87a44d7ebebc341d59665204834c8b7e0932cc93" + integrity sha512-+OVvxH2rHVEhWLdbudP6p0+dNMXu8JA1CbhP19T8paTYAcX7oJ4OVjT+ZUVpv7mITxXHqDMej+GdqXBmXkw09w== + dependencies: + xtend "~4.0.0" + +abstract-leveldown@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-3.0.0.tgz#5cb89f958a44f526779d740d1440e743e0c30a57" + integrity sha512-KUWx9UWGQD12zsmLNj64/pndaz4iJh/Pj7nopgkfDG6RlCcbMZvT6+9l7dchK4idog2Is8VdC/PvNbFuFmalIQ== + dependencies: + xtend "~4.0.0" + +abstract-leveldown@~0.12.1: + version "0.12.4" + resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-0.12.4.tgz#29e18e632e60e4e221d5810247852a63d7b2e410" + integrity sha1-KeGOYy5g5OIh1YECR4UqY9ey5BA= + dependencies: + xtend "~3.0.0" + +abstract-leveldown@~2.6.0: + version "2.6.3" + resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-2.6.3.tgz#1c5e8c6a5ef965ae8c35dfb3a8770c476b82c4b8" + integrity sha512-2++wDf/DYqkPR3o5tbfdhF96EfMApo1GpPfzOsR/ZYXdkSmELlvOOEAl9iKkRsktMPHdGjO4rtkBpf2I7TiTeA== + dependencies: + xtend "~4.0.0" + accepts@^1.3.5, accepts@~1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" @@ -1848,6 +1879,11 @@ aes-js@3.0.0: resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" integrity sha1-4h3xCtbCBTKVvLuNq0Cwnb6ofk0= +aes-js@^0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-0.2.4.tgz#94b881ab717286d015fa219e08fb66709dda5a3d" + integrity sha1-lLiBq3FyhtAV+iGeCPtmcJ3aWj0= + agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0: version "4.2.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" @@ -1982,6 +2018,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argsarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/argsarray/-/argsarray-0.0.1.tgz#6e7207b4ecdb39b0af88303fa5ae22bda8df61cb" + integrity sha1-bnIHtOzbObCviDA/pa4ivajfYcs= + arr-diff@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" @@ -2097,7 +2138,7 @@ assert@^1.1.1: dependencies: util "0.10.3" -assertion-error@^1.1.0: +assertion-error@^1.0.1, assertion-error@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== @@ -2117,6 +2158,19 @@ async-each@^1.0.0: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" integrity sha1-GdOGodntxufByF04iu28xW0zYC0= +async-eventemitter@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/async-eventemitter/-/async-eventemitter-0.2.4.tgz#f5e7c8ca7d3e46aab9ec40a292baf686a0bafaca" + integrity sha512-pd20BwL7Yt1zwDFy+8MX8F1+WCT8aQeKj0kQnTrH9WaeRETlRamVhD0JtRPmrV4GfOJ2F9CvdQkZeZhnh2TuHw== + dependencies: + async "^2.4.0" + +async-eventemitter@ahultgren/async-eventemitter#fa06e39e56786ba541c180061dbf2c0a5bbf951c: + version "0.2.3" + resolved "https://codeload.github.com/ahultgren/async-eventemitter/tar.gz/fa06e39e56786ba541c180061dbf2c0a5bbf951c" + dependencies: + async "^2.4.0" + async-foreach@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" @@ -2127,12 +2181,12 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== -async@1.x: +async@1.x, async@^1.4.2, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= -async@2.6.1, async@^2.1.4, async@^2.5.0: +async@2.6.1, async@^2.0.1, async@^2.1.2, async@^2.1.4, async@^2.4.0, async@^2.5.0: version "2.6.1" resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== @@ -2176,7 +2230,7 @@ babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: esutils "^2.0.2" js-tokens "^3.0.2" -babel-core@^6.0.0, babel-core@^6.26.0: +babel-core@^6.0.0, babel-core@^6.0.14, babel-core@^6.26.0: version "6.26.3" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" integrity sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA== @@ -2215,6 +2269,111 @@ babel-generator@^6.18.0, babel-generator@^6.26.0: source-map "^0.5.7" trim-right "^1.0.1" +babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" + integrity sha1-zORReto1b0IgvK6KAsKzRvmlZmQ= + dependencies: + babel-helper-explode-assignable-expression "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-call-delegate@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" + integrity sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340= + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-define-map@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f" + integrity sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8= + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-helper-explode-assignable-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa" + integrity sha1-8luCz33BBDPFX3BZLVdGQArCLKo= + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-function-name@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" + integrity sha1-00dbjAPtmCQqJbSDUasYOZ01gKk= + dependencies: + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-get-function-arity@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" + integrity sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0= + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-hoist-variables@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" + integrity sha1-HssnaJydJVE+rbyZFKc/VAi+enY= + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-optimise-call-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" + integrity sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc= + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-regex@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72" + integrity sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI= + dependencies: + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-helper-remap-async-to-generator@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b" + integrity sha1-XsWBgnrXI/7N04HxySg5BnbkVRs= + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-replace-supers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" + integrity sha1-v22/5Dk40XNpohPKiov3S2qQqxo= + dependencies: + babel-helper-optimise-call-expression "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + babel-helpers@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" @@ -2248,6 +2407,13 @@ babel-messages@^6.23.0: dependencies: babel-runtime "^6.22.0" +babel-plugin-check-es2015-constants@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" + integrity sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o= + dependencies: + babel-runtime "^6.22.0" + babel-plugin-istanbul@^4.1.6: version "4.1.6" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45" @@ -2263,11 +2429,285 @@ babel-plugin-jest-hoist@^23.2.0: resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.2.0.tgz#e61fae05a1ca8801aadee57a6d66b8cefaf44167" integrity sha1-5h+uBaHKiAGq3uV6bWa4zvr0QWc= +babel-plugin-syntax-async-functions@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" + integrity sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU= + +babel-plugin-syntax-exponentiation-operator@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" + integrity sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4= + babel-plugin-syntax-object-rest-spread@^6.13.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" integrity sha1-/WU28rzhODb/o6VFjEkDpZe7O/U= +babel-plugin-syntax-trailing-function-commas@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" + integrity sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM= + +babel-plugin-transform-async-to-generator@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" + integrity sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E= + dependencies: + babel-helper-remap-async-to-generator "^6.24.1" + babel-plugin-syntax-async-functions "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-arrow-functions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" + integrity sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" + integrity sha1-u8UbSflk1wy42OC5ToICRs46YUE= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoping@^6.23.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f" + integrity sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8= + dependencies: + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-plugin-transform-es2015-classes@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" + integrity sha1-WkxYpQyclGHlZLSyo7+ryXolhNs= + dependencies: + babel-helper-define-map "^6.24.1" + babel-helper-function-name "^6.24.1" + babel-helper-optimise-call-expression "^6.24.1" + babel-helper-replace-supers "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-computed-properties@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3" + integrity sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM= + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-destructuring@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" + integrity sha1-mXux8auWf2gtKwh2/jWNYOdlxW0= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-duplicate-keys@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e" + integrity sha1-c+s9MQypaePvnskcU3QabxV2Qj4= + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-for-of@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" + integrity sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-function-name@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b" + integrity sha1-g0yJhTvDaxrw86TF26qU/Y6sqos= + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-literals@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" + integrity sha1-T1SgLWzWbPkVKAAZox0xklN3yi4= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154" + integrity sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ= + dependencies: + babel-plugin-transform-es2015-modules-commonjs "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1: + version "6.26.2" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3" + integrity sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q== + dependencies: + babel-plugin-transform-strict-mode "^6.24.1" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-types "^6.26.0" + +babel-plugin-transform-es2015-modules-systemjs@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23" + integrity sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM= + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-umd@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468" + integrity sha1-rJl+YoXNGO1hdq22B9YCNErThGg= + dependencies: + babel-plugin-transform-es2015-modules-amd "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-object-super@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d" + integrity sha1-JM72muIcuDp/hgPa0CH1cusnj40= + dependencies: + babel-helper-replace-supers "^6.24.1" + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-parameters@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" + integrity sha1-V6w1GrScrxSpfNE7CfZv3wpiXys= + dependencies: + babel-helper-call-delegate "^6.24.1" + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-shorthand-properties@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0" + integrity sha1-JPh11nIch2YbvZmkYi5R8U3jiqA= + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-spread@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" + integrity sha1-1taKmfia7cRTbIGlQujdnxdG+NE= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-sticky-regex@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc" + integrity sha1-AMHNsaynERLN8M9hJsLta0V8zbw= + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-template-literals@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" + integrity sha1-qEs0UPfp+PH2g51taH2oS7EjbY0= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-typeof-symbol@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" + integrity sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-unicode-regex@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9" + integrity sha1-04sS9C6nMj9yk4fxinxa4frrNek= + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + regexpu-core "^2.0.0" + +babel-plugin-transform-exponentiation-operator@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e" + integrity sha1-KrDJx/MJj6SJB3cruBP+QejeOg4= + dependencies: + babel-helper-builder-binary-assignment-operator-visitor "^6.24.1" + babel-plugin-syntax-exponentiation-operator "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-regenerator@^6.22.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" + integrity sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8= + dependencies: + regenerator-transform "^0.10.0" + +babel-plugin-transform-strict-mode@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" + integrity sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g= + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-preset-env@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.7.0.tgz#dea79fa4ebeb883cd35dab07e260c1c9c04df77a" + integrity sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg== + dependencies: + babel-plugin-check-es2015-constants "^6.22.0" + babel-plugin-syntax-trailing-function-commas "^6.22.0" + babel-plugin-transform-async-to-generator "^6.22.0" + babel-plugin-transform-es2015-arrow-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoping "^6.23.0" + babel-plugin-transform-es2015-classes "^6.23.0" + babel-plugin-transform-es2015-computed-properties "^6.22.0" + babel-plugin-transform-es2015-destructuring "^6.23.0" + babel-plugin-transform-es2015-duplicate-keys "^6.22.0" + babel-plugin-transform-es2015-for-of "^6.23.0" + babel-plugin-transform-es2015-function-name "^6.22.0" + babel-plugin-transform-es2015-literals "^6.22.0" + babel-plugin-transform-es2015-modules-amd "^6.22.0" + babel-plugin-transform-es2015-modules-commonjs "^6.23.0" + babel-plugin-transform-es2015-modules-systemjs "^6.23.0" + babel-plugin-transform-es2015-modules-umd "^6.23.0" + babel-plugin-transform-es2015-object-super "^6.22.0" + babel-plugin-transform-es2015-parameters "^6.23.0" + babel-plugin-transform-es2015-shorthand-properties "^6.22.0" + babel-plugin-transform-es2015-spread "^6.22.0" + babel-plugin-transform-es2015-sticky-regex "^6.22.0" + babel-plugin-transform-es2015-template-literals "^6.22.0" + babel-plugin-transform-es2015-typeof-symbol "^6.23.0" + babel-plugin-transform-es2015-unicode-regex "^6.22.0" + babel-plugin-transform-exponentiation-operator "^6.22.0" + babel-plugin-transform-regenerator "^6.22.0" + browserslist "^3.2.6" + invariant "^2.2.2" + semver "^5.3.0" + babel-preset-jest@^23.2.0: version "23.2.0" resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-23.2.0.tgz#8ec7a03a138f001a1a8fb1e8113652bf1a55da46" @@ -2289,7 +2729,7 @@ babel-register@^6.26.0: mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-runtime@^6.22.0, babel-runtime@^6.26.0: +babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= @@ -2308,7 +2748,7 @@ babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0: babylon "^6.18.0" lodash "^4.17.4" -babel-traverse@^6.0.0, babel-traverse@^6.18.0, babel-traverse@^6.26.0: +babel-traverse@^6.0.0, babel-traverse@^6.18.0, babel-traverse@^6.24.1, babel-traverse@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4= @@ -2323,7 +2763,7 @@ babel-traverse@^6.0.0, babel-traverse@^6.18.0, babel-traverse@^6.26.0: invariant "^2.2.2" lodash "^4.17.4" -babel-types@^6.0.0, babel-types@^6.18.0, babel-types@^6.26.0: +babel-types@^6.0.0, babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc= @@ -2333,16 +2773,36 @@ babel-types@^6.0.0, babel-types@^6.18.0, babel-types@^6.26.0: lodash "^4.17.4" to-fast-properties "^1.0.3" +babelify@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/babelify/-/babelify-7.3.0.tgz#aa56aede7067fd7bd549666ee16dc285087e88e5" + integrity sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU= + dependencies: + babel-core "^6.0.14" + object-assign "^4.0.0" + babylon@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== +backoff@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/backoff/-/backoff-2.5.0.tgz#f616eda9d3e4b66b8ca7fca79f695722c5f8e26f" + integrity sha1-9hbtqdPktmuMp/ynn2lXIsX44m8= + dependencies: + precond "0.2" + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base-x@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-1.1.0.tgz#42d3d717474f9ea02207f6d1aa1f426913eeb7ac" + integrity sha1-QtPXF0dPnqAiB/bRqh9CaRPut6w= + base64-js@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978" @@ -2412,6 +2872,17 @@ bindings@^1.2.1, bindings@^1.3.1: resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.1.tgz#21fc7c6d67c18516ec5aaa2815b145ff77b26ea5" integrity sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew== +bip39@~2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/bip39/-/bip39-2.4.0.tgz#a0b8adbf163f53495f00f05d9ede7c25369ccf13" + integrity sha512-1++HywqIyPtWDo7gm4v0ylYbwkLvHkuwVSKbBlZBbTCP/mnkyrlARBny906VLAwxJbC5xw9EvuJasHFIZaIFMQ== + dependencies: + create-hash "^1.1.0" + pbkdf2 "^3.0.9" + randombytes "^2.0.1" + safe-buffer "^5.0.1" + unorm "^1.3.3" + bip66@^1.1.3: version "1.1.5" resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22" @@ -2427,6 +2898,13 @@ bl@^1.0.0: readable-stream "^2.3.5" safe-buffer "^5.1.1" +bl@~0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/bl/-/bl-0.8.2.tgz#c9b6bca08d1bc2ea00fc8afb4f1a5fd1e1c66e4e" + integrity sha1-yba8oI0bwuoA/Ir7Txpf0eHGbk4= + dependencies: + readable-stream "~1.0.26" + block-stream@*: version "0.0.9" resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" @@ -2449,7 +2927,7 @@ bn.js@4.11.6: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" integrity sha1-UzRK2xRhehP26N0s4okF0cC6MhU= -bn.js@4.11.8, bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.10.0, bn.js@^4.11.0, bn.js@^4.11.3, bn.js@^4.11.6, bn.js@^4.4.0, bn.js@^4.8.0: +bn.js@4.11.8, bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.10.0, bn.js@^4.11.0, bn.js@^4.11.3, bn.js@^4.11.6, bn.js@^4.11.8, bn.js@^4.4.0, bn.js@^4.8.0: version "4.11.8" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== @@ -2592,6 +3070,14 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" +browserslist@^3.2.6: + version "3.2.8" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-3.2.8.tgz#b0005361d6471f0f5952797a76fc985f1f978fc6" + integrity sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ== + dependencies: + caniuse-lite "^1.0.30000844" + electron-to-chromium "^1.3.47" + browserslist@^4.3.4: version "4.3.6" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.3.6.tgz#0f9d9081afc66b36f477c6bdf3813f784f42396a" @@ -2608,6 +3094,26 @@ bs-logger@0.x: dependencies: fast-json-stable-stringify "2.x" +bs58@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-2.0.1.tgz#55908d58f1982aba2008fa1bed8f91998a29bf8d" + integrity sha1-VZCNWPGYKrogCPob7Y+RmYopv40= + +bs58@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-3.1.0.tgz#d4c26388bf4804cac714141b1945aa47e5eb248e" + integrity sha1-1MJjiL9IBMrHFBQbGUWqR+XrJI4= + dependencies: + base-x "^1.1.0" + +bs58check@^1.0.8: + version "1.3.4" + resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-1.3.4.tgz#c52540073749117714fa042c3047eb8f9151cbf8" + integrity sha1-xSVABzdJEXcU+gQsMEfrj5FRy/g= + dependencies: + bs58 "^3.1.0" + create-hash "^1.1.0" + bser@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" @@ -2643,6 +3149,11 @@ buffer-from@1.x, buffer-from@^1.0.0, buffer-from@^1.1.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer-from@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-0.1.2.tgz#15f4b9bcef012044df31142c14333caf6e0260d0" + integrity sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg== + buffer-to-arraybuffer@^0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/buffer-to-arraybuffer/-/buffer-to-arraybuffer-0.0.5.tgz#6064a40fa76eb43c723aba9ef8f6e1216d10511a" @@ -2671,7 +3182,7 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.0.5: +buffer@^5.0.5, buffer@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.1.tgz#dd57fa0f109ac59c602479044dca7b8b3d0b71d6" integrity sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg== @@ -2721,6 +3232,21 @@ bytes@3.0.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= +bytewise-core@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/bytewise-core/-/bytewise-core-1.2.3.tgz#3fb410c7e91558eb1ab22a82834577aa6bd61d42" + integrity sha1-P7QQx+kVWOsasiqCg0V3qmvWHUI= + dependencies: + typewise-core "^1.2" + +bytewise@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/bytewise/-/bytewise-1.1.0.tgz#1d13cbff717ae7158094aa881b35d081b387253e" + integrity sha1-HRPL/3F65xWAlKqIGzXQgbOHJT4= + dependencies: + bytewise-core "^1.2.2" + typewise "^1.0.3" + cacache@^11.0.1, cacache@^11.0.2, cacache@^11.3.2: version "11.3.2" resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.2.tgz#2d81e308e3d258ca38125b676b98b2ac9ce69bfa" @@ -2764,6 +3290,14 @@ cache-content-type@^1.0.0: mime-types "^2.1.18" ylru "^1.2.0" +cachedown@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cachedown/-/cachedown-1.0.0.tgz#d43f036e4510696b31246d7db31ebf0f7ac32d15" + integrity sha1-1D8DbkUQaWsxJG19sx6/D3rDLRU= + dependencies: + abstract-leveldown "^2.4.1" + lru-cache "^3.2.0" + call-me-maybe@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" @@ -2825,7 +3359,7 @@ camelcase@^5.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== -caniuse-lite@^1.0.30000921: +caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30000921: version "1.0.30000923" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000923.tgz#148f9bda508024b5ce957b463ae2e8302b451bb2" integrity sha512-j5ur7eeluOFjjPUkydtXP4KFAsmH3XaQNch5tvWSO+dLHYt5PE+VgJZLWtbVOodfWij6m6zas28T4gB/cLYq1w== @@ -2842,22 +3376,14 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chai-as-promised@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/chai-as-promised/-/chai-as-promised-7.1.1.tgz#08645d825deb8696ee61725dbf590c012eb00ca0" - integrity sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA== +chai@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-3.5.0.tgz#4d02637b067fe958bdbfdd3a40ec56fef7373247" + integrity sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc= dependencies: - check-error "^1.0.2" - -chai-bignumber@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chai-bignumber/-/chai-bignumber-3.0.0.tgz#e90cf1f468355bbb11a9acd051222586cd2648a9" - integrity sha512-SubOtaSI2AILWTWe2j0c6i2yFT/f9J6UBjeVGDuwDiPLkF/U5+/eTWUE3sbCZ1KgcPF6UJsDVYbIxaYA097MQA== - -chai-string@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/chai-string/-/chai-string-1.5.0.tgz#0bdb2d8a5f1dbe90bc78ec493c1c1c180dd4d3d2" - integrity sha512-sydDC3S3pNAQMYwJrs6dQX0oBQ6KfIPuOZ78n7rocW0eJJlsHPh2t3kwW7xfwYA/1Bf6/arGtSUo16rxR2JFlw== + assertion-error "^1.0.1" + deep-eql "^0.1.3" + type-detect "^1.0.0" chai@^4.2.0: version "4.2.0" @@ -2906,6 +3432,13 @@ check-error@^1.0.2: resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= +checkpoint-store@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/checkpoint-store/-/checkpoint-store-1.1.0.tgz#04e4cb516b91433893581e6d4601a78e9552ea06" + integrity sha1-BOTLUWuRQziTWB5tRgGnjpVS6gY= + dependencies: + functional-red-black-tree "^1.0.1" + chokidar@2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.3.tgz#dcbd4f6cbb2a55b4799ba8a840ac527e5f4b1176" @@ -3056,6 +3589,11 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= +clone@^2.0.0, clone@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= + cmd-shim@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb" @@ -3084,6 +3622,14 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= +coinstring@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/coinstring/-/coinstring-2.3.0.tgz#cdb63363a961502404a25afb82c2e26d5ff627a4" + integrity sha1-zbYzY6lhUCQEolr7gsLibV/2J6Q= + dependencies: + bs58 "^2.0.1" + create-hash "^1.1.1" + collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" @@ -3189,7 +3735,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@1.6.2, concat-stream@^1.4.6, concat-stream@^1.5.0, concat-stream@^1.6.0: +concat-stream@1.6.2, concat-stream@^1.4.6, concat-stream@^1.5.0, concat-stream@^1.5.1, concat-stream@^1.6.0: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -3395,6 +3941,11 @@ copyfiles@^2.0.0: through2 "^2.0.1" yargs "^11.0.0" +core-js@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" + integrity sha1-TekR5mew6ukSTjQlS1OupvxhjT4= + core-js@2.5.5: version "2.5.5" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.5.tgz#b14dde936c640c0579a6b50cabcc132dd6127e3b" @@ -3436,7 +3987,7 @@ create-ecdh@^4.0.0: bn.js "^4.1.0" elliptic "^6.0.0" -create-hash@^1.1.0, create-hash@^1.1.2: +create-hash@^1.1.0, create-hash@^1.1.1, create-hash@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== @@ -3459,6 +4010,14 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" +cross-fetch@^2.1.0, cross-fetch@^2.1.1: + version "2.2.3" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-2.2.3.tgz#e8a0b3c54598136e037f8650f8e823ccdfac198e" + integrity sha512-PrWWNH3yL2NYIb/7WF/5vFG3DCQiXDOVf8k3ijatbrtnwNuhMWLC7YF7uqf53tbTFDzHIUD8oITw4Bxt8ST3Nw== + dependencies: + node-fetch "2.1.2" + whatwg-fetch "2.0.4" + cross-spawn@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" @@ -3560,6 +4119,11 @@ cyclist@~0.2.2: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= +d64@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/d64/-/d64-1.0.0.tgz#4002a87e850cbfc9f9d9706b60fca613a3336e90" + integrity sha1-QAKofoUMv8n52XBrYPymE6MzbpA= + daemon@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/daemon/-/daemon-1.1.0.tgz#6c5102c81db0be856fc9008fc2c935b398864ae8" @@ -3719,6 +4283,13 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-eql@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2" + integrity sha1-71WKyrjeJSBs1xOQbXTlaTDrafI= + dependencies: + type-detect "0.1.1" + deep-eql@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" @@ -3741,6 +4312,11 @@ deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= +deepmerge@^2.0.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" + integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== + default-require-extensions@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8" @@ -3755,6 +4331,20 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +deferred-leveldown@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/deferred-leveldown/-/deferred-leveldown-0.2.0.tgz#2cef1f111e1c57870d8bbb8af2650e587cd2f5b4" + integrity sha1-LO8fER4cV4cNi7uK8mUOWHzS9bQ= + dependencies: + abstract-leveldown "~0.12.1" + +deferred-leveldown@~1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/deferred-leveldown/-/deferred-leveldown-1.2.2.tgz#3acd2e0b75d1669924bc0a4b642851131173e1eb" + integrity sha512-uukrWD2bguRtXilKt6cAWKyoXrTSMo5m7crUdLfWQmu8kIm88w3QZoUL+6nhpfKVmhHANER6Re3sKoNoZ3IKMA== + dependencies: + abstract-leveldown "~2.6.0" + define-properties@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -3784,6 +4374,11 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" +defined@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" + integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -3913,6 +4508,13 @@ dot-prop@^4.1.0, dot-prop@^4.2.0: dependencies: is-obj "^1.0.0" +dotenv-safe@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/dotenv-safe/-/dotenv-safe-6.1.0.tgz#df6e72ced7d5bcfd9d870c9f656962a02197337b" + integrity sha512-O02OUTS+XmoRNZR4kRjJ9jlUGvQoXpMeTVVEBc8hUtgvPTgVZpsZH7TOocq4RVDpPrs2xGPwj6gIWjqRX+ErHA== + dependencies: + dotenv "^6.1.0" + dotenv@^6.1.0: version "6.2.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.2.0.tgz#941c0410535d942c8becf28d3f357dbd9d476064" @@ -3960,7 +4562,7 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.92: +electron-to-chromium@^1.3.47, electron-to-chromium@^1.3.92: version "1.3.96" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.96.tgz#25770ec99b8b07706dedf3a5f43fa50cb54c4f9a" integrity sha512-ZUXBUyGLeoJxp4Nt6G/GjBRLnyz8IKQGexZ2ndWaoegThgMGFO1tdDYID5gBV32/1S83osjJHyfzvanE/8HY4Q== @@ -4031,7 +4633,7 @@ err-code@^1.0.0: resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" integrity sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA= -errno@^0.1.3, errno@~0.1.7: +errno@^0.1.3, errno@~0.1.1, errno@~0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg== @@ -4050,7 +4652,7 @@ error-inject@^1.0.0: resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" integrity sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc= -es-abstract@^1.5.1: +es-abstract@^1.5.0, es-abstract@^1.5.1: version "1.12.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" integrity sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA== @@ -4194,6 +4796,33 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +eth-block-tracker@^2.2.2: + version "2.3.1" + resolved "https://registry.yarnpkg.com/eth-block-tracker/-/eth-block-tracker-2.3.1.tgz#ab6d177e5b50128fa06d7ae9e0489c7484bac95e" + integrity sha512-NamWuMBIl8kmkJFVj8WzGatySTzQPQag4Xr677yFxdVtIxACFbL/dQowk0MzEqIKk93U1TwY3MjVU6mOcwZnKA== + dependencies: + async-eventemitter ahultgren/async-eventemitter#fa06e39e56786ba541c180061dbf2c0a5bbf951c + eth-query "^2.1.0" + ethereumjs-tx "^1.3.3" + ethereumjs-util "^5.1.3" + ethjs-util "^0.1.3" + json-rpc-engine "^3.6.0" + pify "^2.3.0" + tape "^4.6.3" + +eth-block-tracker@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/eth-block-tracker/-/eth-block-tracker-3.0.1.tgz#95cd5e763c7293e0b1b2790a2a39ac2ac188a5e1" + integrity sha512-WUVxWLuhMmsfenfZvFO5sbl1qFY2IqUlw/FPVmjjdElpqLsZtSG+wPe9Dz7W/sB6e80HgFKknOmKk2eNlznHug== + dependencies: + eth-query "^2.1.0" + ethereumjs-tx "^1.3.3" + ethereumjs-util "^5.1.3" + ethjs-util "^0.1.3" + json-rpc-engine "^3.6.0" + pify "^2.3.0" + tape "^4.6.3" + eth-ens-namehash@2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/eth-ens-namehash/-/eth-ens-namehash-2.0.8.tgz#229ac46eca86d52e0c991e7cb2aef83ff0f68bcf" @@ -4220,6 +4849,36 @@ eth-gas-reporter@0.1.12: solidity-parser-antlr "^0.2.10" sync-request "^6.0.0" +eth-json-rpc-infura@^3.1.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/eth-json-rpc-infura/-/eth-json-rpc-infura-3.1.2.tgz#04c5d0cee98619e93ba8a9842492b771b316e83a" + integrity sha512-IuK5Iowfs6taluA/3Okmu6EfZcFMq6MQuyrUL1PrCoJstuuBr3TvVeSy3keDyxfbrjFB34nCo538I8G+qMtsbw== + dependencies: + cross-fetch "^2.1.1" + eth-json-rpc-middleware "^1.5.0" + json-rpc-engine "^3.4.0" + json-rpc-error "^2.0.0" + tape "^4.8.0" + +eth-json-rpc-middleware@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/eth-json-rpc-middleware/-/eth-json-rpc-middleware-1.6.0.tgz#5c9d4c28f745ccb01630f0300ba945f4bef9593f" + integrity sha512-tDVCTlrUvdqHKqivYMjtFZsdD7TtpNLBCfKAcOpaVs7orBMS/A8HWro6dIzNtTZIR05FAbJ3bioFOnZpuCew9Q== + dependencies: + async "^2.5.0" + eth-query "^2.1.2" + eth-tx-summary "^3.1.2" + ethereumjs-block "^1.6.0" + ethereumjs-tx "^1.3.3" + ethereumjs-util "^5.1.2" + ethereumjs-vm "^2.1.0" + fetch-ponyfill "^4.0.0" + json-rpc-engine "^3.6.0" + json-rpc-error "^2.0.0" + json-stable-stringify "^1.0.1" + promise-to-callback "^1.0.0" + tape "^4.6.3" + eth-lib@0.1.27, eth-lib@^0.1.26: version "0.1.27" resolved "https://registry.yarnpkg.com/eth-lib/-/eth-lib-0.1.27.tgz#f0b0fd144f865d2d6bf8257a40004f2e75ca1dd6" @@ -4242,7 +4901,78 @@ eth-lib@0.2.7: elliptic "^6.4.0" xhr-request-promise "^0.1.2" -ethereumjs-abi@^0.6.5: +eth-query@^2.0.2, eth-query@^2.1.0, eth-query@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/eth-query/-/eth-query-2.1.2.tgz#d6741d9000106b51510c72db92d6365456a6da5e" + integrity sha1-1nQdkAAQa1FRDHLbktY2VFam2l4= + dependencies: + json-rpc-random-id "^1.0.0" + xtend "^4.0.1" + +eth-sig-util@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/eth-sig-util/-/eth-sig-util-1.4.2.tgz#8d958202c7edbaae839707fba6f09ff327606210" + integrity sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA= + dependencies: + ethereumjs-abi "git+https://github.com/ethereumjs/ethereumjs-abi.git" + ethereumjs-util "^5.1.1" + +eth-sig-util@^2.0.2: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eth-sig-util/-/eth-sig-util-2.1.0.tgz#33e60e5486897a2ddeb4bf5a0993b2c6d5cc9e19" + integrity sha512-JRKmq1zytYoOuAj8llYiGlRGSlWrQ0jGGh9+YPhELfmMP1PD/dkwq2kzMoB8pRF6sEgZojQfSasswto3xsKFvw== + dependencies: + buffer "^5.2.1" + elliptic "^6.4.0" + ethereumjs-abi "0.6.5" + ethereumjs-util "^5.1.1" + tweetnacl "^1.0.0" + tweetnacl-util "^0.15.0" + +eth-tx-summary@^3.1.2: + version "3.2.3" + resolved "https://registry.yarnpkg.com/eth-tx-summary/-/eth-tx-summary-3.2.3.tgz#a52d7215616888e012fbc083b3eacd28f3e64764" + integrity sha512-1gZpA5fKarJOVSb5OUlPnhDQuIazqAkI61zlVvf5LdG47nEgw+/qhyZnuj3CUdE/TLTKuRzPLeyXLjaB4qWTRQ== + dependencies: + async "^2.1.2" + bn.js "^4.11.8" + clone "^2.0.0" + concat-stream "^1.5.1" + end-of-stream "^1.1.0" + eth-query "^2.0.2" + ethereumjs-block "^1.4.1" + ethereumjs-tx "^1.1.1" + ethereumjs-util "^5.0.1" + ethereumjs-vm "2.3.4" + through2 "^2.0.3" + treeify "^1.0.1" + web3-provider-engine "^13.3.2" + +ethereum-common@0.0.16: + version "0.0.16" + resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.0.16.tgz#9a1e169ead34ab75e089f50ca512bfd0fbd12655" + integrity sha1-mh4Wnq00q3XgifUMpRK/0PvRJlU= + +ethereum-common@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.2.0.tgz#13bf966131cce1eeade62a1b434249bb4cb120ca" + integrity sha512-XOnAR/3rntJgbCdGhqdaLIxDLWKLmsZOGhHdBKadEr6gEnJLH52k93Ou+TUdFaPN3hJc3isBZBal3U/XZ15abA== + +ethereum-common@^0.0.18: + version "0.0.18" + resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.0.18.tgz#2fdc3576f232903358976eb39da783213ff9523f" + integrity sha1-L9w1dvIykDNYl26znaeDIT/5Uj8= + +ethereum-waffle@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/ethereum-waffle/-/ethereum-waffle-1.2.0.tgz#832717d55734e8f111f1a3d0f9c4599d3484f9d0" + integrity sha512-eTtXioTXeGj3V3PTVI8OlTMP4l8InnYKHFHS57cDW91PQ+YdXgT24mTDkSStPBuE5/acxxvFDwJqwgi3UaJRaA== + dependencies: + ethers "^4.0.0" + ganache-core "2.2.1" + solc "^0.4.24" + +ethereumjs-abi@0.6.5, ethereumjs-abi@^0.6.5: version "0.6.5" resolved "https://registry.yarnpkg.com/ethereumjs-abi/-/ethereumjs-abi-0.6.5.tgz#5a637ef16ab43473fa72a29ad90871405b3f5241" integrity sha1-WmN+8Wq0NHP6cqKa2QhxQFs/UkE= @@ -4250,6 +4980,60 @@ ethereumjs-abi@^0.6.5: bn.js "^4.10.0" ethereumjs-util "^4.3.0" +"ethereumjs-abi@git+https://github.com/ethereumjs/ethereumjs-abi.git": + version "0.6.5" + resolved "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799" + dependencies: + bn.js "^4.10.0" + ethereumjs-util "^5.0.0" + +ethereumjs-account@^2.0.3, ethereumjs-account@~2.0.4: + version "2.0.5" + resolved "https://registry.yarnpkg.com/ethereumjs-account/-/ethereumjs-account-2.0.5.tgz#eeafc62de544cb07b0ee44b10f572c9c49e00a84" + integrity sha512-bgDojnXGjhMwo6eXQC0bY6UK2liSFUSMwwylOmQvZbSl/D7NXQ3+vrGO46ZeOgjGfxXmgIeVNDIiHw7fNZM4VA== + dependencies: + ethereumjs-util "^5.0.0" + rlp "^2.0.0" + safe-buffer "^5.1.1" + +ethereumjs-block@^1.2.2, ethereumjs-block@^1.4.1, ethereumjs-block@^1.6.0, ethereumjs-block@~1.7.0: + version "1.7.1" + resolved "https://registry.yarnpkg.com/ethereumjs-block/-/ethereumjs-block-1.7.1.tgz#78b88e6cc56de29a6b4884ee75379b6860333c3f" + integrity sha512-B+sSdtqm78fmKkBq78/QLKJbu/4Ts4P2KFISdgcuZUPDm9x+N7qgBPIIFUGbaakQh8bzuquiRVbdmvPKqbILRg== + dependencies: + async "^2.0.1" + ethereum-common "0.2.0" + ethereumjs-tx "^1.2.2" + ethereumjs-util "^5.0.0" + merkle-patricia-tree "^2.1.2" + +ethereumjs-block@~1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/ethereumjs-block/-/ethereumjs-block-1.2.2.tgz#2ec7534a59021b8ec9b83c30e49690c6ebaedda1" + integrity sha1-LsdTSlkCG47JuDww5JaQxuuu3aE= + dependencies: + async "^1.5.2" + ethereum-common "0.0.16" + ethereumjs-tx "^1.0.0" + ethereumjs-util "^4.0.1" + merkle-patricia-tree "^2.1.2" + +ethereumjs-block@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ethereumjs-block/-/ethereumjs-block-2.1.0.tgz#71d1b19e18061f14cf6371bf34ba31a359931360" + integrity sha512-ip+x4/7hUInX+TQfhEKsQh9MJK1Dbjp4AuPjf1UdX3udAV4beYD4EMCNIPzBLCsGS8WQZYXLpo83tVTISYNpow== + dependencies: + async "^2.0.1" + ethereumjs-common "^0.6.0" + ethereumjs-tx "^1.2.2" + ethereumjs-util "^5.0.0" + merkle-patricia-tree "^2.1.2" + +ethereumjs-common@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/ethereumjs-common/-/ethereumjs-common-0.6.1.tgz#ec98edf315a7f107afb6acc48e937a8266979fae" + integrity sha512-4jOrfDu9qOBTTGGb3zrfT1tE1Hyc6a8LJpEk0Vk9AYlLkBY7crjVICyJpRvjNI+KLDMpMITMw3eWVZOLMtZdhw== + ethereumjs-testrpc-sc@6.1.6: version "6.1.6" resolved "https://registry.yarnpkg.com/ethereumjs-testrpc-sc/-/ethereumjs-testrpc-sc-6.1.6.tgz#290595380b5182814564d4aa38f35b7788aab070" @@ -4257,7 +5041,23 @@ ethereumjs-testrpc-sc@6.1.6: dependencies: source-map-support "^0.5.3" -ethereumjs-util@^4.3.0: +ethereumjs-tx@1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/ethereumjs-tx/-/ethereumjs-tx-1.3.4.tgz#c2304912f6c07af03237ad8675ac036e290dad48" + integrity sha512-kOgUd5jC+0tgV7t52UDECMMz9Uf+Lro+6fSpCvzWemtXfMEcwI3EOxf5mVPMRbTFkMMhuERokNNVF3jItAjidg== + dependencies: + ethereum-common "^0.0.18" + ethereumjs-util "^5.0.0" + +ethereumjs-tx@^1.0.0, ethereumjs-tx@^1.1.1, ethereumjs-tx@^1.2.0, ethereumjs-tx@^1.2.2, ethereumjs-tx@^1.3.3: + version "1.3.7" + resolved "https://registry.yarnpkg.com/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz#88323a2d875b10549b8347e09f4862b546f3d89a" + integrity sha512-wvLMxzt1RPhAQ9Yi3/HKZTn0FZYpnsmQdbKYfUUpi4j1SEIcbkd9tndVjcPrufY3V7j2IebOpC00Zp2P/Ay2kA== + dependencies: + ethereum-common "^0.0.18" + ethereumjs-util "^5.0.0" + +ethereumjs-util@^4.0.1, ethereumjs-util@^4.3.0, ethereumjs-util@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz#3e9428b317eebda3d7260d854fddda954b1f1bc6" integrity sha1-PpQosxfuvaPXJg2FT93alUsfG8Y= @@ -4268,7 +5068,7 @@ ethereumjs-util@^4.3.0: rlp "^2.0.0" secp256k1 "^3.0.1" -ethereumjs-util@^5.2.0: +ethereumjs-util@^5.0.0, ethereumjs-util@^5.0.1, ethereumjs-util@^5.1.1, ethereumjs-util@^5.1.2, ethereumjs-util@^5.1.3, ethereumjs-util@^5.1.5, ethereumjs-util@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz#3e0c0d1741471acf1036052d048623dee54ad642" integrity sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA== @@ -4281,6 +5081,83 @@ ethereumjs-util@^5.2.0: safe-buffer "^5.1.1" secp256k1 "^3.0.1" +ethereumjs-util@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-6.0.0.tgz#f14841c182b918615afefd744207c7932c8536c0" + integrity sha512-E3yKUyl0Fs95nvTFQZe/ZSNcofhDzUsDlA5y2uoRmf1+Ec7gpGhNCsgKkZBRh7Br5op8mJcYF/jFbmjj909+nQ== + dependencies: + bn.js "^4.11.0" + create-hash "^1.1.2" + ethjs-util "^0.1.6" + keccak "^1.0.2" + rlp "^2.0.0" + safe-buffer "^5.1.1" + secp256k1 "^3.0.1" + +ethereumjs-vm@2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/ethereumjs-vm/-/ethereumjs-vm-2.3.4.tgz#f635d7cb047571a1840a6e9a74d29de4488f8ad6" + integrity sha512-Y4SlzNDqxrCO58jhp98HdnZVdjOqB+HC0hoU+N/DEp1aU+hFkRX/nru5F7/HkQRPIlA6aJlQp/xIA6xZs1kspw== + dependencies: + async "^2.1.2" + async-eventemitter "^0.2.2" + ethereum-common "0.2.0" + ethereumjs-account "^2.0.3" + ethereumjs-block "~1.7.0" + ethereumjs-util "^5.1.3" + fake-merkle-patricia-tree "^1.0.1" + functional-red-black-tree "^1.0.1" + merkle-patricia-tree "^2.1.2" + rustbn.js "~0.1.1" + safe-buffer "^5.1.1" + +ethereumjs-vm@2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/ethereumjs-vm/-/ethereumjs-vm-2.3.5.tgz#e69306737b8a7ea80c633ceb9b7dd561897007de" + integrity sha512-AJ7x44+xqyE5+UO3Nns19WkTdZfyqFZ+sEjIEpvme7Ipbe3iBU1uwCcHEdiu/yY9bdhr3IfSa/NfIKNeXPaRVQ== + dependencies: + async "^2.1.2" + async-eventemitter "^0.2.2" + ethereum-common "0.2.0" + ethereumjs-account "^2.0.3" + ethereumjs-block "~1.7.0" + ethereumjs-util "^5.1.3" + fake-merkle-patricia-tree "^1.0.1" + functional-red-black-tree "^1.0.1" + merkle-patricia-tree "^2.1.2" + rustbn.js "~0.1.1" + safe-buffer "^5.1.1" + +ethereumjs-vm@^2.0.2, ethereumjs-vm@^2.1.0, ethereumjs-vm@^2.3.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/ethereumjs-vm/-/ethereumjs-vm-2.5.0.tgz#71dde54a093bd813c9defdc6d45ceb8fcca2f603" + integrity sha512-Cp1do4J2FIJFnbofqLsKb/aoZKG+Q8NBIbTa1qwZPQkQxzeR3DZVpFk/VbE1EUO6Ha0kSClJ1jzfj07z3cScSQ== + dependencies: + async "^2.1.2" + async-eventemitter "^0.2.2" + ethereumjs-account "^2.0.3" + ethereumjs-block "~2.1.0" + ethereumjs-common "^0.6.0" + ethereumjs-util "^6.0.0" + fake-merkle-patricia-tree "^1.0.1" + functional-red-black-tree "^1.0.1" + merkle-patricia-tree "^2.1.2" + rustbn.js "~0.2.0" + safe-buffer "^5.1.1" + +ethereumjs-wallet@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/ethereumjs-wallet/-/ethereumjs-wallet-0.6.0.tgz#82763b1697ee7a796be7155da9dfb49b2f98cfdb" + integrity sha1-gnY7Fpfuenlr5xVdqd+0my+Yz9s= + dependencies: + aes-js "^0.2.3" + bs58check "^1.0.8" + ethereumjs-util "^4.4.0" + hdkey "^0.7.0" + scrypt.js "^0.2.0" + utf8 "^2.1.1" + uuid "^2.0.1" + ethers@4.0.0-beta.1: version "4.0.0-beta.1" resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.0-beta.1.tgz#0648268b83e0e91a961b1af971c662cdf8cbab6d" @@ -4297,7 +5174,7 @@ ethers@4.0.0-beta.1: uuid "2.0.1" xmlhttprequest "1.8.0" -ethers@^4.0.17: +ethers@4.0.20, ethers@^4.0.0: version "4.0.20" resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.20.tgz#2b072b283bb19f4870daf42cf5593e5375697504" integrity sha512-1XEejqGYWlcXoVCPFPRfJmf1QQGHW7TNfprtdT1Up66nSuqWE/lYSLgvTNvH/nbSTMRFZqM6ANAbv/1+eUDb7g== @@ -4330,7 +5207,7 @@ ethjs-unit@0.1.6: bn.js "4.11.6" number-to-bn "1.7.0" -ethjs-util@^0.1.3: +ethjs-util@^0.1.3, ethjs-util@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/ethjs-util/-/ethjs-util-0.1.6.tgz#f308b62f185f9fe6237132fb2a9818866a5cd536" integrity sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w== @@ -4353,6 +5230,11 @@ events@^1.0.0: resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= +events@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88" + integrity sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA== + evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" @@ -4551,6 +5433,13 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= +fake-merkle-patricia-tree@^1.0.1, fake-merkle-patricia-tree@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fake-merkle-patricia-tree/-/fake-merkle-patricia-tree-1.0.1.tgz#4b8c3acfb520afadf9860b1f14cd8ce3402cddd3" + integrity sha1-S4w6z7Ugr635hgsfFM2M40As3dM= + dependencies: + checkpoint-store "^1.1.0" + fast-deep-equal@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" @@ -4616,6 +5505,13 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +fetch-ponyfill@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/fetch-ponyfill/-/fetch-ponyfill-4.1.0.tgz#ae3ce5f732c645eab87e4ae8793414709b239893" + integrity sha1-rjzl9zLGReq4fkroeTQUcJsjmJM= + dependencies: + node-fetch "~1.7.1" + figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" @@ -4791,7 +5687,7 @@ follow-redirects@^1.3.0: dependencies: debug "=3.1.0" -for-each@^0.3.2: +for-each@^0.3.2, for-each@~0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== @@ -4964,11 +5860,25 @@ fstream@^1.0.0, fstream@^1.0.2, fstream@^1.0.8: mkdirp ">=0.5 0" rimraf "2" -function-bind@^1.1.1: +function-bind@^1.0.2, function-bind@^1.1.1, function-bind@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +ganache-cli@6.2.4: + version "6.2.4" + resolved "https://registry.yarnpkg.com/ganache-cli/-/ganache-cli-6.2.4.tgz#0ec3d5d993a85c9f252fb632105ab7ffe7668638" + integrity sha512-Yxi+G+Qktr8s1BML0BEC2oIseMP/svWXmojajrdHNG8k84cyihvemuW4DLM7XA/ZOqi88mKt3n/mWTi0RbYpOQ== + dependencies: + bn.js "4.11.8" + source-map-support "0.5.9" + yargs "11.1.0" + ganache-cli@6.2.5: version "6.2.5" resolved "https://registry.yarnpkg.com/ganache-cli/-/ganache-cli-6.2.5.tgz#efda5115fa3a0c62d7f5729fdd78da70ca55b1ad" @@ -4978,6 +5888,47 @@ ganache-cli@6.2.5: source-map-support "0.5.9" yargs "11.1.0" +ganache-core@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ganache-core/-/ganache-core-2.2.1.tgz#c0125d77d3e28d627a812dd002dac21e1d2cc8b7" + integrity sha512-JMLZj7CKxtC5M0qceQYn8FLOx+CZyfISpSRfGdwnKy3juXO8siSAodjq8hviqUY1J0tEdEGqvNm16wSUxtiRjQ== + dependencies: + abstract-leveldown "^3.0.0" + async "^2.5.0" + bip39 "~2.4.0" + bn.js "4.11.6" + cachedown "^1.0.0" + chai "^3.5.0" + clone "^2.1.1" + eth-sig-util "^2.0.2" + ethereumjs-abi "^0.6.5" + ethereumjs-account "~2.0.4" + ethereumjs-block "~1.2.2" + ethereumjs-tx "1.3.4" + ethereumjs-util "^5.2.0" + ethereumjs-vm "2.3.5" + ethereumjs-wallet "0.6.0" + fake-merkle-patricia-tree "~1.0.1" + heap "~0.2.6" + js-scrypt "^0.2.0" + level-sublevel "^6.6.1" + levelup "^1.1.0" + localstorage-down "^0.6.7" + lodash "^4.17.5" + merkle-patricia-tree "^2.2.0" + pify "^3.0.0" + prepend-file "^1.3.1" + request "^2.87.0" + seedrandom "~2.4.2" + shebang-loader "0.0.1" + solc "0.4.24" + temp "^0.8.3" + tmp "0.0.31" + web3 "^1.0.0-beta.34" + web3-provider-engine "^14.0.6" + websocket "^1.0.24" + yargs "^7.0.2" + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -4999,6 +5950,11 @@ gaze@^1.0.0: dependencies: globule "^1.0.0" +generic-pool@~2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-2.0.4.tgz#f9718deda82fa125ed5c43e341c9a215a766d9a3" + integrity sha1-+XGN7agvoSXtXEPjQcmiFadm2aM= + genfun@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" @@ -5171,7 +6127,7 @@ glob@^5.0.15: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@~7.1.1: +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@~7.1.1, glob@~7.1.2: version "7.1.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== @@ -5327,6 +6283,11 @@ has-flag@^3.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= +has-localstorage@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-localstorage/-/has-localstorage-1.0.1.tgz#fe62406c4767fbd6d784dac6905928108b82971b" + integrity sha1-/mJAbEdn+9bXhNrGkFkoEIuClxs= + has-symbol-support-x@^1.4.1: version "1.4.2" resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455" @@ -5380,7 +6341,7 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" -has@^1.0.1: +has@^1.0.1, has@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== @@ -5411,11 +6372,24 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hdkey@^0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/hdkey/-/hdkey-0.7.1.tgz#caee4be81aa77921e909b8d228dd0f29acaee632" + integrity sha1-yu5L6BqneSHpCbjSKN0PKayu5jI= + dependencies: + coinstring "^2.0.0" + secp256k1 "^3.0.1" + he@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= +heap@~0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.6.tgz#087e1f10b046932fc8594dd9e6d378afc9d1e5ac" + integrity sha1-CH4fELBGky/IWU3Z5tN4r8nR5aw= + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -5550,6 +6524,14 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" +humble-localstorage@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/humble-localstorage/-/humble-localstorage-1.4.2.tgz#d05ab0d526c4edbddbf7c6a60df6ff5805283469" + integrity sha1-0Fqw1SbE7b3b98amDfb/WAUoNGk= + dependencies: + has-localstorage "^1.0.1" + localstorage-memory "^1.0.1" + iconv-lite@0.4.23: version "0.4.23" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" @@ -5593,6 +6575,11 @@ ignore@^3.3.5: resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== +immediate@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.2.3.tgz#d140fa8f614659bd6541233097ddaac25cdd991c" + integrity sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw= + import-fresh@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" @@ -5867,6 +6854,11 @@ is-finite@^1.0.0: dependencies: number-is-nan "^1.0.0" +is-fn@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fn/-/is-fn-1.0.0.tgz#9543d5de7bcf5b08a22ec8a20bae6e286d510d8c" + integrity sha1-lUPV3nvPWwiiLsiiC65uKG1RDYw= + is-fullwidth-code-point@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" @@ -6523,6 +7515,13 @@ js-levenshtein@^1.1.3: resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.4.tgz#3a56e3cbf589ca0081eb22cd9ba0b1290a16d26e" integrity sha512-PxfGzSs0ztShKrUYPIn5r0MtyAhYcCwmndozzpz8YObbPnD1jFxzlBGbRnX2mIu6Z13xN6+PTu05TQFnZFlzow== +js-scrypt@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/js-scrypt/-/js-scrypt-0.2.0.tgz#7a62b701b4616e70ad0cde544627aabb99d7fe39" + integrity sha1-emK3AbRhbnCtDN5URiequ5nX/jk= + dependencies: + generic-pool "~2.0.4" + js-sha3@0.5.5: version "0.5.5" resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.5.5.tgz#baf0c0e8c54ad5903447df96ade7a4a1bca79a4a" @@ -6650,6 +7649,30 @@ json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1, json-parse-bet resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== +json-rpc-engine@^3.4.0, json-rpc-engine@^3.6.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-3.8.0.tgz#9d4ff447241792e1d0a232f6ef927302bb0c62a9" + integrity sha512-6QNcvm2gFuuK4TKU1uwfH0Qd/cOSb9c1lls0gbnIhciktIUQJwz6NQNAW4B1KiGPenv7IKu97V222Yo1bNhGuA== + dependencies: + async "^2.0.1" + babel-preset-env "^1.7.0" + babelify "^7.3.0" + json-rpc-error "^2.0.0" + promise-to-callback "^1.0.0" + safe-event-emitter "^1.0.1" + +json-rpc-error@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/json-rpc-error/-/json-rpc-error-2.0.0.tgz#a7af9c202838b5e905c7250e547f1aff77258a02" + integrity sha1-p6+cICg4tekFxyUOVH8a/3cligI= + dependencies: + inherits "^2.0.1" + +json-rpc-random-id@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-rpc-random-id/-/json-rpc-random-id-1.0.1.tgz#ba49d96aded1444dbb8da3d203748acbbcdec8c8" + integrity sha1-uknZat7RRE27jaPSA3SKy7zeyMg= + json-schema-traverse@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" @@ -6665,6 +7688,13 @@ json-schema@0.2.3: resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= +json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8= + dependencies: + jsonify "~0.0.0" + json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -6703,6 +7733,11 @@ jsonfile@^4.0.0: optionalDependencies: graceful-fs "^4.1.6" +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= + jsonparse@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" @@ -6900,6 +7935,90 @@ lerna@^3.4.3: import-local "^1.0.0" libnpm "^2.0.1" +level-codec@~7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/level-codec/-/level-codec-7.0.1.tgz#341f22f907ce0f16763f24bddd681e395a0fb8a7" + integrity sha512-Ua/R9B9r3RasXdRmOtd+t9TCOEIIlts+TN/7XTT2unhDaL6sJn83S3rUyljbr6lVtw49N3/yA0HHjpV6Kzb2aQ== + +level-errors@^1.0.3: + version "1.1.2" + resolved "https://registry.yarnpkg.com/level-errors/-/level-errors-1.1.2.tgz#4399c2f3d3ab87d0625f7e3676e2d807deff404d" + integrity sha512-Sw/IJwWbPKF5Ai4Wz60B52yj0zYeqzObLh8k1Tk88jVmD51cJSKWSYpRyhVIvFzZdvsPqlH5wfhp/yxdsaQH4w== + dependencies: + errno "~0.1.1" + +level-errors@~1.0.3: + version "1.0.5" + resolved "https://registry.yarnpkg.com/level-errors/-/level-errors-1.0.5.tgz#83dbfb12f0b8a2516bdc9a31c4876038e227b859" + integrity sha512-/cLUpQduF6bNrWuAC4pwtUKA5t669pCsCi2XbmojG2tFeOr9j6ShtdDCtFFQO1DRt+EVZhx9gPzP9G2bUaG4ig== + dependencies: + errno "~0.1.1" + +level-iterator-stream@~1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/level-iterator-stream/-/level-iterator-stream-1.3.1.tgz#e43b78b1a8143e6fa97a4f485eb8ea530352f2ed" + integrity sha1-5Dt4sagUPm+pek9IXrjqUwNS8u0= + dependencies: + inherits "^2.0.1" + level-errors "^1.0.3" + readable-stream "^1.0.33" + xtend "^4.0.0" + +level-post@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/level-post/-/level-post-1.0.7.tgz#19ccca9441a7cc527879a0635000f06d5e8f27d0" + integrity sha512-PWYqG4Q00asOrLhX7BejSajByB4EmG2GaKHfj3h5UmmZ2duciXLPGYWIjBzLECFWUGOZWlm5B20h/n3Gs3HKew== + dependencies: + ltgt "^2.1.2" + +level-sublevel@^6.6.1: + version "6.6.5" + resolved "https://registry.yarnpkg.com/level-sublevel/-/level-sublevel-6.6.5.tgz#acddfa2be033b9e503544e2c647f3c03d5a23fbd" + integrity sha512-SBSR60x+dghhwGUxPKS+BvV1xNqnwsEUBKmnFepPaHJ6VkBXyPK9SImGc3K2BkwBfpxlt7GKkBNlCnrdufsejA== + dependencies: + bytewise "~1.1.0" + levelup "~0.19.0" + ltgt "~2.1.1" + pull-defer "^0.2.2" + pull-level "^2.0.3" + pull-stream "^3.6.8" + typewiselite "~1.0.0" + xtend "~4.0.0" + +level-ws@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/level-ws/-/level-ws-0.0.0.tgz#372e512177924a00424b0b43aef2bb42496d228b" + integrity sha1-Ny5RIXeSSgBCSwtDrvK7QkltIos= + dependencies: + readable-stream "~1.0.15" + xtend "~2.1.1" + +levelup@^1.1.0, levelup@^1.2.1: + version "1.3.9" + resolved "https://registry.yarnpkg.com/levelup/-/levelup-1.3.9.tgz#2dbcae845b2bb2b6bea84df334c475533bbd82ab" + integrity sha512-VVGHfKIlmw8w1XqpGOAGwq6sZm2WwWLmlDcULkKWQXEA5EopA8OBNJ2Ck2v6bdk8HeEZSbCSEgzXadyQFm76sQ== + dependencies: + deferred-leveldown "~1.2.1" + level-codec "~7.0.0" + level-errors "~1.0.3" + level-iterator-stream "~1.3.0" + prr "~1.0.1" + semver "~5.4.1" + xtend "~4.0.0" + +levelup@~0.19.0: + version "0.19.1" + resolved "https://registry.yarnpkg.com/levelup/-/levelup-0.19.1.tgz#f3a6a7205272c4b5f35e412ff004a03a0aedf50b" + integrity sha1-86anIFJyxLXzXkEv8ASgOgrt9Qs= + dependencies: + bl "~0.8.1" + deferred-leveldown "~0.2.0" + errno "~0.1.1" + prr "~0.0.0" + readable-stream "~1.0.26" + semver "~5.1.0" + xtend "~3.0.0" + leven@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" @@ -7044,14 +8163,32 @@ loader-runner@^2.3.0: integrity sha512-By6ZFY7ETWOc9RFaAIb23IjJVcM4dvJC/N57nmdz9RSkMXvAXGI7SyVlAw3v8vjtDRlqThgVDVmTnr9fqMlxkw== loader-utils@^1.0.2, loader-utils@^1.1.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.2.tgz#fcfcb6b4109b2358c3df160b2254f0496b261a36" - integrity sha512-Xjb++b55GPoVV1ct93EodsswWEErveAqGhhwujIshShtjIKdPpdpveriwNCNsuVo1zQ1ukmPUszK44C9RD7TCg== + version "1.2.3" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" + integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== dependencies: big.js "^5.2.2" emojis-list "^2.0.0" json5 "^1.0.1" +localstorage-down@^0.6.7: + version "0.6.7" + resolved "https://registry.yarnpkg.com/localstorage-down/-/localstorage-down-0.6.7.tgz#d0799a93b31e6c5fa5188ec06242eb1cce9d6d15" + integrity sha1-0Hmak7MebF+lGI7AYkLrHM6dbRU= + dependencies: + abstract-leveldown "0.12.3" + argsarray "0.0.1" + buffer-from "^0.1.1" + d64 "^1.0.0" + humble-localstorage "^1.4.2" + inherits "^2.0.1" + tiny-queue "0.2.0" + +localstorage-memory@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/localstorage-memory/-/localstorage-memory-1.0.3.tgz#566b37968fe0c4d76ba36a6da564fa613945ca72" + integrity sha512-t9P8WB6DcVttbw/W4PIE8HOqum8Qlvx5SjR6oInwR9Uia0EEmyUeBh7S+weKByW+l/f45Bj4L/dgZikGFDM6ng== + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -7141,6 +8278,16 @@ long@~3: resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" integrity sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s= +looper@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/looper/-/looper-2.0.0.tgz#66cd0c774af3d4fedac53794f742db56da8f09ec" + integrity sha1-Zs0Md0rz1P7axTeU90LbVtqPCew= + +looper@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/looper/-/looper-3.0.0.tgz#2efa54c3b1cbaba9b94aee2e5914b0be57fbb749" + integrity sha1-LvpUw7HLq6m5Su4uWRSwvlf7t0k= + loose-envify@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -7161,6 +8308,13 @@ lowercase-keys@^1.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== +lru-cache@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-3.2.0.tgz#71789b3b7f5399bec8565dda38aa30d2a097efee" + integrity sha1-cXibO39Tmb7IVl3aOKow0qCX7+4= + dependencies: + pseudomap "^1.0.1" + lru-cache@^4.0.1, lru-cache@^4.1.2, lru-cache@^4.1.3: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -7176,6 +8330,16 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +ltgt@^2.1.2, ltgt@~2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5" + integrity sha1-81ypHEk/e3PaDgdJUwTxezH4fuU= + +ltgt@~2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.1.3.tgz#10851a06d9964b971178441c23c9e52698eece34" + integrity sha1-EIUaBtmWS5cReEQcI8nlJpjuzjQ= + magic-string@^0.25.1: version "0.25.1" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.1.tgz#b1c248b399cd7485da0fe7385c2fc7011843266e" @@ -7283,6 +8447,18 @@ mem@^4.0.0: mimic-fn "^1.0.0" p-is-promise "^1.1.0" +memdown@^1.0.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/memdown/-/memdown-1.4.1.tgz#b4e4e192174664ffbae41361aa500f3119efe215" + integrity sha1-tOThkhdGZP+65BNhqlAPMRnv4hU= + dependencies: + abstract-leveldown "~2.7.1" + functional-red-black-tree "^1.0.1" + immediate "^3.2.3" + inherits "~2.0.1" + ltgt "~2.2.0" + safe-buffer "~5.1.1" + memory-fs@^0.4.0, memory-fs@~0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" @@ -7349,6 +8525,20 @@ merge@^1.2.0: resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== +merkle-patricia-tree@^2.1.2, merkle-patricia-tree@^2.2.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/merkle-patricia-tree/-/merkle-patricia-tree-2.3.2.tgz#982ca1b5a0fde00eed2f6aeed1f9152860b8208a" + integrity sha512-81PW5m8oz/pz3GvsAwbauj7Y00rqm81Tzad77tHBwU7pIAtN+TJnMSOJhxBKflSVYhptMMb9RskhqHqrSm1V+g== + dependencies: + async "^1.4.2" + ethereumjs-util "^5.0.0" + level-ws "0.0.0" + levelup "^1.2.1" + memdown "^1.0.0" + readable-stream "^2.0.0" + rlp "^2.0.0" + semaphore ">=1.0.1" + methods@^1.0.1, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -7469,7 +8659,7 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: +minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= @@ -7698,7 +8888,12 @@ node-fetch-npm@^2.0.2: json-parse-better-errors "^1.0.0" safe-buffer "^5.1.1" -node-fetch@^1.0.1: +node-fetch@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5" + integrity sha1-q4hOjn5X44qUR1POxwb3iNF2i7U= + +node-fetch@^1.0.1, node-fetch@~1.7.1: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== @@ -7989,7 +9184,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4, object-assign@^4.0.0, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -8003,11 +9198,21 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-inspect@~1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b" + integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ== + object-keys@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2" integrity sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag== +object-keys@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" + integrity sha1-KKaq50KN0sOpLz2V8hM13SBOAzY= + object-visit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" @@ -8151,7 +9356,7 @@ os-locale@^3.0.0: lcid "^2.0.0" mem "^4.0.0" -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= @@ -8449,7 +9654,7 @@ pathval@^1.1.0: resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA= -pbkdf2@^3.0.3: +pbkdf2@^3.0.3, pbkdf2@^3.0.9: version "3.0.17" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" integrity sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA== @@ -8521,11 +9726,23 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +precond@0.2: + version "0.2.3" + resolved "https://registry.yarnpkg.com/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac" + integrity sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw= + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +prepend-file@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/prepend-file/-/prepend-file-1.3.1.tgz#83b16e0b4ac1901fce88dbd945a22f4cc81df579" + integrity sha1-g7FuC0rBkB/OiNvZRaIvTMgd9Xk= + dependencies: + tmp "0.0.31" + prepend-http@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" @@ -8597,6 +9814,14 @@ promise-retry@^1.1.1: err-code "^1.0.0" retry "^0.10.0" +promise-to-callback@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/promise-to-callback/-/promise-to-callback-1.0.0.tgz#5d2a749010bfb67d963598fcd3960746a68feef7" + integrity sha1-XSp0kBC/tn2WNZj805YHRqaP7vc= + dependencies: + is-fn "^1.0.0" + set-immediate-shim "^1.0.1" + promise@^8.0.0: version "8.0.2" resolved "https://registry.yarnpkg.com/promise/-/promise-8.0.2.tgz#9dcd0672192c589477d56891271bdc27547ae9f0" @@ -8654,12 +9879,17 @@ proxy-from-env@^1.0.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4= +prr@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" + integrity sha1-GoS4WQgyVQFBGFPQCB7j+obikmo= + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= -pseudomap@^1.0.2: +pseudomap@^1.0.1, pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= @@ -8681,6 +9911,54 @@ public-encrypt@^4.0.0: randombytes "^2.0.1" safe-buffer "^5.1.2" +pull-cat@^1.1.9: + version "1.1.11" + resolved "https://registry.yarnpkg.com/pull-cat/-/pull-cat-1.1.11.tgz#b642dd1255da376a706b6db4fa962f5fdb74c31b" + integrity sha1-tkLdElXaN2pwa220+pYvX9t0wxs= + +pull-defer@^0.2.2: + version "0.2.3" + resolved "https://registry.yarnpkg.com/pull-defer/-/pull-defer-0.2.3.tgz#4ee09c6d9e227bede9938db80391c3dac489d113" + integrity sha512-/An3KE7mVjZCqNhZsr22k1Tx8MACnUnHZZNPSJ0S62td8JtYr/AiRG42Vz7Syu31SoTLUzVIe61jtT/pNdjVYA== + +pull-level@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pull-level/-/pull-level-2.0.4.tgz#4822e61757c10bdcc7cf4a03af04c92734c9afac" + integrity sha512-fW6pljDeUThpq5KXwKbRG3X7Ogk3vc75d5OQU/TvXXui65ykm+Bn+fiktg+MOx2jJ85cd+sheufPL+rw9QSVZg== + dependencies: + level-post "^1.0.7" + pull-cat "^1.1.9" + pull-live "^1.0.1" + pull-pushable "^2.0.0" + pull-stream "^3.4.0" + pull-window "^2.1.4" + stream-to-pull-stream "^1.7.1" + +pull-live@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pull-live/-/pull-live-1.0.1.tgz#a4ecee01e330155e9124bbbcf4761f21b38f51f5" + integrity sha1-pOzuAeMwFV6RJLu89HYfIbOPUfU= + dependencies: + pull-cat "^1.1.9" + pull-stream "^3.4.0" + +pull-pushable@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pull-pushable/-/pull-pushable-2.2.0.tgz#5f2f3aed47ad86919f01b12a2e99d6f1bd776581" + integrity sha1-Xy867UethpGfAbEqLpnW8b13ZYE= + +pull-stream@^3.2.3, pull-stream@^3.4.0, pull-stream@^3.6.8: + version "3.6.9" + resolved "https://registry.yarnpkg.com/pull-stream/-/pull-stream-3.6.9.tgz#c774724cd63bc0984c3695f74c819aa02e977320" + integrity sha512-hJn4POeBrkttshdNl0AoSCVjMVSuBwuHocMerUdoZ2+oIUzrWHFTwJMlbHND7OiKLVgvz6TFj8ZUVywUMXccbw== + +pull-window@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/pull-window/-/pull-window-2.1.4.tgz#fc3b86feebd1920c7ae297691e23f705f88552f0" + integrity sha1-/DuG/uvRkgx64pdpHiP3BfiFUvA= + dependencies: + looper "^2.0.0" + pump@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" @@ -8904,7 +10182,7 @@ read@1, read@~1.0.1: dependencies: mute-stream "~0.0.4" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== @@ -8917,7 +10195,17 @@ read@1, read@~1.0.1: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@~1.0.31: +readable-stream@^1.0.33: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@~1.0.15, readable-stream@~1.0.26, readable-stream@~1.0.31: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= @@ -8988,7 +10276,7 @@ regenerate-unicode-properties@^7.0.0: dependencies: regenerate "^1.4.0" -regenerate@^1.4.0: +regenerate@^1.2.1, regenerate@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg== @@ -8998,6 +10286,15 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== +regenerator-transform@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" + integrity sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q== + dependencies: + babel-runtime "^6.18.0" + babel-types "^6.19.0" + private "^0.1.6" + regenerator-transform@^0.13.3: version "0.13.3" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.3.tgz#264bd9ff38a8ce24b06e0636496b2c856b57bcbb" @@ -9020,6 +10317,15 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" +regexpu-core@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" + integrity sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA= + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + regexpu-core@^4.1.3, regexpu-core@^4.2.0: version "4.4.0" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.4.0.tgz#8d43e0d1266883969720345e70c275ee0aec0d32" @@ -9032,11 +10338,23 @@ regexpu-core@^4.1.3, regexpu-core@^4.2.0: unicode-match-property-ecmascript "^1.0.4" unicode-match-property-value-ecmascript "^1.0.2" +regjsgen@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" + integrity sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc= + regjsgen@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.0.tgz#a7634dc08f89209c2049adda3525711fb97265dd" integrity sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA== +regjsparser@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" + integrity sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw= + dependencies: + jsesc "~0.5.0" + regjsparser@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.0.tgz#f1e6ae8b7da2bae96c99399b868cd6c933a2ba9c" @@ -9141,7 +10459,7 @@ request@2.87.0: tunnel-agent "^0.6.0" uuid "^3.1.0" -request@^2.79.0, request@^2.83.0, request@^2.87.0: +request@^2.67.0, request@^2.79.0, request@^2.83.0, request@^2.85.0, request@^2.87.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== @@ -9228,6 +10546,13 @@ resolve@1.x, resolve@^1.1.6, resolve@^1.3.2, resolve@^1.8.1: dependencies: path-parse "^1.0.6" +resolve@~1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3" + integrity sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw== + dependencies: + path-parse "^1.0.5" + restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" @@ -9236,6 +10561,13 @@ restore-cursor@^2.0.0: onetime "^2.0.0" signal-exit "^3.0.2" +resumer@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/resumer/-/resumer-0.0.0.tgz#f1e8f461e4064ba39e82af3cdc2a8c893d076759" + integrity sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k= + dependencies: + through "~2.3.4" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -9253,6 +10585,11 @@ rimraf@2, rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2. dependencies: glob "^7.0.5" +rimraf@~2.2.6: + version "2.2.8" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" + integrity sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI= + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -9339,6 +10676,16 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" +rustbn.js@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/rustbn.js/-/rustbn.js-0.1.2.tgz#979fa0f9562216dd667c9d2cd179ae5d13830eff" + integrity sha512-bAkNqSHYdJdFsBC7Z11JgzYktL31HIpB2o70jZcGiL1U1TVtPyvaVhDrGWwS8uZtaqwW2k6NOPGZCqW/Dgh5Lg== + +rustbn.js@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/rustbn.js/-/rustbn.js-0.2.0.tgz#8082cb886e707155fd1cb6f23bd591ab8d55d0ca" + integrity sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA== + rxjs@^6.1.0: version "6.3.3" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.3.tgz#3c6a7fa420e844a81390fb1158a9ec614f4bad55" @@ -9351,6 +10698,13 @@ safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, s resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-event-emitter@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/safe-event-emitter/-/safe-event-emitter-1.0.1.tgz#5b692ef22329ed8f69fdce607e50ca734f6f20af" + integrity sha512-e1wFe99A91XYYxoQbcq2ZJUWurxEyP8vfz7A7vuUe1s95q8r5ebraVaA1BukYJcpM6V16ugWoD9vngi8Ccu5fg== + dependencies: + events "^3.0.0" + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" @@ -9429,6 +10783,14 @@ scrypt.js@0.2.0: scrypt "^6.0.2" scryptsy "^1.2.1" +scrypt.js@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/scrypt.js/-/scrypt.js-0.2.1.tgz#cc3f751933d6bac7a4bedf5301d7596e8146cdcd" + integrity sha512-XMoqxwABdotuW+l+qACmJ/h0kVSCgMPZXpbncA/zyBO90z/NnDISzVw+xJ4tUY+X/Hh0EFT269OYHm26VCPgmA== + dependencies: + scrypt "^6.0.2" + scryptsy "^1.2.1" + scrypt@^6.0.2: version "6.0.3" resolved "https://registry.yarnpkg.com/scrypt/-/scrypt-6.0.3.tgz#04e014a5682b53fa50c2d5cce167d719c06d870d" @@ -9465,6 +10827,11 @@ secp256k1@^3.0.1: nan "^2.2.1" safe-buffer "^5.1.0" +seedrandom@~2.4.2: + version "2.4.4" + resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-2.4.4.tgz#b25ea98632c73e45f58b77cfaa931678df01f9ba" + integrity sha512-9A+PDmgm+2du77B5i0Ip2cxOqqHjgNxnBgglxLcX78A2D6c2rTo61z4jnVABpF4cKeDMDG+cmXXvdnqse2VqMA== + seek-bzip@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.5.tgz#cfe917cb3d274bcffac792758af53173eb1fabdc" @@ -9472,16 +10839,31 @@ seek-bzip@^1.0.5: dependencies: commander "~2.8.1" +semaphore@>=1.0.1, semaphore@^1.0.3: + version "1.1.0" + resolved "https://registry.yarnpkg.com/semaphore/-/semaphore-1.1.0.tgz#aaad8b86b20fe8e9b32b16dc2ee682a8cd26a8aa" + integrity sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA== + "semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== +semver@~5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.1.1.tgz#a3292a373e6f3e0798da0b20641b9a9c5bc47e19" + integrity sha1-oykqNz5vPgeY2gsgZBuanFvEfhk= + semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= +semver@~5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" + integrity sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg== + send@0.16.2: version "0.16.2" resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" @@ -9537,6 +10919,11 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-immediate-shim@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E= + set-value@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" @@ -9602,6 +10989,11 @@ shebang-command@^1.2.0: dependencies: shebang-regex "^1.0.0" +shebang-loader@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/shebang-loader/-/shebang-loader-0.0.1.tgz#a4000495d44cceefbec63435e7b1698569fa52ec" + integrity sha1-pAAEldRMzu++xjQ157FphWn6Uuw= + shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" @@ -9739,7 +11131,18 @@ sol-explore@^1.6.2: resolved "https://registry.yarnpkg.com/sol-explore/-/sol-explore-1.6.2.tgz#43ae8c419fd3ac056a05f8a9d1fb1022cd41ecc2" integrity sha1-Q66MQZ/TrAVqBfip0fsQIs1B7MI= -solc@0.4.25, solc@^0.4.25: +solc@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/solc/-/solc-0.4.24.tgz#354f14b269b38cbaa82a47d1ff151723502b954e" + integrity sha512-2xd7Cf1HeVwrIb6Bu1cwY2/TaLRodrppCq3l7rhLimFQgmxptXhTC3+/wesVLpB09F1A2kZgvbMOgH7wvhFnBQ== + dependencies: + fs-extra "^0.30.0" + memorystream "^0.3.1" + require-from-string "^1.1.0" + semver "^5.3.0" + yargs "^4.7.1" + +solc@0.4.25, solc@^0.4.2, solc@^0.4.24, solc@^0.4.25: version "0.4.25" resolved "https://registry.yarnpkg.com/solc/-/solc-0.4.25.tgz#06b8321f7112d95b4b903639b1138a4d292f5faa" integrity sha512-jU1YygRVy6zatgXrLY2rRm7HW1d7a8CkkEgNJwvH2VLpWhMFsMdWcJn6kUqZwcSz/Vm+w89dy7Z/aB5p6AFTrg== @@ -10030,6 +11433,14 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= +stream-to-pull-stream@^1.7.1: + version "1.7.2" + resolved "https://registry.yarnpkg.com/stream-to-pull-stream/-/stream-to-pull-stream-1.7.2.tgz#757609ae1cebd33c7432d4afbe31ff78650b9dde" + integrity sha1-dXYJrhzr0zx0MtSvvjH/eGULnd4= + dependencies: + looper "^3.0.0" + pull-stream "^3.2.3" + strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" @@ -10060,6 +11471,15 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string.prototype.trim@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea" + integrity sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo= + dependencies: + define-properties "^1.1.2" + es-abstract "^1.5.0" + function-bind "^1.0.2" + string_decoder@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" @@ -10233,6 +11653,25 @@ tapable@^1.0.0, tapable@^1.1.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.1.tgz#4d297923c5a72a42360de2ab52dadfaaec00018e" integrity sha512-9I2ydhj8Z9veORCw5PRm4u9uebCn0mcCa6scWoNcbZ6dAtoo2618u9UUzxgmsCOreJpqDDuv61LvwofW7hLcBA== +tape@^4.4.0, tape@^4.6.3, tape@^4.8.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/tape/-/tape-4.9.1.tgz#1173d7337e040c76fbf42ec86fcabedc9b3805c9" + integrity sha512-6fKIXknLpoe/Jp4rzHKFPpJUHDHDqn8jus99IfPnHIjyz78HYlefTGD3b5EkbQzuLfaEvmfPK3IolLgq2xT3kw== + dependencies: + deep-equal "~1.0.1" + defined "~1.0.0" + for-each "~0.3.3" + function-bind "~1.1.1" + glob "~7.1.2" + has "~1.0.3" + inherits "~2.0.3" + minimist "~1.2.0" + object-inspect "~1.6.0" + resolve "~1.7.1" + resumer "~0.0.0" + string.prototype.trim "~1.1.2" + through "~2.3.8" + tar-stream@^1.5.2: version "1.6.2" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" @@ -10314,10 +11753,18 @@ temp-write@^3.4.0: temp-dir "^1.0.0" uuid "^3.0.1" +temp@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59" + integrity sha1-4Ma8TSa5AxJEEOT+2BEDAU38H1k= + dependencies: + os-tmpdir "^1.0.0" + rimraf "~2.2.6" + terser-webpack-plugin@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.0.tgz#24697213f2128009080f39f081c18cb2221d91ff" - integrity sha512-QW7RACLS89RalHtLDb0s8+Iqcs/IAEw1rnVrV+mS7Gx1kgPG8o1g33JhAGDgc/CQ84hLsTW5WrAMdVysh692yg== + version "1.2.1" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.1.tgz#7545da9ae5f4f9ae6a0ac961eb46f5e7c845cc26" + integrity sha512-GGSt+gbT0oKcMDmPx4SRSfJPE1XaN3kQRWG4ghxKQw9cn5G9x6aCKSsgYdvyM0na9NJ4Drv0RG6jbBByZ5CMjw== dependencies: cacache "^11.0.2" find-cache-dir "^2.0.0" @@ -10394,7 +11841,7 @@ throat@^4.0.0: resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo= -through2@^2.0.0, through2@^2.0.1, through2@^2.0.2: +through2@^2.0.0, through2@^2.0.1, through2@^2.0.2, through2@^2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== @@ -10402,7 +11849,7 @@ through2@^2.0.0, through2@^2.0.1, through2@^2.0.2: readable-stream "~2.3.6" xtend "~4.0.1" -through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6: +through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@~2.3.4, through@~2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -10419,6 +11866,18 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" +tiny-queue@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/tiny-queue/-/tiny-queue-0.2.0.tgz#c49fcb5c87555be1b4a5df7eb87101d5b78bc9dc" + integrity sha1-xJ/LXIdVW+G0pd9+uHEB1beLydw= + +tmp@0.0.31: + version "0.0.31" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" + integrity sha1-jzirlDjhcxXl29izZX6L+yd65Kc= + dependencies: + os-tmpdir "~1.0.1" + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -10528,6 +11987,11 @@ tree-kill@^1.2.0: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.1.tgz#5398f374e2f292b9dcc7b2e71e30a5c3bb6c743a" integrity sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q== +treeify@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8" + integrity sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A== + trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" @@ -10681,7 +12145,7 @@ truffle-flattener@^1.2.8: truffle-resolver "^4.0.4" tsort "0.0.1" -truffle-hdwallet-provider@1.0.1: +truffle-hdwallet-provider@1.0.1, truffle-hdwallet-provider@^1.0.0-web3one.5: version "1.0.1" resolved "https://registry.yarnpkg.com/truffle-hdwallet-provider/-/truffle-hdwallet-provider-1.0.1.tgz#940f89a59642b9d280d9dd58b4a966cc88b2252e" integrity sha512-SC/eGTBlCCLr+1rEL4sRXJwcPQP0kQKdb+0tWU1HFqP+l36CIIxqyCDN+z2HR3qjm5h6wcpYq0Ii/SCN4hR25Q== @@ -10745,6 +12209,29 @@ ts-jest@23.10.5: semver "^5.5" yargs-parser "10.x" +ts-mocha@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ts-mocha/-/ts-mocha-2.0.0.tgz#0dbd3cd04671df9933b9303b4aa46347573c5635" + integrity sha512-Rj6+vvwKtOTs5GsNO1jLl4DIXUGnyAg5HFt2Yb4SHIRN45clTJkHWpNdTxCSL0u+1oeavSYJah6d1PZ++Ju5pw== + dependencies: + ts-node "7.0.0" + optionalDependencies: + tsconfig-paths "^3.5.0" + +ts-node@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-7.0.0.tgz#a94a13c75e5e1aa6b82814b84c68deb339ba7bff" + integrity sha512-klJsfswHP0FuOLsvBZ/zzCfUvakOSSxds78mVeK7I+qP76YWtxf16hEZsp3U+b0kIo82R5UatGFeblYMqabb2Q== + dependencies: + arrify "^1.0.0" + buffer-from "^1.1.0" + diff "^3.1.0" + make-error "^1.1.1" + minimist "^1.2.0" + mkdirp "^0.5.1" + source-map-support "^0.5.6" + yn "^2.0.0" + ts-node@^7.0.0, ts-node@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-7.0.1.tgz#9562dc2d1e6d248d24bc55f773e3f614337d9baf" @@ -10759,6 +12246,17 @@ ts-node@^7.0.0, ts-node@^7.0.1: source-map-support "^0.5.6" yn "^2.0.0" +tsconfig-paths@^3.5.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.7.0.tgz#02ae978db447b22e09dafcd4198be95c4885ceb2" + integrity sha512-7iE+Q/2E1lgvxD+c0Ot+GFFmgmfIjt/zCayyruXkXQ84BLT85gHXy0WSoQSiuFX9+d+keE/jiON7notV74ZY+A== + dependencies: + "@types/json5" "^0.0.29" + deepmerge "^2.0.1" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" + tslib@1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8" @@ -10891,11 +12389,21 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tweetnacl-util@^0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.0.tgz#4576c1cee5e2d63d207fee52f1ba02819480bc75" + integrity sha1-RXbBzuXi1j0gf+5S8boCgZSAvHU= + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= +tweetnacl@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.0.tgz#713d8b818da42068740bf68386d0479e66fc8a7b" + integrity sha1-cT2LgY2kIGh0C/aDhtBHnmb8ins= + type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -10903,6 +12411,16 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" +type-detect@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-0.1.1.tgz#0ba5ec2a885640e470ea4e8505971900dac58822" + integrity sha1-C6XsKohWQORw6k6FBZcZANrFiCI= + +type-detect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2" + integrity sha1-diIXzAbbJY7EiQihKY6LlRIejqI= + type-detect@^4.0.0, type-detect@^4.0.5: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -10928,6 +12446,13 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript-memoize@^1.0.0-alpha.3: + version "1.0.0-alpha.3" + resolved "https://registry.yarnpkg.com/typescript-memoize/-/typescript-memoize-1.0.0-alpha.3.tgz#699a5415f886694a8d6e2e5451bc28a39a6bc2f9" + integrity sha1-aZpUFfiGaUqNbi5UUbwoo5prwvk= + dependencies: + core-js "2.4.1" + typescript@^3.1.2, typescript@^3.1.3: version "3.2.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.2.2.tgz#fe8101c46aa123f8353523ebdcf5730c2ae493e5" @@ -10943,6 +12468,23 @@ typescript@~3.1.6: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.6.tgz#b6543a83cfc8c2befb3f4c8fba6896f5b0c9be68" integrity sha512-tDMYfVtvpb96msS1lDX9MEdHrW4yOuZ4Kdc4Him9oU796XldPYF/t2+uKoX0BBa0hXXwDlqYQbXY5Rzjzc5hBA== +typewise-core@^1.2, typewise-core@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/typewise-core/-/typewise-core-1.2.0.tgz#97eb91805c7f55d2f941748fa50d315d991ef195" + integrity sha1-l+uRgFx/VdL5QXSPpQ0xXZke8ZU= + +typewise@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typewise/-/typewise-1.0.3.tgz#1067936540af97937cc5dcf9922486e9fa284651" + integrity sha1-EGeTZUCvl5N8xdz5kiSG6fooRlE= + dependencies: + typewise-core "^1.2.0" + +typewiselite@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typewiselite/-/typewiselite-1.0.0.tgz#c8882fa1bb1092c06005a97f34ef5c8508e3664e" + integrity sha1-yIgvobsQksBgBal/NO9chQjjZk4= + uglify-es@^3.3.9: version "3.3.9" resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" @@ -11046,6 +12588,11 @@ universalify@^0.1.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +unorm@^1.3.3: + version "1.4.1" + resolved "https://registry.yarnpkg.com/unorm/-/unorm-1.4.1.tgz#364200d5f13646ca8bcd44490271335614792300" + integrity sha1-NkIA1fE2RsqLzURJAnEzVhR5IwA= + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -11158,6 +12705,11 @@ uuid@2.0.1: resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.1.tgz#c2a30dedb3e535d72ccf82e343941a50ba8533ac" integrity sha1-wqMN7bPlNdcsz4LjQ5QaULqFM6w= +uuid@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" + integrity sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho= + uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" @@ -11404,6 +12956,57 @@ web3-net@1.0.0-beta.37: web3-core-method "1.0.0-beta.37" web3-utils "1.0.0-beta.37" +web3-provider-engine@^13.3.2: + version "13.8.0" + resolved "https://registry.yarnpkg.com/web3-provider-engine/-/web3-provider-engine-13.8.0.tgz#4c7c1ad2af5f1fe10343b8a65495879a2f9c00df" + integrity sha512-fZXhX5VWwWpoFfrfocslyg6P7cN3YWPG/ASaevNfeO80R+nzgoPUBXcWQekSGSsNDkeRTis4aMmpmofYf1TNtQ== + dependencies: + async "^2.5.0" + clone "^2.0.0" + eth-block-tracker "^2.2.2" + eth-sig-util "^1.4.2" + ethereumjs-block "^1.2.2" + ethereumjs-tx "^1.2.0" + ethereumjs-util "^5.1.1" + ethereumjs-vm "^2.0.2" + fetch-ponyfill "^4.0.0" + json-rpc-error "^2.0.0" + json-stable-stringify "^1.0.1" + promise-to-callback "^1.0.0" + readable-stream "^2.2.9" + request "^2.67.0" + semaphore "^1.0.3" + solc "^0.4.2" + tape "^4.4.0" + xhr "^2.2.0" + xtend "^4.0.1" + +web3-provider-engine@^14.0.6: + version "14.1.0" + resolved "https://registry.yarnpkg.com/web3-provider-engine/-/web3-provider-engine-14.1.0.tgz#91590020f8b8c1b65846321310cbfdb039090fc6" + integrity sha512-vGZtqhSUzGTiMGhJXNnB/aRDlrPZLhLnBZ2NPArkZtr8XSrwg9m08tw4+PuWg5za0TJuoE/vuPQc501HddZZWw== + dependencies: + async "^2.5.0" + backoff "^2.5.0" + clone "^2.0.0" + cross-fetch "^2.1.0" + eth-block-tracker "^3.0.0" + eth-json-rpc-infura "^3.1.0" + eth-sig-util "^1.4.2" + ethereumjs-block "^1.2.2" + ethereumjs-tx "^1.2.0" + ethereumjs-util "^5.1.5" + ethereumjs-vm "^2.3.4" + json-rpc-error "^2.0.0" + json-stable-stringify "^1.0.1" + promise-to-callback "^1.0.0" + readable-stream "^2.2.9" + request "^2.85.0" + semaphore "^1.0.3" + ws "^5.1.1" + xhr "^2.2.0" + xtend "^4.0.1" + web3-providers-http@1.0.0-beta.37: version "1.0.0-beta.37" resolved "https://registry.yarnpkg.com/web3-providers-http/-/web3-providers-http-1.0.0-beta.37.tgz#c06efd60e16e329e25bd268d2eefc68d82d13651" @@ -11475,7 +13078,7 @@ web3@^0.18.4: xhr2 "*" xmlhttprequest "*" -web3@^1.0.0-beta.37: +web3@^1.0.0-beta.34, web3@^1.0.0-beta.37: version "1.0.0-beta.37" resolved "https://registry.yarnpkg.com/web3/-/web3-1.0.0-beta.37.tgz#b42c30e67195f816cd19d048fda872f70eca7083" integrity sha512-8XLgUspdzicC/xHG82TLrcF/Fxzj2XYNJ1KTYnepOI77bj5rvpsxxwHYBWQ6/JOjk0HkZqoBfnXWgcIHCDhZhQ== @@ -11551,7 +13154,7 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== -websocket@^1.0.28: +websocket@^1.0.24, websocket@^1.0.28: version "1.0.28" resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.28.tgz#9e5f6fdc8a3fe01d4422647ef93abdd8d45a78d3" integrity sha512-00y/20/80P7H4bCYkzuuvvfDvh+dgtXi5kzDf3UcZwN6boTYaKvsrtZ5lIYm1Gsg48siMErd9M4zjSYfYFHTrA== @@ -11880,7 +13483,7 @@ xhr2@*: resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.1.4.tgz#7f87658847716db5026323812f818cadab387a5f" integrity sha1-f4dliEdxbbUCYyOBL4GMras4el8= -xhr@^2.0.4, xhr@^2.3.3: +xhr@^2.0.4, xhr@^2.2.0, xhr@^2.3.3: version "2.5.0" resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.5.0.tgz#bed8d1676d5ca36108667692b74b316c496e49dd" integrity sha512-4nlO/14t3BNUZRXIXfXe+3N6w3s1KoxcJUUURctd64BLRe67E4gRwp4PjywtDY72fXpZ1y6Ch0VZQRY/gMPzzQ== @@ -11900,11 +13503,23 @@ xmlhttprequest@*, xmlhttprequest@1.8.0: resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" integrity sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw= -xtend@^4.0.0, xtend@~4.0.1: +xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= +xtend@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b" + integrity sha1-bv7MKk2tjmlixJAbM3znuoe10os= + dependencies: + object-keys "~0.4.0" + +xtend@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-3.0.0.tgz#5cce7407baf642cba7becda568111c493f59665a" + integrity sha1-XM50B7r2Qsunvs2laBEcST9ZZlo= + y18n@^3.2.0, y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" @@ -12061,7 +13676,7 @@ yargs@^4.6.0, yargs@^4.7.1: y18n "^3.2.1" yargs-parser "^2.4.1" -yargs@^7.0.0: +yargs@^7.0.0, yargs@^7.0.2: version "7.1.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" integrity sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=