From 801a505b69d8f38279846f1ae6faaad999cf3c14 Mon Sep 17 00:00:00 2001 From: Mitchell Van Der Hoeff <8631205+mvanderh@users.noreply.github.com> Date: Mon, 26 Nov 2018 15:48:02 -0500 Subject: [PATCH 01/11] [CFjs] Add message format section to Node Spec (#270) - Explain format - Add requestId field --- packages/cf.js/API_REFERENCE.md | 44 ++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/packages/cf.js/API_REFERENCE.md b/packages/cf.js/API_REFERENCE.md index 516cea182..454ebd0d9 100755 --- a/packages/cf.js/API_REFERENCE.md +++ b/packages/cf.js/API_REFERENCE.md @@ -17,22 +17,22 @@ - `on(eventType, callback: Function)` - `install` - [Node event](#event-install) - - Params: `(appInstance: AppInstance)` + - Callback Params: `(appInstance: AppInstance)` - `rejectInstall` - [Node event](#event-rejectinstall) - - Params: `(appInstance: AppInstance)` + - Callback Params: `(appInstance: AppInstance)` - `updateState` - [Node event](#event-rejectinstall) - - Params: `(appInstance: AppInstance, oldState: AppState, newState: AppState)` + - Callback Params: `(appInstance: AppInstance, oldState: AppState, newState: AppState)` - `proposeState` - [Node event](#event-proposestate) - - Params: `(appInstance: AppInstance, oldState: AppState, newState: AppState)` + - Callback Params: `(appInstance: AppInstance, oldState: AppState, newState: AppState)` - `rejectState` - [Node event](#event-rejectstate) - - Params: `(appInstance: AppInstance, state: AppState)` + - Callback Params: `(appInstance: AppInstance, state: AppState)` - `uninstall` - [Node event](#event-uninstall) - - Params: `(appInstance: AppInstance, finalState: AppState, myPayout: BigNumber, peerPayout: BigNumber)` + - Callback Params: `(appInstance: AppInstance, finalState: AppState, myPayout: BigNumber, peerPayout: BigNumber)` - `AppFactory` - Properties - `provider: Provider` @@ -100,18 +100,34 @@ Node Protocol ============= +Message Format +-------------- + +Messages in the Node Protocol have the following fields: + +- `type: string` + - Name of the Method or Event that this message represents e.g. "getAppInstances", "install" +- `requestId?: string` + - Unique ID for a Method request. + - Only required for Methods. Leave empty for Events. +- `data: { [key: string]: any }` + - Data payload for this message. + - See "Result" section of a Method and "Data" section of an Event for details. + Public Methods -------------- ### Method: `getAppInstances` -Returns **all** app instances currently installed on the Node. +Returns all app instances currently installed on the Node. -**NOTE**: This is terrible from a security perspective. In the future this method will be changed or deprecated to fix the security flaw. +NOTE: This is terrible from a security perspective. In the future this method will be changed or deprecated to fix the security flaw. -Params: `[]` +Params: None -Result: list of [`AppInstanceInfo`](#data-type-appinstanceinfo) +Result: +- `appInstances:`[`AppInstanceInfo`](#data-type-appinstanceinfo)`[]` + - All the app instances installed on the Node ### Method: `proposeInstall` @@ -254,7 +270,7 @@ Events Fired if new app instance was successfully installed. -Params: +Data: - `appInstance:`[`AppInstanceInfo`](#data-type-appinstanceinfo) - Newly installed app instance @@ -262,7 +278,7 @@ Params: Fired if installation of a new app instance was rejected. -Params: +Data: - `appInstance:`[`AppInstanceInfo`](#data-type-appinstanceinfo) - Rejected app instance @@ -270,7 +286,7 @@ Params: Fired if app state is successfully updated. -Params: +Data: - `appInstanceId: string` - Unique ID of app instance - `newState:`[`AppState`](#data-type-appstate) @@ -282,7 +298,7 @@ Params: Fired if app instance is successfully uninstalled -Params: +Data: - `appInstance:`[`AppInstanceInfo`](#data-type-appinstanceinfo) - Uninstalled app instance - `myPayout: BigNumber` From bb120c9ddcfe9eab909c8443fbb9898760921e58 Mon Sep 17 00:00:00 2001 From: Patience Tema Baron Date: Mon, 26 Nov 2018 16:34:21 -0500 Subject: [PATCH 02/11] add cf.js data types (#269) * add cf.js data types * date type fixes * remove AppDefinition * make all AppFactory constructor args readonly --- packages/cf.js/src/app-factory.ts | 10 +++++-- packages/cf.js/src/app-instance.ts | 2 +- packages/cf.js/src/provider.ts | 6 ---- packages/cf.js/src/structs.ts | 7 ----- packages/cf.js/src/types.ts | 4 +-- packages/cf.js/src/types/protocol-types.ts | 30 +++++++++++++++++++ .../cf.js/src/{ => types}/simple-types.ts | 3 ++ packages/cf.js/src/utils/signature.ts | 2 +- 8 files changed, 45 insertions(+), 19 deletions(-) delete mode 100644 packages/cf.js/src/structs.ts create mode 100644 packages/cf.js/src/types/protocol-types.ts rename packages/cf.js/src/{ => types}/simple-types.ts (69%) diff --git a/packages/cf.js/src/app-factory.ts b/packages/cf.js/src/app-factory.ts index d023ef693..e43918dd3 100644 --- a/packages/cf.js/src/app-factory.ts +++ b/packages/cf.js/src/app-factory.ts @@ -1,5 +1,11 @@ -import { AppDefinition } from "./structs"; +import { Provider } from "./provider"; +import { AppABIEncodings } from "./types/protocol-types"; +import { Address } from "./types/simple-types"; export class AppFactory { - constructor(readonly appDefinition: AppDefinition) {} + constructor( + readonly provider: Provider, + readonly appId: Address, + readonly encodings: AppABIEncodings + ) {} } diff --git a/packages/cf.js/src/app-instance.ts b/packages/cf.js/src/app-instance.ts index 4adccc4a9..5db098a6f 100644 --- a/packages/cf.js/src/app-instance.ts +++ b/packages/cf.js/src/app-instance.ts @@ -1,4 +1,4 @@ -import { AppInstanceID } from "./simple-types"; +import { AppInstanceID } from "./types/simple-types"; export class AppInstance { constructor(readonly id: AppInstanceID) {} diff --git a/packages/cf.js/src/provider.ts b/packages/cf.js/src/provider.ts index d2d8567af..f2357f4a2 100644 --- a/packages/cf.js/src/provider.ts +++ b/packages/cf.js/src/provider.ts @@ -8,9 +8,7 @@ import { import cuid from "cuid"; -import { AppFactory } from "./app-factory"; import { AppInstance } from "./app-instance"; -import { AppDefinition } from "./structs"; export enum CounterfactualEventType { INSTALL = "cf_install", @@ -43,10 +41,6 @@ export class Provider { ); } - createAppFactory(appDefinition: AppDefinition): AppFactory { - return new AppFactory(appDefinition); - } - on( eventType: CounterfactualEventType, callback: (e: CounterfactualEvent) => void diff --git a/packages/cf.js/src/structs.ts b/packages/cf.js/src/structs.ts deleted file mode 100644 index 3be79156e..000000000 --- a/packages/cf.js/src/structs.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ABIEncoding, Address } from "./simple-types"; - -export interface AppDefinition { - address: Address; - appStateEncoding: ABIEncoding; - appActionEncoding: ABIEncoding; -} diff --git a/packages/cf.js/src/types.ts b/packages/cf.js/src/types.ts index 28bf68a09..9e1995a04 100644 --- a/packages/cf.js/src/types.ts +++ b/packages/cf.js/src/types.ts @@ -1,2 +1,2 @@ -export * from "./simple-types"; -export * from "./structs"; +export * from "./types/simple-types"; +export * from "./types/protocol-types"; diff --git a/packages/cf.js/src/types/protocol-types.ts b/packages/cf.js/src/types/protocol-types.ts new file mode 100644 index 000000000..a3309be08 --- /dev/null +++ b/packages/cf.js/src/types/protocol-types.ts @@ -0,0 +1,30 @@ +// https://github.com/counterfactual/monorepo/blob/master/packages/cf.js/API_REFERENCE.md#data-types +import { BigNumber } from "ethers/utils"; + +import { ABIEncoding, Address } from "./simple-types"; + +export interface AppInstanceInfo { + id: string; + appId: Address; + abiEncodings: AppABIEncodings; + asset: BlockchainAsset; + myDeposit: BigNumber; + peerDeposit: BigNumber; + timeout: BigNumber; +} + +export interface AppABIEncodings { + stateEncoding: ABIEncoding; + actionEncoding?: ABIEncoding; +} + +export enum AssetType { + ETH = 0, + ERC20 = 1, + Other = 2 +} + +export interface BlockchainAsset { + assetType: AssetType; + token?: Address; +} diff --git a/packages/cf.js/src/simple-types.ts b/packages/cf.js/src/types/simple-types.ts similarity index 69% rename from packages/cf.js/src/simple-types.ts rename to packages/cf.js/src/types/simple-types.ts index e9ffcb750..7fea1389f 100644 --- a/packages/cf.js/src/simple-types.ts +++ b/packages/cf.js/src/types/simple-types.ts @@ -2,3 +2,6 @@ export type ABIEncoding = string; export type AppInstanceID = string; export type Address = string; export type Bytes32 = string; + +export type AppState = any; +export type AppAction = any; diff --git a/packages/cf.js/src/utils/signature.ts b/packages/cf.js/src/utils/signature.ts index b5c049782..69175dd00 100644 --- a/packages/cf.js/src/utils/signature.ts +++ b/packages/cf.js/src/utils/signature.ts @@ -1,6 +1,6 @@ import * as ethers from "ethers"; -import { Bytes32 } from "../simple-types"; +import { Bytes32 } from "../types/simple-types"; export function signaturesToBytes( ...signatures: ethers.utils.Signature[] From d9be8524a691c45b6aac1b5e1cf2ff81059203df Mon Sep 17 00:00:00 2001 From: Li Xuanji Date: Tue, 27 Nov 2018 23:54:06 +0800 Subject: [PATCH 03/11] add new unlockedAccount (#268) * shift ranges * add unlockedAccount3 --- package.json | 11 +-- .../cf.js/src/legacy/utils/free-balance.ts | 6 ++ packages/contracts/package.json | 2 +- packages/machine/package.json | 6 +- .../test/integration/lifecycle.spec.ts | 74 ++++++++++++------- .../test/unit/state-transition.spec.ts | 6 +- 6 files changed, 66 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index 8bc5c1b8a..5f724fd84 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "build": "lerna run build --sort", "clean": "lerna run clean --parallel --no-bail", "test": "lerna run test --stream", - "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}\" &> /dev/null &", - "ganache:ci": "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}\"", + "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:ci": "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:stop": "ps aux | grep ganache-cli | grep -v grep | awk '{print $2}' | xargs kill -9", "lint": "lerna run lint --parallel --no-bail", "lint:fix": "lerna run lint:fix --parallel --no-bail" @@ -18,9 +18,10 @@ "ganachePort": 9545, "ganacheGasLimit": "0xfffffffffff", "ganacheGasPrice": "0x01", - "unlockedAccount0": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d", - "unlockedAccount1": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257e", - "unlockedAccount2": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257f", + "unlockedAccount0": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257a", + "unlockedAccount1": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257b", + "unlockedAccount2": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257c", + "unlockedAccount3": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d", "etherBalance": "1000000000000000000000000" }, "keywords": [ diff --git a/packages/cf.js/src/legacy/utils/free-balance.ts b/packages/cf.js/src/legacy/utils/free-balance.ts index 316653c0e..710844931 100644 --- a/packages/cf.js/src/legacy/utils/free-balance.ts +++ b/packages/cf.js/src/legacy/utils/free-balance.ts @@ -51,4 +51,10 @@ export class FreeBalance { readonly timeout: number, readonly dependencyNonce: Nonce ) {} + + public balanceOfAddress(address: Address): ethers.utils.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/contracts/package.json b/packages/contracts/package.json index 7cf03b3e9..33bf49732 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -24,7 +24,7 @@ }, "config": { "testFiles": "dist/test/**/*.spec.js", - "unlockedAccount": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d" + "unlockedAccount": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257a" }, "keywords": [ "ethereum", diff --git a/packages/machine/package.json b/packages/machine/package.json index 8697716a7..52bb8caa5 100644 --- a/packages/machine/package.json +++ b/packages/machine/package.json @@ -37,9 +37,9 @@ "ethers": "^4.0.4" }, "config": { - "unlockedAccount0": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d", - "unlockedAccount1": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257e", - "unlockedAccount2": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257f", + "unlockedAccount0": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257a", + "unlockedAccount1": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257b", + "unlockedAccount2": "0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257c", "etherBalance": "1000000000000000000000000" }, "jest": { diff --git a/packages/machine/test/integration/lifecycle.spec.ts b/packages/machine/test/integration/lifecycle.spec.ts index 4b1a4cc95..ff9e40948 100644 --- a/packages/machine/test/integration/lifecycle.spec.ts +++ b/packages/machine/test/integration/lifecycle.spec.ts @@ -351,16 +351,7 @@ class TicTacToeSimulator { peerA: TestResponseSink, peerB: TestResponseSink ): Promise { - TicTacToeSimulator.validateInstallWallet(peerA, peerB); - // Wait for other client to finish, since the machine is async await cf.legacy.utils.sleep(50); - return TicTacToeSimulator.validateInstallWallet(peerB, peerA); - } - - public static validateInstallWallet( - peerA: TestResponseSink, - peerB: TestResponseSink - ): string { const stateChannel = peerA.instructionExecutor.node.channelStates[UNUSED_FUNDED_ACCOUNT]; const appInstances = stateChannel.appInstances; @@ -372,15 +363,34 @@ class TicTacToeSimulator { expect(appInstances[cfAddr].peerA.balance.toNumber()).toEqual(2); expect(appInstances[cfAddr].peerB.balance.toNumber()).toEqual(2); - // now validate the free balance - const channel = - peerA.instructionExecutor.node.channelStates[UNUSED_FUNDED_ACCOUNT]; - // start with 10, 5 and both parties deposit 2 into TicTacToeSimulator. - expect(channel.freeBalance.aliceBalance.toNumber()).toEqual(8); - expect(channel.freeBalance.bobBalance.toNumber()).toEqual(3); + 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. */ @@ -495,17 +505,12 @@ class TicTacToeSimulator { const response = await peerA.runProtocol(msg); expect(response.status).toEqual(cf.legacy.node.ResponseStatus.COMPLETED); // A wins so give him 2 and subtract 2 from B + await cf.legacy.utils.sleep(50); TicTacToeSimulator.validateUninstall( cfAddr, peerA, ethers.utils.bigNumberify(12), - ethers.utils.bigNumberify(3) - ); - await cf.legacy.utils.sleep(50); - TicTacToeSimulator.validateUninstall( - cfAddr, peerB, - ethers.utils.bigNumberify(12), ethers.utils.bigNumberify(3) ); } @@ -537,15 +542,32 @@ class TicTacToeSimulator { public static validateUninstall( cfAddr: string, - wallet: TestResponseSink, + 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 channel = - wallet.instructionExecutor.node.channelStates[UNUSED_FUNDED_ACCOUNT]; const app = channel.appInstances[cfAddr]; - expect(channel.freeBalance.aliceBalance).toEqual(amountA); - expect(channel.freeBalance.bobBalance).toEqual(amountB); + 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/unit/state-transition.spec.ts b/packages/machine/test/unit/state-transition.spec.ts index c266e00cf..175261ac9 100644 --- a/packages/machine/test/unit/state-transition.spec.ts +++ b/packages/machine/test/unit/state-transition.spec.ts @@ -105,10 +105,8 @@ function validateSetupInfos(infos: cf.legacy.channel.StateChannelInfos) { expect(info.counterParty).toEqual(B_ADDRESS); expect(info.me).toEqual(A_ADDRESS); expect(Object.keys(info.appInstances).length).toEqual(0); - expect(info.freeBalance.alice).toEqual(A_ADDRESS); - expect(info.freeBalance.aliceBalance.toNumber()).toEqual(0); - expect(info.freeBalance.bob).toEqual(B_ADDRESS); - expect(info.freeBalance.bobBalance.toNumber()).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); From 9c7874eb9735da8df5b799241206bfbc5a1b9f4f Mon Sep 17 00:00:00 2001 From: Mitchell Van Der Hoeff <8631205+mvanderh@users.noreply.github.com> Date: Wed, 28 Nov 2018 14:46:04 -0500 Subject: [PATCH 04/11] [CFjs] Types for Node protocol methods: getAppInstances, proposeInstall (#274) * WIP: Types for methods: getAppInstances, proposeInstall - Move all Node protocol types to CFjs node-protocol.ts - Enum types for all methods and events - All types around methods: getAppInstances, proposeInstall - Update Provider to work - Some types reorg * Fix issues with packages * Lint * Node Namespace * Fixy * Fixies * Reshuffling * CfEvent -> DappEvent --- packages/cf.js/package.json | 1 - packages/cf.js/src/app-factory.ts | 3 +- packages/cf.js/src/app-instance.ts | 7 +- packages/cf.js/src/provider.ts | 83 +++++++------- packages/cf.js/src/types.ts | 2 - .../{protocol-types.ts => data-types.ts} | 0 packages/cf.js/src/types/index.ts | 32 ++++++ packages/cf.js/src/types/node-protocol.ts | 102 ++++++++++++++++++ packages/cf.js/test/provider.spec.ts | 93 +++++++++------- packages/node-provider/package.json | 1 + packages/node-provider/src/index.ts | 20 +--- packages/node-provider/src/node-provider.ts | 8 +- packages/node-provider/src/types.ts | 34 ------ 13 files changed, 243 insertions(+), 143 deletions(-) delete mode 100644 packages/cf.js/src/types.ts rename packages/cf.js/src/types/{protocol-types.ts => data-types.ts} (100%) create mode 100644 packages/cf.js/src/types/index.ts create mode 100644 packages/cf.js/src/types/node-protocol.ts delete mode 100644 packages/node-provider/src/types.ts diff --git a/packages/cf.js/package.json b/packages/cf.js/package.json index 10b583fdb..9b3dda52f 100644 --- a/packages/cf.js/package.json +++ b/packages/cf.js/package.json @@ -28,7 +28,6 @@ }, "dependencies": { "@counterfactual/contracts": "0.0.2", - "@counterfactual/node-provider": "0.0.1", "cuid": "^2.1.4", "ethers": "^4.0.4" }, diff --git a/packages/cf.js/src/app-factory.ts b/packages/cf.js/src/app-factory.ts index e43918dd3..178bfd13d 100644 --- a/packages/cf.js/src/app-factory.ts +++ b/packages/cf.js/src/app-factory.ts @@ -1,6 +1,5 @@ import { Provider } from "./provider"; -import { AppABIEncodings } from "./types/protocol-types"; -import { Address } from "./types/simple-types"; +import { Address, AppABIEncodings } from "./types"; export class AppFactory { constructor( diff --git a/packages/cf.js/src/app-instance.ts b/packages/cf.js/src/app-instance.ts index 5db098a6f..06f00611c 100644 --- a/packages/cf.js/src/app-instance.ts +++ b/packages/cf.js/src/app-instance.ts @@ -1,5 +1,8 @@ -import { AppInstanceID } from "./types/simple-types"; +import { AppInstanceID, AppInstanceInfo } from "./types"; export class AppInstance { - constructor(readonly id: AppInstanceID) {} + readonly id: AppInstanceID; + constructor(readonly info: AppInstanceInfo) { + this.id = info.id; + } } diff --git a/packages/cf.js/src/provider.ts b/packages/cf.js/src/provider.ts index f2357f4a2..8ea3c56a9 100644 --- a/packages/cf.js/src/provider.ts +++ b/packages/cf.js/src/provider.ts @@ -1,23 +1,12 @@ -import { - INodeProvider, - NodeMessage, - NodeMessageType, - NodeQueryData, - QueryType -} from "@counterfactual/node-provider"; - import cuid from "cuid"; import { AppInstance } from "./app-instance"; +import { INodeProvider, Node } from "./types"; -export enum CounterfactualEventType { - INSTALL = "cf_install", - PROPOSE_INSTALL = "cf_proposeInstall", - REJECT_INSTALL = "cf_rejectInstall" -} +export import DappEventType = Node.EventName; -export interface CounterfactualEvent { - readonly eventType: CounterfactualEventType; +export interface DappEvent { + readonly type: DappEventType; readonly data: any; // TODO } @@ -25,7 +14,7 @@ const NODE_REQUEST_TIMEOUT = 1500; export class Provider { private readonly requestListeners: { - [requestId: string]: (msg: NodeMessage) => void; + [requestId: string]: (msg: Node.Message) => void; } = {}; constructor(readonly nodeProvider: INodeProvider) { @@ -33,32 +22,37 @@ export class Provider { } async getAppInstances(): Promise { - const response = await this.sendNodeRequest(NodeMessageType.QUERY, { - queryType: QueryType.GET_APP_INSTANCES - }); - return (response.data as NodeQueryData).appInstances!.map( - ({ id }) => new AppInstance(id) + const response = await this.callNodeMethod( + Node.MethodName.GET_APP_INSTANCES, + {} ); + const result = response.result as Node.GetAppInstancesResult; + return result.appInstances.map(info => new AppInstance(info)); } - on( - eventType: CounterfactualEventType, - callback: (e: CounterfactualEvent) => void - ) { + on(eventName: DappEventType, callback: (e: DappEvent) => void) { // TODO: support notification observers } - private async sendNodeRequest( - messageType: NodeMessageType, - data: any - ): Promise { + private async callNodeMethod( + methodName: Node.MethodName, + params: Node.MethodParams + ): Promise { const requestId = cuid(); - return new Promise((resolve, reject) => { - this.requestListeners[requestId] = msg => { - if (msg.messageType === NodeMessageType.ERROR) { - return reject(msg); + return new Promise((resolve, reject) => { + this.requestListeners[requestId] = response => { + if (response.type === Node.ErrorType.ERROR) { + return reject(response.data); + } + if (response.type !== methodName) { + return reject({ + errorName: "unexpected_message_type", + message: `Unexpected response type. Expected ${methodName}, got ${ + response.type + }` + }); } - resolve(msg); + resolve(response as Node.MethodResponse); }; setTimeout(() => { if (this.requestListeners[requestId] !== undefined) { @@ -66,20 +60,23 @@ export class Provider { delete this.requestListeners[requestId]; } }, NODE_REQUEST_TIMEOUT); - this.nodeProvider.postMessage({ + this.nodeProvider.sendMessage({ requestId, - messageType, - data + params, + type: methodName }); }); } - private onNodeMessage(message: NodeMessage) { - const { requestId } = message; - if (this.requestListeners[requestId]) { - this.requestListeners[requestId](message); - delete this.requestListeners[requestId]; + private onNodeMessage(message: Node.Message) { + const requestId = (message as Node.MethodResponse).requestId; + if (requestId) { + if (this.requestListeners[requestId]) { + this.requestListeners[requestId](message); + delete this.requestListeners[requestId]; + } + } else { + // TODO: notify observers } - // TODO: notify observers } } diff --git a/packages/cf.js/src/types.ts b/packages/cf.js/src/types.ts deleted file mode 100644 index 9e1995a04..000000000 --- a/packages/cf.js/src/types.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./types/simple-types"; -export * from "./types/protocol-types"; diff --git a/packages/cf.js/src/types/protocol-types.ts b/packages/cf.js/src/types/data-types.ts similarity index 100% rename from packages/cf.js/src/types/protocol-types.ts rename to packages/cf.js/src/types/data-types.ts diff --git a/packages/cf.js/src/types/index.ts b/packages/cf.js/src/types/index.ts new file mode 100644 index 000000000..dcf6f5ed1 --- /dev/null +++ b/packages/cf.js/src/types/index.ts @@ -0,0 +1,32 @@ +import { + ABIEncoding, + Address, + AppAction, + AppInstanceID, + AppState, + Bytes32 +} from "./simple-types"; + +import { + AppABIEncodings, + AppInstanceInfo, + AssetType, + BlockchainAsset +} from "./data-types"; + +import { INodeProvider, Node } from "./node-protocol"; + +export { + INodeProvider, + Node, + AssetType, + AppABIEncodings, + BlockchainAsset, + AppInstanceInfo, + ABIEncoding, + Address, + AppAction, + AppInstanceID, + AppState, + Bytes32 +}; diff --git a/packages/cf.js/src/types/node-protocol.ts b/packages/cf.js/src/types/node-protocol.ts new file mode 100644 index 000000000..bb44a3a00 --- /dev/null +++ b/packages/cf.js/src/types/node-protocol.ts @@ -0,0 +1,102 @@ +import { BigNumber } from "ethers/utils"; + +import { + AppABIEncodings, + AppInstanceInfo, + BlockchainAsset +} from "./data-types"; +import { Address, AppInstanceID, AppState } from "./simple-types"; + +export interface INodeProvider { + onMessage(callback: (message: Node.Message) => void); + sendMessage(message: Node.Message); +} + +export namespace Node { + export enum ErrorType { + ERROR = "error" + } + + // SOURCE: https://github.com/counterfactual/monorepo/blob/master/packages/cf.js/API_REFERENCE.md#public-methods + export enum MethodName { + GET_APP_INSTANCES = "getAppInstances", + PROPOSE_INSTALL = "proposeInstall", + REJECT_INSTALL = "rejectInstall", + INSTALL = "install", + GET_STATE = "getState", + GET_APP_INSTANCE_DETAILS = "getAppInstanceDetails", + TAKE_ACTION = "takeAction", + UNINSTALL = "uninstall", + PROPOSE_STATE = "proposeState", + ACCEPT_STATE = "acceptState", + REJECT_STATE = "rejectState" + } + + // SOURCE: https://github.com/counterfactual/monorepo/blob/master/packages/cf.js/API_REFERENCE.md#events + export enum EventName { + INSTALL = "install", + REJECT_INSTALL = "rejectInstall", + UPDATE_STATE = "updateState", + UNINSTALL = "uninstall", + PROPOSE_STATE = "proposeState", + REJECT_STATE = "rejectState" + } + + export interface GetAppInstancesParams {} + + export interface GetAppInstancesResult { + appInstances: AppInstanceInfo[]; + } + + export interface ProposeInstallParams { + peerAddress: Address; + appId: Address; + abiEncodings: AppABIEncodings; + asset: BlockchainAsset; + myDeposit: BigNumber; + peerDeposit: BigNumber; + timeout: BigNumber; + initialState: AppState; + } + export interface ProposeInstallResult { + appInstanceId: AppInstanceID; + } + + export type MethodParams = GetAppInstancesParams | ProposeInstallParams; + export type MethodResult = GetAppInstancesResult | ProposeInstallResult; + + export interface InstallEventData { + appInstanceId: AppInstanceID; + } + + export type EventData = InstallEventData; + + export interface MethodMessage { + type: MethodName; + requestId: string; + } + + export interface MethodRequest extends MethodMessage { + params: MethodParams; + } + + export interface MethodResponse extends MethodMessage { + result: MethodResult; + } + + export interface Event { + type: EventName; + data: EventData; + } + + export interface Error { + type: ErrorType; + requestId?: string; + data: { + errorName: string; + message?: string; + }; + } + + export type Message = MethodRequest | MethodResponse | Event | Error; +} diff --git a/packages/cf.js/test/provider.spec.ts b/packages/cf.js/test/provider.spec.ts index 73371436b..64c4582d6 100644 --- a/packages/cf.js/test/provider.spec.ts +++ b/packages/cf.js/test/provider.spec.ts @@ -1,27 +1,23 @@ -import { - INodeProvider, - NodeMessage, - NodeMessageType, - NodeQueryData, - QueryType -} from "@counterfactual/node-provider"; +import { BigNumber } from "ethers/utils"; import { AppInstance } from "../src/app-instance"; import { Provider } from "../src/provider"; +import { AssetType } from "../src/types"; +import { INodeProvider, Node } from "../src/types/node-protocol"; class TestNodeProvider implements INodeProvider { - public postedMessages: NodeMessage[] = []; - readonly callbacks: ((message: NodeMessage) => void)[] = []; + public postedMessages: Node.Message[] = []; + readonly callbacks: ((message: Node.Message) => void)[] = []; - public sendMessageToClient(message: NodeMessage) { + public simulateMessageFromNode(message: Node.Message) { this.callbacks.forEach(cb => cb(message)); } - public onMessage(callback: (message: NodeMessage) => void) { + public onMessage(callback: (message: Node.Message) => void) { this.callbacks.push(callback); } - public postMessage(message: NodeMessage) { + public sendMessage(message: Node.Message) { this.postedMessages.push(message); } } @@ -35,33 +31,61 @@ describe("CF.js Provider", async () => { provider = new Provider(nodeProvider); }); - it("should respond correctly to errors", async () => { - expect.assertions(4); + it("should respond correctly to a generic error", async () => { + expect.assertions(3); + const promise = provider.getAppInstances(); + + expect(nodeProvider.postedMessages).toHaveLength(1); + + const request = nodeProvider.postedMessages[0] as Node.MethodResponse; + expect(request.type).toBe(Node.MethodName.GET_APP_INSTANCES); + + nodeProvider.simulateMessageFromNode({ + requestId: request.requestId, + type: Node.ErrorType.ERROR, + data: { errorName: "music_too_loud", message: "Music too loud" } + }); + + try { + await promise; + } catch (e) { + expect(e.message).toBe("Music too loud"); + } + }); + + it("should respond correctly to message type mismatch", async () => { + expect.assertions(3); const promise = provider.getAppInstances(); expect(nodeProvider.postedMessages).toHaveLength(1); - const queryMessage = nodeProvider.postedMessages[0]; - expect(queryMessage.messageType).toBe(NodeMessageType.QUERY); - const queryData = queryMessage.data! as NodeQueryData; - expect(queryData.queryType).toBe(QueryType.GET_APP_INSTANCES); + const request = nodeProvider.postedMessages[0] as Node.MethodResponse; + expect(request.type).toBe(Node.MethodName.GET_APP_INSTANCES); - nodeProvider.sendMessageToClient({ - requestId: queryMessage.requestId, - messageType: NodeMessageType.ERROR, - data: { message: "Music too loud" } + nodeProvider.simulateMessageFromNode({ + requestId: request.requestId, + type: Node.MethodName.PROPOSE_INSTALL, + result: { appInstanceId: "" } }); try { await promise; } catch (e) { - expect(e.data.message).toBe("Music too loud"); + expect(e.errorName).toBe("unexpected_message_type"); } }); it("should query app instances and return them", async () => { - expect.assertions(5); - const testInstance = new AppInstance("TEST_ID"); + expect.assertions(4); + const testInstance = new AppInstance({ + id: "TEST_ID", + asset: { assetType: AssetType.ETH }, + abiEncodings: { actionEncoding: "", stateEncoding: "" }, + appId: "", + myDeposit: new BigNumber("0"), + peerDeposit: new BigNumber("0"), + timeout: new BigNumber("0") + }); provider.getAppInstances().then(instances => { expect(instances).toHaveLength(1); @@ -69,17 +93,14 @@ describe("CF.js Provider", async () => { }); expect(nodeProvider.postedMessages).toHaveLength(1); - const queryMessage = nodeProvider.postedMessages[0]; - expect(queryMessage.messageType).toBe(NodeMessageType.QUERY); - const queryData = queryMessage.data as NodeQueryData; - expect(queryData.queryType).toBe(QueryType.GET_APP_INSTANCES); - - nodeProvider.sendMessageToClient({ - requestId: queryMessage.requestId, - messageType: NodeMessageType.QUERY, - data: { - queryType: QueryType.GET_APP_INSTANCES, - appInstances: [testInstance] + const request = nodeProvider.postedMessages[0] as Node.MethodRequest; + expect(request.type).toBe(Node.MethodName.GET_APP_INSTANCES); + + nodeProvider.simulateMessageFromNode({ + type: Node.MethodName.GET_APP_INSTANCES, + requestId: request.requestId, + result: { + appInstances: [testInstance.info] } }); }); diff --git a/packages/node-provider/package.json b/packages/node-provider/package.json index 14cc8bfc2..2b8b9c3d8 100644 --- a/packages/node-provider/package.json +++ b/packages/node-provider/package.json @@ -17,6 +17,7 @@ "lint": "tslint -c tslint.json -p ." }, "devDependencies": { + "@counterfactual/cf.js": "0.0.1", "@types/jest": "^23.3.3", "@types/node": "^10.9.3", "jest": "^23.6.0", diff --git a/packages/node-provider/src/index.ts b/packages/node-provider/src/index.ts index 7ee262339..5e1f32bcf 100644 --- a/packages/node-provider/src/index.ts +++ b/packages/node-provider/src/index.ts @@ -1,21 +1,3 @@ import NodeProvider from "./node-provider"; -import { - AppInstanceInfo, - INodeProvider, - NodeErrorData, - NodeMessage, - NodeMessageType, - NodeQueryData, - QueryType -} from "./types"; -export { - NodeProvider, - INodeProvider, - NodeMessageType, - QueryType, - AppInstanceInfo, - NodeQueryData, - NodeErrorData, - NodeMessage -}; +export { NodeProvider }; diff --git a/packages/node-provider/src/node-provider.ts b/packages/node-provider/src/node-provider.ts index 65bb763d2..c1c1df350 100644 --- a/packages/node-provider/src/node-provider.ts +++ b/packages/node-provider/src/node-provider.ts @@ -1,7 +1,7 @@ -import { INodeProvider, NodeMessage } from "./types"; +import * as cf from "@counterfactual/cf.js"; -export default class NodeProvider implements INodeProvider { - public onMessage(callback: (message: NodeMessage) => void) {} +export default class NodeProvider implements cf.types.INodeProvider { + public onMessage(callback: (message: cf.types.Node.Message) => void) {} - public postMessage(message: NodeMessage) {} + public sendMessage(message: cf.types.Node.Message) {} } diff --git a/packages/node-provider/src/types.ts b/packages/node-provider/src/types.ts deleted file mode 100644 index e4fdd7a81..000000000 --- a/packages/node-provider/src/types.ts +++ /dev/null @@ -1,34 +0,0 @@ -export interface INodeProvider { - onMessage(callback: (message: NodeMessage) => void); - postMessage(message: NodeMessage); -} - -export enum NodeMessageType { - INSTALL = "install", - QUERY = "query", - ERROR = "error" -} - -export enum QueryType { - GET_APP_INSTANCES = "getAppInstances" -} - -export interface AppInstanceInfo { - id: string; -} - -export interface NodeQueryData { - queryType: QueryType; - appInstances?: AppInstanceInfo[]; -} - -export interface NodeErrorData { - message: string; - extra?: { [key: string]: any }; -} - -export interface NodeMessage { - requestId: string; - messageType: NodeMessageType; - data: NodeQueryData | NodeErrorData | null; -} From 5f973da56386363469a06f680f4073d2068a5c7f Mon Sep 17 00:00:00 2001 From: Patience Tema Baron Date: Wed, 28 Nov 2018 17:56:36 -0500 Subject: [PATCH 05/11] add apps-list and apps-list-item components (#277) --- .../src/assets/icon/high-roller.svg | 36 +++++++++++ .../src/components/app-root/app-root.tsx | 16 +++++ .../apps-list-item/apps-list-item.css | 64 +++++++++++++++++++ .../apps-list-item/apps-list-item.e2e.ts | 11 ++++ .../apps-list-item/apps-list-item.spec.ts | 7 ++ .../apps-list-item/apps-list-item.tsx | 25 ++++++++ .../src/components/apps-list/apps-list.css | 14 ++++ .../src/components/apps-list/apps-list.e2e.ts | 11 ++++ .../components/apps-list/apps-list.spec.ts | 7 ++ .../src/components/apps-list/apps-list.tsx | 26 ++++++++ packages/playground/src/global/app.css | 13 ++++ packages/playground/src/types.ts | 5 ++ 12 files changed, 235 insertions(+) create mode 100644 packages/playground/src/assets/icon/high-roller.svg create mode 100644 packages/playground/src/components/apps-list-item/apps-list-item.css create mode 100644 packages/playground/src/components/apps-list-item/apps-list-item.e2e.ts create mode 100644 packages/playground/src/components/apps-list-item/apps-list-item.spec.ts create mode 100644 packages/playground/src/components/apps-list-item/apps-list-item.tsx create mode 100644 packages/playground/src/components/apps-list/apps-list.css create mode 100644 packages/playground/src/components/apps-list/apps-list.e2e.ts create mode 100644 packages/playground/src/components/apps-list/apps-list.spec.ts create mode 100644 packages/playground/src/components/apps-list/apps-list.tsx create mode 100644 packages/playground/src/types.ts diff --git a/packages/playground/src/assets/icon/high-roller.svg b/packages/playground/src/assets/icon/high-roller.svg new file mode 100644 index 000000000..61522d6db --- /dev/null +++ b/packages/playground/src/assets/icon/high-roller.svg @@ -0,0 +1,36 @@ + + + + #UI / Components / Logo + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/playground/src/components/app-root/app-root.tsx b/packages/playground/src/components/app-root/app-root.tsx index fe333c74f..98cbdf87d 100644 --- a/packages/playground/src/components/app-root/app-root.tsx +++ b/packages/playground/src/components/app-root/app-root.tsx @@ -1,5 +1,19 @@ import { Component } from "@stencil/core"; +const apps = { + // TODO: How do we get a list of available apps? + "0x822c045f6F5e7E8090eA820E24A5f327C4E62c96": { + name: "High Roller", + url: "dapps/high-roller.html", + icon: "assets/icon/high-roller.svg" + }, + "0xd545e82792b6EF2000908F224275ED0456Cf36FA": { + name: "Tic-Tac-Toe", + url: "dapps/tic-tac-toe.html", + icon: "assets/icon/icon.png" + } +}; + @Component({ tag: "app-root", styleUrl: "app-root.css", @@ -19,6 +33,8 @@ export class AppRoot { + + ); diff --git a/packages/playground/src/components/apps-list-item/apps-list-item.css b/packages/playground/src/components/apps-list-item/apps-list-item.css new file mode 100644 index 000000000..5800b25e8 --- /dev/null +++ b/packages/playground/src/components/apps-list-item/apps-list-item.css @@ -0,0 +1,64 @@ +.item { + position: relative; + flex-basis: calc(100% / 5 - 2rem); + margin: 1rem; +} + +.icon { + position: relative; + width: 100%; + height: 0; + padding-top: 100%; + border-radius: 24px; +} + +.icon > .notification { + position: absolute; + top: -0.25rem; + right: -0.25rem; + display: flex; + justify-content: center; + align-items: center; + width: 1.25rem; + height: 1.25rem; + border-radius: 50%; + color: var(--c-white); + font-size: var(--f-sm); + background: var(--c-red); + z-index: 2; +} + +.icon > img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 24px; + object-fit: cover; + z-index: 1; +} + +.name { + display: block; + font-size: var(--f-base); + text-align: center; + font-weight: var(--f-light); + color: var(--c-darkgrey); + color: #707070; + line-height: 37.5px; +} + +@media screen and (max-width: var(--screen-sm)) { + .item { + flex-basis: calc(100% / 3 - 2rem); + } + + .icon { + border-radius: 16px; + } + + .icon > img { + border-radius: 16px; + } +} \ No newline at end of file diff --git a/packages/playground/src/components/apps-list-item/apps-list-item.e2e.ts b/packages/playground/src/components/apps-list-item/apps-list-item.e2e.ts new file mode 100644 index 000000000..c791dd988 --- /dev/null +++ b/packages/playground/src/components/apps-list-item/apps-list-item.e2e.ts @@ -0,0 +1,11 @@ +import { newE2EPage } from "@stencil/core/testing"; + +describe("apps-list-item", () => { + it("renders", async () => { + const page = await newE2EPage(); + await page.setContent(""); + + const element = await page.find("apps-list-item"); + expect(element).toHaveClass("hydrated"); + }); +}); diff --git a/packages/playground/src/components/apps-list-item/apps-list-item.spec.ts b/packages/playground/src/components/apps-list-item/apps-list-item.spec.ts new file mode 100644 index 000000000..84905aea0 --- /dev/null +++ b/packages/playground/src/components/apps-list-item/apps-list-item.spec.ts @@ -0,0 +1,7 @@ +import { AppsListItem } from "./apps-list-item"; + +describe("app", () => { + it("builds", () => { + expect(new AppsListItem()).toBeTruthy(); + }); +}); diff --git a/packages/playground/src/components/apps-list-item/apps-list-item.tsx b/packages/playground/src/components/apps-list-item/apps-list-item.tsx new file mode 100644 index 000000000..ddad29e2d --- /dev/null +++ b/packages/playground/src/components/apps-list-item/apps-list-item.tsx @@ -0,0 +1,25 @@ +import { Component, Prop } from "@stencil/core"; + +@Component({ + tag: "apps-list-item", + styleUrl: "apps-list-item.css", + shadow: true +}) +export class AppsListItem { + @Prop() icon: string; + @Prop() name: string; + @Prop() url: string; + + render() { + return ( +
  • + +
    + {this.name} +
    + {this.name} +
    +
  • + ); + } +} diff --git a/packages/playground/src/components/apps-list/apps-list.css b/packages/playground/src/components/apps-list/apps-list.css new file mode 100644 index 000000000..86146b74d --- /dev/null +++ b/packages/playground/src/components/apps-list/apps-list.css @@ -0,0 +1,14 @@ +.list { + display: flex; + margin: -1rem; + flex-wrap: wrap; + list-style: none; +} + +@media screen and (max-width: var(--screen-sm)) { + .list { + display: flex; + margin: -1rem; + flex-wrap: wrap; + } +} \ No newline at end of file diff --git a/packages/playground/src/components/apps-list/apps-list.e2e.ts b/packages/playground/src/components/apps-list/apps-list.e2e.ts new file mode 100644 index 000000000..1c56bdb04 --- /dev/null +++ b/packages/playground/src/components/apps-list/apps-list.e2e.ts @@ -0,0 +1,11 @@ +import { newE2EPage } from "@stencil/core/testing"; + +describe("apps-list", () => { + it("renders", async () => { + const page = await newE2EPage(); + await page.setContent(""); + + const element = await page.find("apps-list"); + expect(element).toHaveClass("hydrated"); + }); +}); diff --git a/packages/playground/src/components/apps-list/apps-list.spec.ts b/packages/playground/src/components/apps-list/apps-list.spec.ts new file mode 100644 index 000000000..f963a9190 --- /dev/null +++ b/packages/playground/src/components/apps-list/apps-list.spec.ts @@ -0,0 +1,7 @@ +import { AppsList } from "./apps-list"; + +describe("app", () => { + it("builds", () => { + expect(new AppsList()).toBeTruthy(); + }); +}); diff --git a/packages/playground/src/components/apps-list/apps-list.tsx b/packages/playground/src/components/apps-list/apps-list.tsx new file mode 100644 index 000000000..f7c3d9ef2 --- /dev/null +++ b/packages/playground/src/components/apps-list/apps-list.tsx @@ -0,0 +1,26 @@ +import { Component, Prop } from "@stencil/core"; + +import { AppDefinition } from "../../types"; + +@Component({ + tag: "apps-list", + styleUrl: "apps-list.css", + shadow: true +}) +export class AppsList { + @Prop() apps: { [s: string]: AppDefinition }; + + public get appsList(): AppDefinition[] { + return Object.keys(this.apps).map(key => this.apps[key]); + } + + render() { + return ( +
      + {this.appsList.map(app => ( + + ))} +
    + ); + } +} diff --git a/packages/playground/src/global/app.css b/packages/playground/src/global/app.css index c6d938ccb..fc4835b8b 100644 --- a/packages/playground/src/global/app.css +++ b/packages/playground/src/global/app.css @@ -14,3 +14,16 @@ body { padding: 0px; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; } + +:root { + --c-darkgrey: #707070; + --c-red: #FF0000; + --c-white: #FFF; + + --f-base: 1rem; + --f-sm: 0.73rem; + + --f-light: 300; + + --screen-sm: 600px; +} \ No newline at end of file diff --git a/packages/playground/src/types.ts b/packages/playground/src/types.ts new file mode 100644 index 000000000..de982b8be --- /dev/null +++ b/packages/playground/src/types.ts @@ -0,0 +1,5 @@ +export interface AppDefinition { + name: string; + url: string; + icon: string; +} From c4764f9deb841e8607f268ad98a89dcdeb6a76ab Mon Sep 17 00:00:00 2001 From: Joel Alejandro Villarreal Bertoldi Date: Wed, 28 Nov 2018 20:19:41 -0300 Subject: [PATCH 06/11] [node-provider] Implements basic API (#273) * node-provider: initial tests + interface tweaks * node-provider: added rollup config, EventEmitter3 * node-provider: added implementation, added tests * node-provider: renamed 'emit' to 'sendMessage' * node-provider: removed unneeded imports * node-provider: mocked addEventListener / postMessage * node-provider: cleaned up mocks * node-provider: inlined messagePort() * node-provider: enabled 'browser' in rollup config * node-provider: finished tests * node-provider: fixed typo in comments * node-provider: updated tests to use cf.js types * node-provider: added sendMessage test case (coverage 91.6%) * node-provider: added comments about `isConnected` --- packages/node-provider/package.json | 15 +++- packages/node-provider/rollup.config.js | 9 +- packages/node-provider/src/node-provider.ts | 69 ++++++++++++++- .../test/unit/node-provider.spec.ts | 87 +++++++++++++++++++ .../test/utils/message-api-mocks.ts | 74 ++++++++++++++++ 5 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 packages/node-provider/test/utils/message-api-mocks.ts diff --git a/packages/node-provider/package.json b/packages/node-provider/package.json index 2b8b9c3d8..487ab0f2f 100644 --- a/packages/node-provider/package.json +++ b/packages/node-provider/package.json @@ -1,9 +1,9 @@ { "name": "@counterfactual/node-provider", "version": "0.0.1", - "main": "dist/index.js", - "iife": "dist/index-iife.js", - "types": "dist/index.d.ts", + "main": "dist/node-provider.js", + "iife": "dist/node-provider.iife.js", + "types": "dist/node-provider.d.ts", "files": [ "dist" ], @@ -11,7 +11,7 @@ "scripts": { "clean": "rm -rf .rpt2_cache jest-cache build dist", "build": "tsc -p tsconfig.json && rollup -c", - "test": "tsc -b && jest --runInBand --detectOpenHandles --bail", + "test": "tsc -b && jest --runInBand --detectOpenHandles --bail --forceExit", "test-debug": "node --inspect-brk jest --runInBand", "lint:fix": "tslint -c tslint.json -p . --fix", "lint": "tslint -c tslint.json -p ." @@ -21,6 +21,10 @@ "@types/jest": "^23.3.3", "@types/node": "^10.9.3", "jest": "^23.6.0", + "rollup": "^0.67.3", + "rollup-plugin-commonjs": "^9.2.0", + "rollup-plugin-node-resolve": "^3.4.0", + "rollup-plugin-typescript2": "^0.18.0", "ts-jest": "^23.1.4", "tslint": "^5.11.0", "typescript": "^3.1.2" @@ -47,5 +51,8 @@ "json" ], "testURL": "http://localhost/" + }, + "dependencies": { + "eventemitter3": "^3.1.0" } } diff --git a/packages/node-provider/rollup.config.js b/packages/node-provider/rollup.config.js index b0a6ef9f1..0a5cfc479 100644 --- a/packages/node-provider/rollup.config.js +++ b/packages/node-provider/rollup.config.js @@ -1,5 +1,6 @@ import typescript from "rollup-plugin-typescript2"; - +import resolve from "rollup-plugin-node-resolve"; +import commonjs from "rollup-plugin-commonjs"; import pkg from "./package.json"; export default [ @@ -17,7 +18,11 @@ export default [ } ], plugins: [ - typescript() + typescript(), + resolve({ + browser: true + }), + commonjs(), ] } ]; diff --git a/packages/node-provider/src/node-provider.ts b/packages/node-provider/src/node-provider.ts index c1c1df350..5ed9db81c 100644 --- a/packages/node-provider/src/node-provider.ts +++ b/packages/node-provider/src/node-provider.ts @@ -1,7 +1,72 @@ import * as cf from "@counterfactual/cf.js"; +import EventEmitter from "eventemitter3"; export default class NodeProvider implements cf.types.INodeProvider { - public onMessage(callback: (message: cf.types.Node.Message) => void) {} + /** + * This boolean determines if the NodeProvider has received a MessagePort + * via the `cf-node-provider:port` message. + * + * It is used to prevent attempts to send messages without an instance + * of MessagePort stored locally. + */ + private isConnected: boolean; + private eventEmitter: EventEmitter; + private messagePort?: MessagePort; - public sendMessage(message: cf.types.Node.Message) {} + constructor() { + this.isConnected = false; + this.eventEmitter = new EventEmitter(); + } + + public onMessage(callback: (message: cf.types.Node.Message) => void) { + this.eventEmitter.on("message", callback); + } + + public sendMessage(message: cf.types.Node.Message) { + if (!this.isConnected || !this.messagePort) { + // We fail because we do not have a messagePort available. + throw new Error( + "It's not possible to use postMessage() before the NodeProvider is connected. Call the connect() method first." + ); + } + + this.messagePort.postMessage(message); + } + + public async connect(): Promise { + if (this.isConnected) { + console.warn("NodeProvider is already connected."); + return Promise.resolve(this); + } + + return new Promise((resolve, reject) => { + window.addEventListener("message", event => { + if (event.data === "cf-node-provider:port") { + // This message is received from the Playground to connect it + // to the dApp so they can exchange messages. + this.startMessagePort(event); + this.notifyNodeProviderIsConnected(); + resolve(this); + } + }); + + window.postMessage("cf-node-provider:init", "*"); + }); + } + + private startMessagePort(event: MessageEvent) { + this.messagePort = event.ports[0]; + this.messagePort.addEventListener("message", event => { + // Every message received by the messagePort will be + // relayed to whoever has subscribed to the "message" + // event using `onMessage()`. + this.eventEmitter.emit("message", event.data); + }); + this.messagePort.start(); + } + + private notifyNodeProviderIsConnected() { + window.postMessage("cf-node-provider:ready", "*"); + this.isConnected = true; + } } diff --git a/packages/node-provider/test/unit/node-provider.spec.ts b/packages/node-provider/test/unit/node-provider.spec.ts index 1e06c9463..3ac0a95e6 100644 --- a/packages/node-provider/test/unit/node-provider.spec.ts +++ b/packages/node-provider/test/unit/node-provider.spec.ts @@ -1,7 +1,94 @@ +import * as cf from "@counterfactual/cf.js"; + import NodeProvider from "../../src/node-provider"; +import { + createMockMessageChannel, + mockAddEventListenerFunction, + MockMessagePort, + mockPostMessageFunction +} from "../utils/message-api-mocks"; + +const originalAddEventListener = window.addEventListener; +const originalPostMessage = window.postMessage; + +const context = { + originalAddEventListener, + messageCallbacks: [], + connected: false, + dappPort: new MockMessagePort(), + nodeProviderPort: new MockMessagePort() +}; describe("NodeProvider", () => { + beforeAll(() => { + window.addEventListener = mockAddEventListenerFunction(context); + window.postMessage = mockPostMessageFunction(context); + + window.addEventListener("message", event => { + if (event.data === "cf-node-provider:init") { + const { port1, port2 } = createMockMessageChannel(); + context.dappPort = port1; + context.nodeProviderPort = port2; + window.postMessage("cf-node-provider:port", "*", [port2]); + } + + if (event.data === "cf-node-provider:ready") { + context.connected = true; + } + }); + }); + beforeEach(() => { + context.connected = false; + context.dappPort = new MockMessagePort(); + context.nodeProviderPort = new MockMessagePort(); + }); it("should instantiate", () => { new NodeProvider(); }); + it("should connect", async () => { + const nodeProvider = new NodeProvider(); + await nodeProvider.connect(); + + expect(context.connected).toBe(true); + }); + it("should emit a warning if you're connecting twice", async () => { + const originalConsoleWarn = console.warn; + console.warn = jest.fn(); + + const nodeProvider = new NodeProvider(); + await nodeProvider.connect(); + await nodeProvider.connect(); + + expect(console.warn).toBeCalledWith("NodeProvider is already connected."); + console.warn = originalConsoleWarn; + }); + it("should fail to send a message if not connected", () => { + const nodeProvider = new NodeProvider(); + + expect(() => { + nodeProvider.sendMessage({ + type: cf.types.Node.MethodName.INSTALL + } as cf.types.Node.Message); + }).toThrow( + "It's not possible to use postMessage() before the NodeProvider is connected. Call the connect() method first." + ); + }); + it("should send a message", async () => { + const nodeProvider = new NodeProvider(); + await nodeProvider.connect(); + + const messageToSend = { + type: cf.types.Node.MethodName.INSTALL + } as cf.types.Node.Message; + + const port = context.nodeProviderPort as MockMessagePort; + const spyPortPostMessage = jest.spyOn(port, "postMessage"); + + nodeProvider.sendMessage(messageToSend); + expect(spyPortPostMessage).toBeCalledWith(messageToSend); + }); + afterAll(() => { + window.addEventListener = originalAddEventListener; + window.postMessage = originalPostMessage; + }); }); diff --git a/packages/node-provider/test/utils/message-api-mocks.ts b/packages/node-provider/test/utils/message-api-mocks.ts new file mode 100644 index 000000000..e10b8849a --- /dev/null +++ b/packages/node-provider/test/utils/message-api-mocks.ts @@ -0,0 +1,74 @@ +export function createMockMessageEvent(message, transferables) { + return { + data: message, + ports: transferables, + type: "message" + }; +} + +export function createMockMessageChannel() { + return { + port1: createMockMessagePort(), + port2: createMockMessagePort() + }; +} + +export class MockMessagePort { + // These properties are needed to fool TypeScript into believing + // this is a Transferable type. + height: number = 0; + width: number = 0; + + private onMessageCallback: Function[] = []; + private onMessageErrorCallback: Function[] = []; + + onMessage(callback: Function) { + this.onMessageCallback.push(callback); + } + + onMessageError(callback: Function) { + this.onMessageErrorCallback.push(callback); + } + + addEventListener(event: string, callback: Function) { + if (event === "message") { + this.onMessage(callback); + } + + if (event === "messageerror") { + this.onMessageError(callback); + } + } + + postMessage(message, transferables) { + this.onMessageCallback.forEach(callback => + createMockMessageEvent(message, transferables) + ); + } + + start() {} + close() {} +} + +export function createMockMessagePort() { + return new MockMessagePort(); +} + +export function mockAddEventListenerFunction(context) { + return (eventName, callback) => { + if (eventName !== "message") { + context.originalAddEventListener(eventName, callback); + return; + } + + context.messageCallbacks.push(callback); + }; +} + +export function mockPostMessageFunction(context) { + return (message, target, transferables) => { + context.messageCallbacks.forEach(callback => { + callback(createMockMessageEvent(message, transferables)); + }); + }; +} From 4d0cfc827e9bae461df9c49cacc448e3c98387b2 Mon Sep 17 00:00:00 2001 From: Li Xuanji Date: Thu, 29 Nov 2018 12:15:28 +0800 Subject: [PATCH 07/11] [machine] create `lifecycle/` folder (#272) * create lifecycle/ folder * spelling * inline cf.legacy.utils.sleep * inline --- packages/cf.js/src/legacy/utils/index.ts | 4 - .../test/integration/cf-operations.spec.ts | 3 +- .../test/integration/lifecycle.spec.ts | 574 ------------------ .../test/integration/lifecycle/depositor.ts | 232 +++++++ .../integration/lifecycle/lifecycle.spec.ts | 52 ++ .../{ => lifecycle}/setup-protocol.ts | 4 +- .../lifecycle/tic-tac-toe-simulator.ts | 304 ++++++++++ 7 files changed, 592 insertions(+), 581 deletions(-) delete mode 100644 packages/machine/test/integration/lifecycle.spec.ts create mode 100644 packages/machine/test/integration/lifecycle/depositor.ts create mode 100644 packages/machine/test/integration/lifecycle/lifecycle.spec.ts rename packages/machine/test/integration/{ => lifecycle}/setup-protocol.ts (96%) create mode 100644 packages/machine/test/integration/lifecycle/tic-tac-toe-simulator.ts diff --git a/packages/cf.js/src/legacy/utils/index.ts b/packages/cf.js/src/legacy/utils/index.ts index 959f0ba62..ffc6724f4 100644 --- a/packages/cf.js/src/legacy/utils/index.ts +++ b/packages/cf.js/src/legacy/utils/index.ts @@ -18,10 +18,6 @@ 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 sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - export async function mineOneBlock(provider: ethers.providers.JsonRpcProvider) { return provider.send("evm_mine", []); } diff --git a/packages/machine/test/integration/cf-operations.spec.ts b/packages/machine/test/integration/cf-operations.spec.ts index 3e00830c4..7da3fbcc9 100644 --- a/packages/machine/test/integration/cf-operations.spec.ts +++ b/packages/machine/test/integration/cf-operations.spec.ts @@ -422,7 +422,8 @@ async function installBalanceRefund( 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 cf.legacy.utils.sleep(50); + + 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 diff --git a/packages/machine/test/integration/lifecycle.spec.ts b/packages/machine/test/integration/lifecycle.spec.ts deleted file mode 100644 index ff9e40948..000000000 --- a/packages/machine/test/integration/lifecycle.spec.ts +++ /dev/null @@ -1,574 +0,0 @@ -import * as cf from "@counterfactual/cf.js"; -import { ethers } from "ethers"; - -import { - A_PRIVATE_KEY, - B_PRIVATE_KEY, - UNUSED_FUNDED_ACCOUNT -} from "../utils/environment"; - -import { TestResponseSink } from "./test-response-sink"; -import { SetupProtocol } from "./setup-protocol"; - -/** - * 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.peer = peerB; - peerB.io.peer = peerA; - - return [peerA, peerB]; -} - -/** - * 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. - */ -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 amountBCumualtive 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, - amountBCumlative: ethers.utils.BigNumber - ) { - const cfAddr = await Depositor.installBalanceRefund( - peerA, - peerB, - amountBCumlative - ); - await Depositor.uninstallBalanceRefund( - cfAddr, - peerA, - peerB, - amountA, - amountBCumlative - ); - } - - 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.runProtocol(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 it's state - await cf.legacy.utils.sleep(50); - // 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.node.ClientActionMessage { - 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; - const installData: cf.legacy.app.InstallData = { - terms, - app, - timeout, - peerA: canon.peerA, - peerB: canon.peerB, - keyA: from, - keyB: to, - encodedAppState: "0x1234" - }; - return { - requestId: "1", - appId: "", - action: cf.legacy.node.ActionName.INSTALL, - data: installData, - multisigAddress: UNUSED_FUNDED_ACCOUNT, - toAddress: to, - fromAddress: from, - seq: 0 - }; - } - - 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 msg = Depositor.startUninstallBalanceRefundMsg( - cfAddr, - peerA.signingKey.address!, - peerB.signingKey.address!, - amountA - ); - const response = await peerA.runProtocol(msg); - 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( - appId: 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 { - appId, - requestId: "2", - action: cf.legacy.node.ActionName.UNINSTALL, - data: uninstallData, - multisigAddress: UNUSED_FUNDED_ACCOUNT, - fromAddress: from, - toAddress: to, - seq: 0 - }; - } -} - -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.runProtocol(msg); - expect(response.status).toEqual(cf.legacy.node.ResponseStatus.COMPLETED); - return TicTacToeSimulator.validateInstall(peerA, peerB); - } - - public static installMsg( - to: string, - from: string - ): cf.legacy.node.ClientActionMessage { - 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; - const installData: cf.legacy.app.InstallData = { - 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" - }; - return { - requestId: "5", - appId: "", - action: cf.legacy.node.ActionName.INSTALL, - data: installData, - multisigAddress: UNUSED_FUNDED_ACCOUNT, - toAddress: to, - fromAddress: from, - seq: 0 - }; - } - - public static async validateInstall( - peerA: TestResponseSink, - peerB: TestResponseSink - ): Promise { - await cf.legacy.utils.sleep(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 msg = TicTacToeSimulator.updateMsg( - state, - cell, - peerA.signingKey.address!, - peerB.signingKey.address!, - cfAddr - ); - const response = await peerA.runProtocol(msg); - expect(response.status).toEqual(cf.legacy.node.ResponseStatus.COMPLETED); - TicTacToeSimulator.validateMakeMove( - peerA, - peerB, - cfAddr, - state, - moveNumber - ); - await cf.legacy.utils.sleep(50); - TicTacToeSimulator.validateMakeMove( - peerB, - peerA, - cfAddr, - state, - moveNumber - ); - } - - public static updateMsg( - state: string, - cell: number, - to: string, - from: string, - cfAddr: string - ): cf.legacy.node.ClientActionMessage { - const updateData: cf.legacy.app.UpdateData = { - encodedAppState: state, - appStateHash: ethers.constants.HashZero // TODO: - }; - return { - requestId: "1", - appId: cfAddr, - action: cf.legacy.node.ActionName.UPDATE, - data: updateData, - multisigAddress: UNUSED_FUNDED_ACCOUNT, - toAddress: to, - fromAddress: from, - seq: 0 - }; - } - - 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 msg = TicTacToeSimulator.uninstallStartMsg( - cfAddr, - peerA.signingKey.address!, - ethers.utils.bigNumberify(4), - peerB.signingKey.address!, - ethers.utils.bigNumberify(0) - ); - const response = await peerA.runProtocol(msg); - expect(response.status).toEqual(cf.legacy.node.ResponseStatus.COMPLETED); - // A wins so give him 2 and subtract 2 from B - await cf.legacy.utils.sleep(50); - TicTacToeSimulator.validateUninstall( - cfAddr, - peerA, - ethers.utils.bigNumberify(12), - peerB, - ethers.utils.bigNumberify(3) - ); - } - - public static uninstallStartMsg( - cfAddr: string, - addressA: string, - amountA: ethers.utils.BigNumber, - addressB: string, - amountB: ethers.utils.BigNumber - ): cf.legacy.node.ClientActionMessage { - const uninstallData = { - peerAmounts: [ - new cf.legacy.utils.PeerBalance(addressA, amountA), - new cf.legacy.utils.PeerBalance(addressB, amountB) - ] - }; - return { - requestId: "2", - appId: cfAddr, - action: cf.legacy.node.ActionName.UNINSTALL, - data: uninstallData, - multisigAddress: UNUSED_FUNDED_ACCOUNT, - fromAddress: addressA, - toAddress: addressB, - seq: 0 - }; - } - - 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/depositor.ts b/packages/machine/test/integration/lifecycle/depositor.ts new file mode 100644 index 000000000..26ed322bf --- /dev/null +++ b/packages/machine/test/integration/lifecycle/depositor.ts @@ -0,0 +1,232 @@ +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.runProtocol(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 it's state + await new Promise(resolve => setTimeout(resolve, 50)); + // 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.node.ClientActionMessage { + 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; + const installData: cf.legacy.app.InstallData = { + terms, + app, + timeout, + peerA: canon.peerA, + peerB: canon.peerB, + keyA: from, + keyB: to, + encodedAppState: "0x1234" + }; + return { + requestId: "1", + appId: "", + action: cf.legacy.node.ActionName.INSTALL, + data: installData, + multisigAddress: UNUSED_FUNDED_ACCOUNT, + toAddress: to, + fromAddress: from, + seq: 0 + }; + } + + 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 msg = Depositor.startUninstallBalanceRefundMsg( + cfAddr, + peerA.signingKey.address!, + peerB.signingKey.address!, + amountA + ); + const response = await peerA.runProtocol(msg); + 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( + appId: 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 { + appId, + requestId: "2", + 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/lifecycle.spec.ts b/packages/machine/test/integration/lifecycle/lifecycle.spec.ts new file mode 100644 index 000000000..456ca9178 --- /dev/null +++ b/packages/machine/test/integration/lifecycle/lifecycle.spec.ts @@ -0,0 +1,52 @@ +import { + A_PRIVATE_KEY, + B_PRIVATE_KEY, +} 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.peer = peerB; + peerB.io.peer = peerA; + + return [peerA, peerB]; +} + + diff --git a/packages/machine/test/integration/setup-protocol.ts b/packages/machine/test/integration/lifecycle/setup-protocol.ts similarity index 96% rename from packages/machine/test/integration/setup-protocol.ts rename to packages/machine/test/integration/lifecycle/setup-protocol.ts index a5f3aa172..901121522 100644 --- a/packages/machine/test/integration/setup-protocol.ts +++ b/packages/machine/test/integration/lifecycle/setup-protocol.ts @@ -1,9 +1,9 @@ import * as cf from "@counterfactual/cf.js"; import { ethers } from "ethers"; -import { UNUSED_FUNDED_ACCOUNT } from "../utils/environment"; +import { UNUSED_FUNDED_ACCOUNT } from "../../utils/environment"; -import { TestResponseSink } from "./test-response-sink"; +import { TestResponseSink } from "../test-response-sink"; /** * A collection of static methods responsible for running the setup potocol diff --git a/packages/machine/test/integration/lifecycle/tic-tac-toe-simulator.ts b/packages/machine/test/integration/lifecycle/tic-tac-toe-simulator.ts new file mode 100644 index 000000000..6e363fa9e --- /dev/null +++ b/packages/machine/test/integration/lifecycle/tic-tac-toe-simulator.ts @@ -0,0 +1,304 @@ +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.runProtocol(msg); + expect(response.status).toEqual(cf.legacy.node.ResponseStatus.COMPLETED); + return TicTacToeSimulator.validateInstall(peerA, peerB); + } + + public static installMsg( + to: string, + from: string + ): cf.legacy.node.ClientActionMessage { + 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; + const installData: cf.legacy.app.InstallData = { + 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" + }; + return { + requestId: "5", + appId: "", + action: cf.legacy.node.ActionName.INSTALL, + data: installData, + multisigAddress: UNUSED_FUNDED_ACCOUNT, + toAddress: to, + fromAddress: from, + seq: 0 + }; + } + + 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 msg = TicTacToeSimulator.updateMsg( + state, + cell, + peerA.signingKey.address!, + peerB.signingKey.address!, + cfAddr + ); + const response = await peerA.runProtocol(msg); + 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 updateMsg( + state: string, + cell: number, + to: string, + from: string, + cfAddr: string + ): cf.legacy.node.ClientActionMessage { + const updateData: cf.legacy.app.UpdateData = { + encodedAppState: state, + appStateHash: ethers.constants.HashZero // TODO: + }; + return { + requestId: "1", + appId: cfAddr, + action: cf.legacy.node.ActionName.UPDATE, + data: updateData, + multisigAddress: UNUSED_FUNDED_ACCOUNT, + toAddress: to, + fromAddress: from, + seq: 0 + }; + } + + 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 msg = TicTacToeSimulator.uninstallStartMsg( + cfAddr, + peerA.signingKey.address!, + ethers.utils.bigNumberify(4), + peerB.signingKey.address!, + ethers.utils.bigNumberify(0) + ); + const response = await peerA.runProtocol(msg); + expect(response.status).toEqual(cf.legacy.node.ResponseStatus.COMPLETED); + // A wins so give him 2 and subtract 2 from B + await new Promise(resolve => setTimeout(resolve, 50)); + TicTacToeSimulator.validateUninstall( + cfAddr, + peerA, + ethers.utils.bigNumberify(12), + peerB, + ethers.utils.bigNumberify(3) + ); + } + + public static uninstallStartMsg( + cfAddr: string, + addressA: string, + amountA: ethers.utils.BigNumber, + addressB: string, + amountB: ethers.utils.BigNumber + ): cf.legacy.node.ClientActionMessage { + const uninstallData = { + peerAmounts: [ + new cf.legacy.utils.PeerBalance(addressA, amountA), + new cf.legacy.utils.PeerBalance(addressB, amountB) + ] + }; + return { + requestId: "2", + appId: cfAddr, + action: cf.legacy.node.ActionName.UNINSTALL, + data: uninstallData, + multisigAddress: UNUSED_FUNDED_ACCOUNT, + fromAddress: addressA, + toAddress: addressB, + seq: 0 + }; + } + + 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); + } +} From 3aeb9bd01cf3e9ae3c0a4ac8d362eb87217b648b Mon Sep 17 00:00:00 2001 From: Joel Alejandro Villarreal Bertoldi Date: Thu, 29 Nov 2018 11:08:42 -0300 Subject: [PATCH 08/11] [playground] Moves usage of apps-list to app-home route (#279) * playground: moved apps-list usage to app-home * playground: initialized properties for components --- .../src/components/app-home/app-home.tsx | 15 ++++++++++++++- .../src/components/app-root/app-root.tsx | 18 +++--------------- .../apps-list-item/apps-list-item.tsx | 6 +++--- .../src/components/apps-list/apps-list.tsx | 2 +- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/playground/src/components/app-home/app-home.tsx b/packages/playground/src/components/app-home/app-home.tsx index 17cd9daa4..8e13789b7 100644 --- a/packages/playground/src/components/app-home/app-home.tsx +++ b/packages/playground/src/components/app-home/app-home.tsx @@ -1,5 +1,18 @@ import { Component } from "@stencil/core"; +const apps = { + // TODO: How do we get a list of available apps? + "0x822c045f6F5e7E8090eA820E24A5f327C4E62c96": { + name: "High Roller", + url: "dapps/high-roller.html", + icon: "assets/icon/high-roller.svg" + }, + "0xd545e82792b6EF2000908F224275ED0456Cf36FA": { + name: "Tic-Tac-Toe", + url: "dapps/tic-tac-toe.html", + icon: "assets/icon/icon.png" + } +}; @Component({ tag: "app-home", styleUrl: "app-home.css", @@ -9,7 +22,7 @@ export class AppHome { render() { return (
    - Soon, we'll have some dApps for you! +
    ); } diff --git a/packages/playground/src/components/app-root/app-root.tsx b/packages/playground/src/components/app-root/app-root.tsx index 98cbdf87d..9513f56ef 100644 --- a/packages/playground/src/components/app-root/app-root.tsx +++ b/packages/playground/src/components/app-root/app-root.tsx @@ -1,18 +1,8 @@ import { Component } from "@stencil/core"; -const apps = { - // TODO: How do we get a list of available apps? - "0x822c045f6F5e7E8090eA820E24A5f327C4E62c96": { - name: "High Roller", - url: "dapps/high-roller.html", - icon: "assets/icon/high-roller.svg" - }, - "0xd545e82792b6EF2000908F224275ED0456Cf36FA": { - name: "Tic-Tac-Toe", - url: "dapps/tic-tac-toe.html", - icon: "assets/icon/icon.png" - } -}; +// @ts-ignore +// Needed due to https://github.com/ionic-team/stencil-router/issues/62 +import { MatchResults } from "@stencil/router"; @Component({ tag: "app-root", @@ -33,8 +23,6 @@ export class AppRoot { - - ); diff --git a/packages/playground/src/components/apps-list-item/apps-list-item.tsx b/packages/playground/src/components/apps-list-item/apps-list-item.tsx index ddad29e2d..b654551f8 100644 --- a/packages/playground/src/components/apps-list-item/apps-list-item.tsx +++ b/packages/playground/src/components/apps-list-item/apps-list-item.tsx @@ -6,9 +6,9 @@ import { Component, Prop } from "@stencil/core"; shadow: true }) export class AppsListItem { - @Prop() icon: string; - @Prop() name: string; - @Prop() url: string; + @Prop() icon: string = ""; + @Prop() name: string = ""; + @Prop() url: string = ""; render() { return ( diff --git a/packages/playground/src/components/apps-list/apps-list.tsx b/packages/playground/src/components/apps-list/apps-list.tsx index f7c3d9ef2..369522858 100644 --- a/packages/playground/src/components/apps-list/apps-list.tsx +++ b/packages/playground/src/components/apps-list/apps-list.tsx @@ -8,7 +8,7 @@ import { AppDefinition } from "../../types"; shadow: true }) export class AppsList { - @Prop() apps: { [s: string]: AppDefinition }; + @Prop() apps: { [s: string]: AppDefinition } = {}; public get appsList(): AppDefinition[] { return Object.keys(this.apps).map(key => this.apps[key]); From c402d2a307c73fb3418df800a187625e3bf18c76 Mon Sep 17 00:00:00 2001 From: Joel Alejandro Villarreal Bertoldi Date: Thu, 29 Nov 2018 11:38:36 -0300 Subject: [PATCH 09/11] [playground] Adds dapp-container component (#278) * playground: created dapp-container component * playground: added tests for dapp-container * playground: added EventEmitter3 * playground: fixed attribute typo in e2e test --- packages/playground/package.json | 3 +- .../dapp-container/dapp-container.e2e.ts | 14 ++ .../dapp-container/dapp-container.spec.ts | 7 + .../dapp-container/dapp-container.tsx | 126 ++++++++++++++++++ 4 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 packages/playground/src/components/dapp-container/dapp-container.e2e.ts create mode 100644 packages/playground/src/components/dapp-container/dapp-container.spec.ts create mode 100644 packages/playground/src/components/dapp-container/dapp-container.tsx diff --git a/packages/playground/package.json b/packages/playground/package.json index 16a4c700b..65dd4b2ca 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -11,7 +11,8 @@ }, "dependencies": { "@stencil/core": "~0.15.1", - "@stencil/router": "~0.3.0" + "@stencil/router": "~0.3.0", + "eventemitter3": "^3.1.0" }, "repository": { "type": "git", diff --git a/packages/playground/src/components/dapp-container/dapp-container.e2e.ts b/packages/playground/src/components/dapp-container/dapp-container.e2e.ts new file mode 100644 index 000000000..87e0c9325 --- /dev/null +++ b/packages/playground/src/components/dapp-container/dapp-container.e2e.ts @@ -0,0 +1,14 @@ +import { newE2EPage } from "@stencil/core/testing"; + +describe("dapp-container", () => { + it("renders", async () => { + const page = await newE2EPage(); + await page.setContent(''); + + const element = await page.find("dapp-container"); + expect(element).toHaveClass("hydrated"); + + const iframe = await page.find("dapp-container >>> iframe"); + expect(iframe).toEqualAttribute("src", "foo.html"); + }); +}); diff --git a/packages/playground/src/components/dapp-container/dapp-container.spec.ts b/packages/playground/src/components/dapp-container/dapp-container.spec.ts new file mode 100644 index 000000000..338213a7f --- /dev/null +++ b/packages/playground/src/components/dapp-container/dapp-container.spec.ts @@ -0,0 +1,7 @@ +import { DappContainer } from "./dapp-container"; + +describe("app", () => { + it("builds", () => { + new DappContainer(); + }); +}); diff --git a/packages/playground/src/components/dapp-container/dapp-container.tsx b/packages/playground/src/components/dapp-container/dapp-container.tsx new file mode 100644 index 000000000..be40269ce --- /dev/null +++ b/packages/playground/src/components/dapp-container/dapp-container.tsx @@ -0,0 +1,126 @@ +import { Component, Element, Prop } from "@stencil/core"; +import EventEmitter from "eventemitter3"; + +@Component({ + tag: "dapp-container", + shadow: true +}) +export class DappContainer { + @Element() private element: HTMLElement | undefined; + @Prop() url: string = ""; + + private frameWindow: Window | null = null; + private port: MessagePort | null = null; + private eventEmitter: EventEmitter = new EventEmitter(); + private messageQueue: object[] = []; + + constructor() { + this.eventEmitter.on("message", this.postOrQueueMessage.bind(this)); + } + + componentDidLoad() { + /** + * Once the component has loaded, we store a reference of the IFRAME + * element's window so we can bind the message relay system. + **/ + const element = this.element as HTMLElement; + const iframe = element.querySelector("iframe") as HTMLIFrameElement; + + this.frameWindow = iframe.contentWindow as Window; + this.frameWindow.addEventListener( + "message", + this.configureMessageChannel.bind(this) + ); + } + + /** + * Attempts to relay a message through the MessagePort. If the port + * isn't available, we store the message in `this.messageQueue` + * until the port is available. + * + * @param message {any} + */ + private postOrQueueMessage(message: any) { + if (this.port) { + this.port.postMessage(message); + } else { + this.queueMessage(message); + } + } + + /** + * Binds the port with the MessageChannel created for this dApp + * by responding to NodeProvider configuration messages. + * + * @param event {MessageEvent} + */ + private configureMessageChannel(event: MessageEvent) { + if (!this.frameWindow) { + return; + } + + if (event.data === "cf-node-provider:init") { + const { port2 } = this.configureMessagePorts(); + this.frameWindow.postMessage("cf-node-provider:port", "*", [port2]); + } + + if (event.data === "cf-node-provider:ready") { + this.flushMessageQueue(); + } + } + + /** + * Binds this end of the MessageChannel (aka `port1`) to the dApp + * container, and attachs a listener to relay messages via the + * EventEmitter. + */ + private configureMessagePorts() { + const channel = new MessageChannel(); + + this.port = channel.port1; + this.port.addEventListener("message", this.relayMessage.bind(this)); + this.port.start(); + + return channel; + } + + /** + * Echoes a message received via PostMessage through + * the EventEmitter. + * + * @param event {MessageEvent} + */ + private relayMessage(event) { + this.eventEmitter.emit("message", event.data); + } + + /** + * Echoes a message received via PostMessage through + * the EventEmitter. + * + * @param event {MessageEvent} + */ + private queueMessage(message) { + this.messageQueue.push(message); + } + + /** + * Clears the message queue and forwards any messages + * stored there through the MessagePort. + */ + private flushMessageQueue() { + if (!this.port) { + return; + } + + let message; + while ((message = this.messageQueue.shift())) { + this.port.postMessage(message); + } + } + + render() { + // tslint:disable-next-line:prettier + return ; + } +} From da7c0e7a436d5b7f5a7d8034069e1a5690112c8c Mon Sep 17 00:00:00 2001 From: Joel Alejandro Villarreal Bertoldi Date: Thu, 29 Nov 2018 15:33:03 -0300 Subject: [PATCH 10/11] [playground] Added support for opening a dApp (#285) * playground: reverted to @stencil/router: 0.2.6 * playground: moved mock app registry to independent file * playground: made clickable * playground: made open a dapp by url * playground: added "slug" property to AppDefinition --- packages/playground/package.json | 2 +- .../src/components/app-home/app-home.tsx | 26 +++++------- .../src/components/app-root/app-root.tsx | 1 + .../apps-list-item/apps-list-item.tsx | 23 ++++++++++- .../src/components/apps-list/apps-list.tsx | 14 ++++++- .../dapp-container/dapp-container.tsx | 41 +++++++++++++++---- packages/playground/src/types.ts | 1 + packages/playground/src/utils/app-list.ts | 17 ++++++++ 8 files changed, 96 insertions(+), 29 deletions(-) create mode 100644 packages/playground/src/utils/app-list.ts diff --git a/packages/playground/package.json b/packages/playground/package.json index 65dd4b2ca..c4eb4c68f 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@stencil/core": "~0.15.1", - "@stencil/router": "~0.3.0", + "@stencil/router": "~0.2.6", "eventemitter3": "^3.1.0" }, "repository": { diff --git a/packages/playground/src/components/app-home/app-home.tsx b/packages/playground/src/components/app-home/app-home.tsx index 8e13789b7..076194d1b 100644 --- a/packages/playground/src/components/app-home/app-home.tsx +++ b/packages/playground/src/components/app-home/app-home.tsx @@ -1,28 +1,24 @@ -import { Component } from "@stencil/core"; +import { Component, Prop } from "@stencil/core"; +import { RouterHistory } from "@stencil/router"; + +import apps from "../../utils/app-list"; -const apps = { - // TODO: How do we get a list of available apps? - "0x822c045f6F5e7E8090eA820E24A5f327C4E62c96": { - name: "High Roller", - url: "dapps/high-roller.html", - icon: "assets/icon/high-roller.svg" - }, - "0xd545e82792b6EF2000908F224275ED0456Cf36FA": { - name: "Tic-Tac-Toe", - url: "dapps/tic-tac-toe.html", - icon: "assets/icon/icon.png" - } -}; @Component({ tag: "app-home", styleUrl: "app-home.css", shadow: true }) export class AppHome { + @Prop() history: RouterHistory = {} as RouterHistory; + + appClickedHandler(e) { + this.history.push(e.detail.dappContainerUrl, e.detail); + } + render() { return (
    - + this.appClickedHandler(e)} />
    ); } diff --git a/packages/playground/src/components/app-root/app-root.tsx b/packages/playground/src/components/app-root/app-root.tsx index 9513f56ef..d31bfad4a 100644 --- a/packages/playground/src/components/app-root/app-root.tsx +++ b/packages/playground/src/components/app-root/app-root.tsx @@ -21,6 +21,7 @@ export class AppRoot { + diff --git a/packages/playground/src/components/apps-list-item/apps-list-item.tsx b/packages/playground/src/components/apps-list-item/apps-list-item.tsx index b654551f8..9df4e9c72 100644 --- a/packages/playground/src/components/apps-list-item/apps-list-item.tsx +++ b/packages/playground/src/components/apps-list-item/apps-list-item.tsx @@ -1,4 +1,4 @@ -import { Component, Prop } from "@stencil/core"; +import { Component, Event, EventEmitter, Prop } from "@stencil/core"; @Component({ tag: "apps-list-item", @@ -6,14 +6,33 @@ import { Component, Prop } from "@stencil/core"; shadow: true }) export class AppsListItem { + @Event() appClicked: EventEmitter = {} as EventEmitter; @Prop() icon: string = ""; @Prop() name: string = ""; @Prop() url: string = ""; + private getAppSlug() { + return this.name.toLowerCase().replace(/ /g, "-"); + } + + appClickedHandler(event) { + this.appClicked.emit(event); + } + + private openApp(event: MouseEvent) { + event.preventDefault(); + + this.appClicked.emit({ + name: this.name, + dappContainerUrl: `/dapp/${this.getAppSlug()}`, + dappUrl: this.url + }); + } + render() { return (
  • - + this.openApp(e)}>
    {this.name}
    diff --git a/packages/playground/src/components/apps-list/apps-list.tsx b/packages/playground/src/components/apps-list/apps-list.tsx index 369522858..6c46dbdf9 100644 --- a/packages/playground/src/components/apps-list/apps-list.tsx +++ b/packages/playground/src/components/apps-list/apps-list.tsx @@ -1,4 +1,4 @@ -import { Component, Prop } from "@stencil/core"; +import { Component, Event, EventEmitter, Prop } from "@stencil/core"; import { AppDefinition } from "../../types"; @@ -8,17 +8,27 @@ import { AppDefinition } from "../../types"; shadow: true }) export class AppsList { + @Event() appClicked: EventEmitter = {} as EventEmitter; @Prop() apps: { [s: string]: AppDefinition } = {}; public get appsList(): AppDefinition[] { return Object.keys(this.apps).map(key => this.apps[key]); } + appClickedHandler(event) { + this.appClicked.emit(event.detail); + } + render() { return (
      {this.appsList.map(app => ( - + this.appClickedHandler(e)} + icon={app.icon} + name={app.name} + url={app.url} + /> ))}
    ); diff --git a/packages/playground/src/components/dapp-container/dapp-container.tsx b/packages/playground/src/components/dapp-container/dapp-container.tsx index be40269ce..1e61ea067 100644 --- a/packages/playground/src/components/dapp-container/dapp-container.tsx +++ b/packages/playground/src/components/dapp-container/dapp-container.tsx @@ -1,36 +1,59 @@ import { Component, Element, Prop } from "@stencil/core"; +import { MatchResults, RouterHistory } from "@stencil/router"; import EventEmitter from "eventemitter3"; +import apps from "../../utils/app-list"; + @Component({ tag: "dapp-container", shadow: true }) export class DappContainer { @Element() private element: HTMLElement | undefined; - @Prop() url: string = ""; + + @Prop() match: MatchResults = {} as MatchResults; + @Prop() history: RouterHistory = {} as RouterHistory; + + @Prop({ mutable: true }) url: string = ""; private frameWindow: Window | null = null; private port: MessagePort | null = null; private eventEmitter: EventEmitter = new EventEmitter(); private messageQueue: object[] = []; - constructor() { - this.eventEmitter.on("message", this.postOrQueueMessage.bind(this)); + getDappUrl() { + const dappSlug = this.match.params.dappName; + for (const address in apps) { + if (dappSlug === apps[address].slug) { + return apps[address].url; + } + } + + return ""; } componentDidLoad() { + this.eventEmitter.on("message", this.postOrQueueMessage.bind(this)); + this.url = this.getDappUrl(); + /** * Once the component has loaded, we store a reference of the IFRAME * element's window so we can bind the message relay system. **/ - const element = this.element as HTMLElement; + const element = (this.element as HTMLElement).shadowRoot as ShadowRoot; const iframe = element.querySelector("iframe") as HTMLIFrameElement; - this.frameWindow = iframe.contentWindow as Window; - this.frameWindow.addEventListener( - "message", - this.configureMessageChannel.bind(this) - ); + iframe.addEventListener("load", () => { + this.frameWindow = iframe.contentWindow as Window; + + // TODO: This won't work if the dapp is in a different host. + // We need to think a way of exchanging messages to make this + // possible. + this.frameWindow.addEventListener( + "message", + this.configureMessageChannel.bind(this) + ); + }); } /** diff --git a/packages/playground/src/types.ts b/packages/playground/src/types.ts index de982b8be..63e5af960 100644 --- a/packages/playground/src/types.ts +++ b/packages/playground/src/types.ts @@ -1,5 +1,6 @@ export interface AppDefinition { name: string; + slug: string; url: string; icon: string; } diff --git a/packages/playground/src/utils/app-list.ts b/packages/playground/src/utils/app-list.ts new file mode 100644 index 000000000..b4cd2367d --- /dev/null +++ b/packages/playground/src/utils/app-list.ts @@ -0,0 +1,17 @@ +import { AppDefinition } from "../types"; + +export default { + // TODO: How do we get a list of available apps? + "0x822c045f6F5e7E8090eA820E24A5f327C4E62c96": { + name: "High Roller", + slug: "high-roller", + url: "/dapps/high-roller.html", + icon: "assets/icon/high-roller.svg" + }, + "0xd545e82792b6EF2000908F224275ED0456Cf36FA": { + name: "Tic-Tac-Toe", + slug: "tic-tac-toe", + url: "/dapps/tic-tac-toe.html", + icon: "assets/icon/icon.png" + } +} as { [index: string]: AppDefinition }; From 5a1fe09e7987256ba1d7c27d38224c3d98149adc Mon Sep 17 00:00:00 2001 From: Patience Tema Baron Date: Thu, 29 Nov 2018 14:02:27 -0500 Subject: [PATCH 11/11] [playground] add multiple apps lists (#286) * add multiple apps lists * lint * update to support utils/app-list --- packages/playground/package.json | 1 + .../app-home/{app-home.css => app-home.scss} | 0 .../src/components/app-home/app-home.tsx | 19 +- .../app-root/{app-root.css => app-root.scss} | 0 .../src/components/app-root/app-root.tsx | 2 +- .../apps-list-item/apps-list-item.css | 64 ---- .../apps-list-item/apps-list-item.e2e.ts | 16 + .../apps-list-item/apps-list-item.scss | 64 ++++ .../apps-list-item/apps-list-item.tsx | 6 +- .../{apps-list.css => apps-list.scss} | 7 +- .../src/components/apps-list/apps-list.tsx | 28 +- packages/playground/src/global/app.css | 22 +- .../playground/src/global/box-sizing.scss | 13 + packages/playground/src/global/reset.scss | 36 +++ .../playground/src/global/typography.scss | 44 +++ packages/playground/src/global/variables.scss | 20 ++ packages/playground/src/types.ts | 1 + packages/playground/stencil.config.ts | 13 +- yarn.lock | 284 ++++++++++++++++-- 19 files changed, 519 insertions(+), 121 deletions(-) rename packages/playground/src/components/app-home/{app-home.css => app-home.scss} (100%) rename packages/playground/src/components/app-root/{app-root.css => app-root.scss} (100%) delete mode 100644 packages/playground/src/components/apps-list-item/apps-list-item.css create mode 100644 packages/playground/src/components/apps-list-item/apps-list-item.scss rename packages/playground/src/components/apps-list/{apps-list.css => apps-list.scss} (60%) create mode 100644 packages/playground/src/global/box-sizing.scss create mode 100644 packages/playground/src/global/reset.scss create mode 100644 packages/playground/src/global/typography.scss create mode 100644 packages/playground/src/global/variables.scss diff --git a/packages/playground/package.json b/packages/playground/package.json index c4eb4c68f..8eeef71de 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@stencil/core": "~0.15.1", + "@stencil/sass": "^0.1.1", "@stencil/router": "~0.2.6", "eventemitter3": "^3.1.0" }, diff --git a/packages/playground/src/components/app-home/app-home.css b/packages/playground/src/components/app-home/app-home.scss similarity index 100% rename from packages/playground/src/components/app-home/app-home.css rename to packages/playground/src/components/app-home/app-home.scss diff --git a/packages/playground/src/components/app-home/app-home.tsx b/packages/playground/src/components/app-home/app-home.tsx index 076194d1b..5400fec3d 100644 --- a/packages/playground/src/components/app-home/app-home.tsx +++ b/packages/playground/src/components/app-home/app-home.tsx @@ -3,9 +3,19 @@ import { RouterHistory } from "@stencil/router"; import apps from "../../utils/app-list"; +const runningAppKey = Object.keys(apps)[0]; +const runningApps = { + [runningAppKey]: Object.assign( + { + notifications: 11 + }, + apps[runningAppKey] + ) +}; + @Component({ tag: "app-home", - styleUrl: "app-home.css", + styleUrl: "app-home.scss", shadow: true }) export class AppHome { @@ -18,7 +28,12 @@ export class AppHome { render() { return (
    - this.appClickedHandler(e)} /> + this.appClickedHandler(e)} + name="Available Apps" + /> +
    ); } diff --git a/packages/playground/src/components/app-root/app-root.css b/packages/playground/src/components/app-root/app-root.scss similarity index 100% rename from packages/playground/src/components/app-root/app-root.css rename to packages/playground/src/components/app-root/app-root.scss diff --git a/packages/playground/src/components/app-root/app-root.tsx b/packages/playground/src/components/app-root/app-root.tsx index d31bfad4a..d9f6bd6a5 100644 --- a/packages/playground/src/components/app-root/app-root.tsx +++ b/packages/playground/src/components/app-root/app-root.tsx @@ -6,7 +6,7 @@ import { MatchResults } from "@stencil/router"; @Component({ tag: "app-root", - styleUrl: "app-root.css", + styleUrl: "app-root.scss", shadow: true }) export class AppRoot { diff --git a/packages/playground/src/components/apps-list-item/apps-list-item.css b/packages/playground/src/components/apps-list-item/apps-list-item.css deleted file mode 100644 index 5800b25e8..000000000 --- a/packages/playground/src/components/apps-list-item/apps-list-item.css +++ /dev/null @@ -1,64 +0,0 @@ -.item { - position: relative; - flex-basis: calc(100% / 5 - 2rem); - margin: 1rem; -} - -.icon { - position: relative; - width: 100%; - height: 0; - padding-top: 100%; - border-radius: 24px; -} - -.icon > .notification { - position: absolute; - top: -0.25rem; - right: -0.25rem; - display: flex; - justify-content: center; - align-items: center; - width: 1.25rem; - height: 1.25rem; - border-radius: 50%; - color: var(--c-white); - font-size: var(--f-sm); - background: var(--c-red); - z-index: 2; -} - -.icon > img { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - border-radius: 24px; - object-fit: cover; - z-index: 1; -} - -.name { - display: block; - font-size: var(--f-base); - text-align: center; - font-weight: var(--f-light); - color: var(--c-darkgrey); - color: #707070; - line-height: 37.5px; -} - -@media screen and (max-width: var(--screen-sm)) { - .item { - flex-basis: calc(100% / 3 - 2rem); - } - - .icon { - border-radius: 16px; - } - - .icon > img { - border-radius: 16px; - } -} \ No newline at end of file diff --git a/packages/playground/src/components/apps-list-item/apps-list-item.e2e.ts b/packages/playground/src/components/apps-list-item/apps-list-item.e2e.ts index c791dd988..fb3dcd088 100644 --- a/packages/playground/src/components/apps-list-item/apps-list-item.e2e.ts +++ b/packages/playground/src/components/apps-list-item/apps-list-item.e2e.ts @@ -8,4 +8,20 @@ describe("apps-list-item", () => { const element = await page.find("apps-list-item"); expect(element).toHaveClass("hydrated"); }); + + it("renders a notification bubble if notifications are present", async () => { + const page = await newE2EPage(); + await page.setContent(""); + + const element = await page.find("apps-list-item >>> .notification"); + expect(element.innerText).toEqual("11"); + }); + + it("does not render a notification bubble if no notifications are present", async () => { + const page = await newE2EPage(); + await page.setContent(""); + + const element = await page.find("apps-list-item >>> .notification"); + expect(element).toBeNull(); + }); }); diff --git a/packages/playground/src/components/apps-list-item/apps-list-item.scss b/packages/playground/src/components/apps-list-item/apps-list-item.scss new file mode 100644 index 000000000..0b00f718e --- /dev/null +++ b/packages/playground/src/components/apps-list-item/apps-list-item.scss @@ -0,0 +1,64 @@ +.item { + position: relative; + flex-basis: calc(100% / 5 - 2rem); + margin: 1rem; +} + +.icon { + position: relative; + width: 100%; + height: 0; + padding-top: 100%; + border-radius: 24px; + + > .notification { + position: absolute; + top: -0.25rem; + right: -0.25rem; + display: flex; + justify-content: center; + align-items: center; + width: 1.25rem; + height: 1.25rem; + border-radius: 50%; + color: $c-white; + font-size: $f-sm; + background: $c-red; + z-index: 2; + } + + > img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 24px; + object-fit: cover; + z-index: 1; + } +} + +.name { + display: block; + @include f-base; + text-align: center; + font-weight: $f-light; + color: $c-darkgrey; + color: #707070; + line-height: 37.5px; +} + +@media screen and (max-width: $screen-sm) { + .item { + flex-basis: calc(100% / 3 - 2rem); + } + + .icon { + border-radius: 16px; + + > img { + border-radius: 16px; + } + } +} \ No newline at end of file diff --git a/packages/playground/src/components/apps-list-item/apps-list-item.tsx b/packages/playground/src/components/apps-list-item/apps-list-item.tsx index 9df4e9c72..b685d9e3c 100644 --- a/packages/playground/src/components/apps-list-item/apps-list-item.tsx +++ b/packages/playground/src/components/apps-list-item/apps-list-item.tsx @@ -2,13 +2,14 @@ import { Component, Event, EventEmitter, Prop } from "@stencil/core"; @Component({ tag: "apps-list-item", - styleUrl: "apps-list-item.css", + styleUrl: "apps-list-item.scss", shadow: true }) export class AppsListItem { @Event() appClicked: EventEmitter = {} as EventEmitter; @Prop() icon: string = ""; @Prop() name: string = ""; + @Prop() notifications: number | null = null; @Prop() url: string = ""; private getAppSlug() { @@ -34,6 +35,9 @@ export class AppsListItem {
  • this.openApp(e)}>
    + {this.notifications ? ( +
    {this.notifications}
    + ) : null} {this.name}
    {this.name} diff --git a/packages/playground/src/components/apps-list/apps-list.css b/packages/playground/src/components/apps-list/apps-list.scss similarity index 60% rename from packages/playground/src/components/apps-list/apps-list.css rename to packages/playground/src/components/apps-list/apps-list.scss index 86146b74d..347203446 100644 --- a/packages/playground/src/components/apps-list/apps-list.css +++ b/packages/playground/src/components/apps-list/apps-list.scss @@ -1,3 +1,8 @@ +.title { + @include f-heading; + margin-bottom: 1.5rem; +} + .list { display: flex; margin: -1rem; @@ -5,7 +10,7 @@ list-style: none; } -@media screen and (max-width: var(--screen-sm)) { +@media screen and (max-width: $screen-sm) { .list { display: flex; margin: -1rem; diff --git a/packages/playground/src/components/apps-list/apps-list.tsx b/packages/playground/src/components/apps-list/apps-list.tsx index 6c46dbdf9..601d8d887 100644 --- a/packages/playground/src/components/apps-list/apps-list.tsx +++ b/packages/playground/src/components/apps-list/apps-list.tsx @@ -4,12 +4,13 @@ import { AppDefinition } from "../../types"; @Component({ tag: "apps-list", - styleUrl: "apps-list.css", + styleUrl: "apps-list.scss", shadow: true }) export class AppsList { @Event() appClicked: EventEmitter = {} as EventEmitter; @Prop() apps: { [s: string]: AppDefinition } = {}; + @Prop() name: string = ""; public get appsList(): AppDefinition[] { return Object.keys(this.apps).map(key => this.apps[key]); @@ -21,16 +22,21 @@ export class AppsList { render() { return ( -
      - {this.appsList.map(app => ( - this.appClickedHandler(e)} - icon={app.icon} - name={app.name} - url={app.url} - /> - ))} -
    +
    +

    {this.name}

    + +
      + {this.appsList.map(app => ( + this.appClickedHandler(e)} + icon={app.icon} + name={app.name} + notifications={app.notifications} + url={app.url} + /> + ))} +
    +
    ); } } diff --git a/packages/playground/src/global/app.css b/packages/playground/src/global/app.css index fc4835b8b..670bf2848 100644 --- a/packages/playground/src/global/app.css +++ b/packages/playground/src/global/app.css @@ -6,24 +6,4 @@ most apps will want applied to all components. Any global CSS variables should also be applied here. -*/ - - -body { - margin: 0px; - padding: 0px; - font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; -} - -:root { - --c-darkgrey: #707070; - --c-red: #FF0000; - --c-white: #FFF; - - --f-base: 1rem; - --f-sm: 0.73rem; - - --f-light: 300; - - --screen-sm: 600px; -} \ No newline at end of file +*/ \ No newline at end of file diff --git a/packages/playground/src/global/box-sizing.scss b/packages/playground/src/global/box-sizing.scss new file mode 100644 index 000000000..41b29bb19 --- /dev/null +++ b/packages/playground/src/global/box-sizing.scss @@ -0,0 +1,13 @@ +// apply a natural box layout model to all elements, but allowing components to change +html { + box-sizing: border-box; +} + +* { + box-sizing: inherit; + + &:before, + &:after { + box-sizing: inherit; + } +} diff --git a/packages/playground/src/global/reset.scss b/packages/playground/src/global/reset.scss new file mode 100644 index 000000000..75e8e8db7 --- /dev/null +++ b/packages/playground/src/global/reset.scss @@ -0,0 +1,36 @@ +html { + font-size: $f-rem; +} + +body { + margin: 0; + padding: 0; + font-family: 'Chivo', sans-serif; +} + +h1, +h2, +h3, +h4, +h5, +h6, +p { + margin: 1rem 0; +} + +ul { + margin: 1rem 0; + padding-left: 0; + list-style: none; +} + +button { + background: none; + border: 0; + cursor: pointer; +} + +a { + text-decoration: none; + color: inherit; +} \ No newline at end of file diff --git a/packages/playground/src/global/typography.scss b/packages/playground/src/global/typography.scss new file mode 100644 index 000000000..d83b32577 --- /dev/null +++ b/packages/playground/src/global/typography.scss @@ -0,0 +1,44 @@ +@mixin f-logo { + font-size: $f-logo; + font-weight: $f-bold; + text-transform: uppercase; + + @media screen and (max-width: $screen-sm) { + font-size: 0.8rem; + } +} + +.f-logo { + @include f-logo; +} + +@mixin f-heading { + font-size: $f-heading; + font-weight: $f-reg; +} + +.f-heading { + @include f-heading; +} + +@mixin f-subheading { + font-size: $f-heading; + font-weight: $f-reg; + + @media screen and (max-width: $screen-sm) { + font-size: $f-base; + } +} + +.f-subheading { + @include f-subheading; +} + +@mixin f-base { + font-size: $f-base; + font-weight: $f-light; +} + +.f-base { + @include f-base; +} \ No newline at end of file diff --git a/packages/playground/src/global/variables.scss b/packages/playground/src/global/variables.scss new file mode 100644 index 000000000..99732d855 --- /dev/null +++ b/packages/playground/src/global/variables.scss @@ -0,0 +1,20 @@ +$c-black: #101010; +$c-blue: #2F80ED; +$c-lightgrey: #E6E6E6; +$c-darkgrey: #707070; +$c-red: #FF0000; +$c-white: #FFF; + +$f-rem: 15px; + +$f-logo: 1rem; +$f-heading: 1.5rem; +$f-base: 1rem; +$f-sm: 0.73rem; + +$f-light: 300; +$f-reg: 400; +$f-bold: 700; + +$site-width: 50rem; +$screen-sm: 600px; \ No newline at end of file diff --git a/packages/playground/src/types.ts b/packages/playground/src/types.ts index 63e5af960..c1eb6457d 100644 --- a/packages/playground/src/types.ts +++ b/packages/playground/src/types.ts @@ -1,5 +1,6 @@ export interface AppDefinition { name: string; + notifications?: number; slug: string; url: string; icon: string; diff --git a/packages/playground/stencil.config.ts b/packages/playground/stencil.config.ts index a5578eff3..2bb267ea4 100644 --- a/packages/playground/stencil.config.ts +++ b/packages/playground/stencil.config.ts @@ -1,7 +1,18 @@ import { Config } from "@stencil/core"; +import { sass } from '@stencil/sass'; // https://stenciljs.com/docs/config export const config: Config = { - globalStyle: "src/global/app.css" + globalStyle: "src/global/app.css", + plugins: [ + sass({ + injectGlobalPaths: [ + 'src/global/variables.scss', + 'src/global/box-sizing.scss', + 'src/global/reset.scss', + 'src/global/typography.scss' + ] + }) + ] }; diff --git a/yarn.lock b/yarn.lock index ad95413e3..ddcc5ee53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -625,6 +625,13 @@ dependencies: "@stencil/state-tunnel" "0.0.9-1" +"@stencil/sass@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@stencil/sass/-/sass-0.1.1.tgz#5be4b174ecdda0a71895e08d4df0cf4c7c9dfdb3" + integrity sha512-vhEP1Tk+/ADjiohFvjSeNQNGUIAhL4Diolg8TiA5+H1zbtKNoZjoCVEt2+w/s846tpsOCsDZYLe7NZABMSQ1Lw== + dependencies: + node-sass "4.9.3" + "@stencil/state-tunnel@0.0.9-1": version "0.0.9-1" resolved "https://registry.yarnpkg.com/@stencil/state-tunnel/-/state-tunnel-0.0.9-1.tgz#15bcb569779fc5ca4ffbc489d57e4c076457eefd" @@ -809,7 +816,7 @@ agentkeepalive@^3.4.1: dependencies: humanize-ms "^1.2.1" -ajv@^5.1.1, ajv@^5.2.2: +ajv@^5.1.0, ajv@^5.1.1, ajv@^5.2.2: version "5.5.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" integrity sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU= @@ -1052,6 +1059,11 @@ async-eventemitter@ahultgren/async-eventemitter#fa06e39e56786ba541c180061dbf2c0a 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" + integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI= + async-limiter@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" @@ -1084,7 +1096,7 @@ aws-sign2@~0.7.0: resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= -aws4@^1.8.0: +aws4@^1.6.0, aws4@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== @@ -2026,6 +2038,11 @@ builtin-modules@^1.0.0, builtin-modules@^1.1.1: resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= +builtin-modules@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-2.0.0.tgz#60b7ef5ae6546bd7deefa74b08b62a43a232648e" + integrity sha512-3U5kUA5VPsRUA3nofm/BXX7GVHKfxz0hOBAPxXrIvHzlDRkQVqEn6yi8QJegxl4LzOHLdvb7XF5dVawa/VVYBg== + builtins@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" @@ -2188,7 +2205,7 @@ chai@^4.2.0: pathval "^1.1.0" type-detect "^4.0.5" -chalk@^1.1.3: +chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= @@ -2402,7 +2419,7 @@ columnify@^1.5.4: strip-ansi "^3.0.0" wcwidth "^1.0.0" -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@~1.0.5, combined-stream@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== @@ -2688,6 +2705,14 @@ cross-fetch@^2.1.0, cross-fetch@^2.1.1: 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" + integrity sha1-ElYDfsufDF9549bvE14wdwGEuYI= + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -3717,6 +3742,11 @@ eventemitter3@1.1.1: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.1.1.tgz#47786bdaa087caf7b1b75e73abc5c7d540158cd0" integrity sha1-R3hr2qCHyvext15zq8XH1UAVjNA= +eventemitter3@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" + integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA== + events@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88" @@ -3871,7 +3901,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@~3.0.2: +extend@~3.0.1, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -4126,7 +4156,7 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= -form-data@^2.2.0, form-data@~2.3.2: +form-data@^2.2.0, form-data@~2.3.1, form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== @@ -4274,6 +4304,13 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" +gaze@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a" + integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g== + dependencies: + globule "^1.0.0" + genfun@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" @@ -4432,7 +4469,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.2: +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== @@ -4470,6 +4507,15 @@ globby@^8.0.1: pify "^3.0.0" slash "^1.0.0" +globule@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.1.tgz#5dffb1b191f22d20797a9369b49eab4e9839696d" + integrity sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ== + dependencies: + glob "~7.1.1" + lodash "~4.17.10" + minimatch "~3.0.2" + got@7.1.0, got@^7.1.0: version "7.1.0" resolved "https://registry.npmjs.org/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a" @@ -4526,6 +4572,14 @@ har-schema@^2.0.0: resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= +har-validator@~5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" + integrity sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0= + dependencies: + ajv "^5.1.0" + har-schema "^2.0.0" + har-validator@~5.1.0: version "5.1.3" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" @@ -4827,6 +4881,11 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= +in-publish@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" + integrity sha1-4g/146KvwmkDILbcVSaCqcf631E= + indent-string@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" @@ -5118,6 +5177,11 @@ is-hex-prefixed@1.0.0: resolved "https://registry.yarnpkg.com/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz#7d8d37e6ad77e5d127148913c573e082d777f554" integrity sha1-fY035q135dEnFIkTxXPggtd39VQ= +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= + is-natural-number@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" @@ -5687,6 +5751,11 @@ jest@^23.6.0: import-local "^1.0.0" jest-cli "^23.6.0" +js-base64@^2.1.8: + version "2.4.9" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.9.tgz#748911fb04f48a60c4771b375cac45a80df11c03" + integrity sha512-xcinL3AuDJk7VSzsHgb9DvvIXayBbadtMZ4HFPx8rUszbW1MuNMlwYVC4zzCZ6e1sqZpnNS5ZFYOhXqA39T7LQ== + js-sha3@0.5.5: version "0.5.5" resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.5.5.tgz#baf0c0e8c54ad5903447df96ade7a4a1bca79a4a" @@ -6122,11 +6191,21 @@ lodash._reinterpolate@~3.0.0: resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= -lodash.assign@^4.0.3, lodash.assign@^4.0.6: +lodash.assign@^4.0.3, lodash.assign@^4.0.6, lodash.assign@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc= +lodash.clonedeep@^4.3.2: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + +lodash.mergewith@^4.6.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" + integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ== + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -6152,7 +6231,7 @@ lodash@4.17.10: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" integrity sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg== -lodash@^4.13.1, lodash@^4.14.2, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1: +lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.2, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@~4.17.10: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== @@ -6190,6 +6269,13 @@ ltgt@~2.2.0: resolved "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5" integrity sha1-81ypHEk/e3PaDgdJUwTxezH4fuU= +magic-string@^0.25.1: + version "0.25.1" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.1.tgz#b1c248b399cd7485da0fe7385c2fc7011843266e" + integrity sha512-sCuTz6pYom8Rlt4ISPFn6wuFodbKMIHUMv4Qko9P17dpxb7s52KJTmRuZZqHdGmLCK9AOcDare039nRIcfdkEg== + dependencies: + sourcemap-codec "^1.4.1" + make-dir@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" @@ -6307,7 +6393,7 @@ memorystream@^0.3.1: resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI= -meow@^3.3.0: +meow@^3.3.0, meow@^3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= @@ -6430,7 +6516,7 @@ mime-db@~1.37.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg== -mime-types@^2.1.12, mime-types@^2.1.16, mime-types@~2.1.18, mime-types@~2.1.19: +mime-types@^2.1.12, mime-types@^2.1.16, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.19: version "2.1.21" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96" integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg== @@ -6474,7 +6560,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.3, minimatch@^3.0.4: +"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -6639,7 +6725,7 @@ nan@2.10.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== -nan@^2.0.8, nan@^2.2.1, nan@^2.3.3, nan@^2.9.2: +nan@^2.0.8, nan@^2.10.0, nan@^2.2.1, nan@^2.3.3, nan@^2.9.2: version "2.11.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766" integrity sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA== @@ -6761,6 +6847,31 @@ node-pre-gyp@^0.10.0: semver "^5.3.0" tar "^4" +node-sass@4.9.3: + version "4.9.3" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.9.3.tgz#f407cf3d66f78308bb1e346b24fa428703196224" + integrity sha512-XzXyGjO+84wxyH7fV6IwBOTrEBe2f0a6SBze9QWWYR/cL74AcQUks2AsqcCZenl/Fp/JVbuEaLpgrLtocwBUww== + dependencies: + async-foreach "^0.1.3" + chalk "^1.1.1" + cross-spawn "^3.0.0" + gaze "^1.0.0" + get-stdin "^4.0.1" + glob "^7.0.3" + in-publish "^2.0.0" + lodash.assign "^4.2.0" + lodash.clonedeep "^4.3.2" + lodash.mergewith "^4.6.0" + meow "^3.7.0" + mkdirp "^0.5.1" + nan "^2.10.0" + node-gyp "^3.8.0" + npmlog "^4.0.0" + request "2.87.0" + sass-graph "^2.2.4" + stdout-stream "^1.4.0" + "true-case-path" "^1.0.2" + noms@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/noms/-/noms-0.0.0.tgz#da8ebd9f3af9d6760919b27d9cdc8092a7332859" @@ -6866,7 +6977,7 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" -"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.2, npmlog@^4.1.2: +"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2, npmlog@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== @@ -6894,6 +7005,11 @@ nwsapi@^2.0.0, nwsapi@^2.0.7: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.9.tgz#77ac0cdfdcad52b6a1151a84e73254edc33ed016" integrity sha512-nlWFSCTYQcHk/6A9FFnfhKc14c3aFhfdNBXgo8Qgi9QTBu/qg3Ww+Uiz9wMzXd1T8GFxPc2QIHB6Qtf2XFryFQ== +oauth-sign@~0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + integrity sha1-Rqarfwrq2N6unsBWV4C31O/rnUM= + oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" @@ -7613,7 +7729,7 @@ q@^1.5.1: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= -qs@6.5.2, qs@^6.4.0, qs@~6.5.2: +qs@6.5.2, qs@^6.4.0, qs@~6.5.1, qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== @@ -7965,6 +8081,32 @@ request-promise-native@^1.0.5: stealthy-require "^1.1.0" tough-cookie ">=2.3.3" +request@2.87.0: + version "2.87.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e" + integrity sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + tough-cookie "~2.3.3" + tunnel-agent "^0.6.0" + uuid "^3.1.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" @@ -8038,7 +8180,7 @@ resolve@1.1.7, resolve@1.1.x: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@1.8.1, resolve@^1.1.6, resolve@^1.3.2: +resolve@1.8.1, resolve@^1.1.6, resolve@^1.3.2, resolve@^1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" integrity sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA== @@ -8099,6 +8241,16 @@ rlp@^2.0.0: dependencies: safe-buffer "^5.1.1" +rollup-plugin-commonjs@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.2.0.tgz#4604e25069e0c78a09e08faa95dc32dec27f7c89" + integrity sha512-0RM5U4Vd6iHjL6rLvr3lKBwnPsaVml+qxOGaaNUWN1lSq6S33KhITOfHmvxV3z2vy9Mk4t0g4rNlVaJJsNQPWA== + dependencies: + estree-walker "^0.5.2" + magic-string "^0.25.1" + resolve "^1.8.1" + rollup-pluginutils "^2.3.3" + rollup-plugin-json@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/rollup-plugin-json/-/rollup-plugin-json-3.1.0.tgz#7c1daf60c46bc21021ea016bd00863561a03321b" @@ -8106,6 +8258,15 @@ rollup-plugin-json@^3.1.0: dependencies: rollup-pluginutils "^2.3.1" +rollup-plugin-node-resolve@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.4.0.tgz#908585eda12e393caac7498715a01e08606abc89" + integrity sha512-PJcd85dxfSBWih84ozRtBkB731OjXk0KnzN0oGp7WOWcarAFkVa71cV5hTJg2qpVsV2U8EUwrzHP3tvy9vS3qg== + dependencies: + builtin-modules "^2.0.0" + is-module "^1.0.0" + resolve "^1.1.6" + rollup-plugin-typescript2@^0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.18.0.tgz#ab3f7003f47c3858810d516e4d3fada546bfb1df" @@ -8116,7 +8277,7 @@ rollup-plugin-typescript2@^0.18.0: rollup-pluginutils "2.3.3" tslib "1.9.3" -rollup-pluginutils@2.3.3, rollup-pluginutils@^2.3.1: +rollup-pluginutils@2.3.3, rollup-pluginutils@^2.3.1, rollup-pluginutils@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.3.3.tgz#3aad9b1eb3e7fe8262820818840bf091e5ae6794" integrity sha512-2XZwja7b6P5q4RZ5FhyX1+f46xi1Z3qBKigLRZ6VTZjwbN0K1IFGMlwm06Uu0Emcre2Z63l77nq/pzn+KxIEoA== @@ -8132,6 +8293,14 @@ rollup@^0.67.0: "@types/estree" "0.0.39" "@types/node" "*" +rollup@^0.67.3: + version "0.67.3" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.67.3.tgz#55475b1b62c43220c3b4bd7edc5846233932f50b" + integrity sha512-TyNQCz97rKuVVbsKUTXfwIjV7UljWyTVd7cTMuE+aqlQ7WJslkYF5QaYGjMLR2BlQtUOO5CAxSVnpQ55iYp5jg== + dependencies: + "@types/estree" "0.0.39" + "@types/node" "*" + rsvp@^3.3.3: version "3.6.2" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" @@ -8208,6 +8377,16 @@ sane@^2.0.0: optionalDependencies: fsevents "^1.2.3" +sass-graph@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" + integrity sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k= + dependencies: + glob "^7.0.0" + lodash "^4.0.0" + scss-tokenizer "^0.2.3" + yargs "^7.0.0" + sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -8240,6 +8419,14 @@ scryptsy@^1.2.1: dependencies: pbkdf2 "^3.0.3" +scss-tokenizer@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" + integrity sha1-jrBtualyMzOCTT9VMGQRSYR85dE= + dependencies: + js-base64 "^2.1.8" + source-map "^0.4.2" + secp256k1@^3.0.1: version "3.5.2" resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-3.5.2.tgz#f95f952057310722184fe9c914e6b71281f2f2ae" @@ -8695,6 +8882,13 @@ source-map-url@^0.4.0: resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= +source-map@^0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + integrity sha1-66T12pwNyZneaAMti092FzZSA2s= + dependencies: + amdefine ">=0.0.4" + source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -8712,6 +8906,11 @@ source-map@~0.2.0: dependencies: amdefine ">=0.0.4" +sourcemap-codec@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.4.tgz#c63ea927c029dd6bd9a2b7fa03b3fec02ad56e9f" + integrity sha512-CYAPYdBu34781kLHkaW3m6b/uUSyMOC2R61gcYMWooeuaGtjof86ZA/8T+qVPPt7np1085CR9hmMGrySwEc8Xg== + spdx-correct@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.2.tgz#19bb409e91b47b1ad54159243f7312a858db3c2e" @@ -8809,6 +9008,13 @@ statuses@~1.4.0: resolved "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== +stdout-stream@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de" + integrity sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA== + dependencies: + readable-stream "^2.0.1" + stealthy-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" @@ -8840,7 +9046,7 @@ string-length@^2.0.0: astral-regex "^1.0.0" strip-ansi "^4.0.0" -string-width@^1.0.1: +string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= @@ -9233,6 +9439,13 @@ tough-cookie@>=2.3.3, tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2. psl "^1.1.24" punycode "^1.4.1" +tough-cookie@~2.3.3: + version "2.3.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" + integrity sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA== + dependencies: + punycode "^1.4.1" + tr46@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" @@ -9275,6 +9488,13 @@ trim@0.0.1: resolved "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" integrity sha1-WFhUf2spB1fulczMZm+1AITEYN0= +"true-case-path@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d" + integrity sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew== + dependencies: + glob "^7.1.2" + truffle-blockchain-utils@^0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/truffle-blockchain-utils/-/truffle-blockchain-utils-0.0.5.tgz#a4e5c064dadd69f782a137f3d276d21095da7a47" @@ -9726,7 +9946,7 @@ uuid@^2.0.1: resolved "http://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" integrity sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho= -uuid@^3.0.1, uuid@^3.3.2: +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" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== @@ -10338,6 +10558,13 @@ yargs-parser@^2.4.1: camelcase "^3.0.0" lodash.assign "^4.0.6" +yargs-parser@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" + integrity sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo= + dependencies: + camelcase "^3.0.0" + yargs-parser@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.1.0.tgz#f1376a33b6629a5d063782944da732631e966950" @@ -10426,6 +10653,25 @@ yargs@^4.6.0, yargs@^4.7.1: y18n "^3.2.1" yargs-parser "^2.4.1" +yargs@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" + integrity sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg= + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^5.0.0" + yauzl@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005"