From 0918403024b7e9ee5b5dde63a043be5594ac729d Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Fri, 23 Nov 2018 14:46:24 +0100 Subject: [PATCH] split discovery actions to coin specific files --- src/actions/DiscoveryActions.js | 135 ++++---- .../ethereum/EthereumDiscoveryActions.js | 90 ++++++ src/actions/ripple/RippleDiscoveryActions.js | 292 ++---------------- src/reducers/AccountsReducer.js | 1 + 4 files changed, 176 insertions(+), 342 deletions(-) create mode 100644 src/actions/ethereum/EthereumDiscoveryActions.js diff --git a/src/actions/DiscoveryActions.js b/src/actions/DiscoveryActions.js index 3c497f2e..57330eda 100644 --- a/src/actions/DiscoveryActions.js +++ b/src/actions/DiscoveryActions.js @@ -1,7 +1,6 @@ /* @flow */ import TrezorConnect from 'trezor-connect'; -import EthereumjsUtil from 'ethereumjs-util'; import * as DISCOVERY from 'actions/constants/discovery'; import * as ACCOUNT from 'actions/constants/account'; import * as NOTIFICATION from 'actions/constants/notification'; @@ -14,29 +13,25 @@ import type { GetState, Dispatch, TrezorDevice, + Account, } from 'flowtype'; import type { Discovery, State } from 'reducers/DiscoveryReducer'; import * as BlockchainActions from './BlockchainActions'; +import * as EthereumDiscoveryActions from './ethereum/EthereumDiscoveryActions'; +import * as RippleDiscoveryActions from './ripple/RippleDiscoveryActions'; -export type DiscoveryStartAction = { - type: typeof DISCOVERY.START, - device: TrezorDevice, - network: string, - publicKey: string, - chainCode: string, - basePath: Array, -} +export type DiscoveryStartAction = EthereumDiscoveryActions.DiscoveryStartAction | RippleDiscoveryActions.DiscoveryStartAction; export type DiscoveryWaitingAction = { type: typeof DISCOVERY.WAITING_FOR_DEVICE | typeof DISCOVERY.WAITING_FOR_BLOCKCHAIN, device: TrezorDevice, - network: string + network: string, } export type DiscoveryCompleteAction = { type: typeof DISCOVERY.COMPLETE, device: TrezorDevice, - network: string + network: string, } export type DiscoveryAction = { @@ -122,44 +117,40 @@ const start = (device: TrezorDevice, network: string, ignoreCompleted?: boolean) // first iteration // generate public key for this account // start discovery process -const begin = (device: TrezorDevice, network: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { +const begin = (device: TrezorDevice, networkName: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { const { config } = getState().localStorage; - const networkData = config.networks.find(c => c.shortcut === network); - if (!networkData) return; + const network = config.networks.find(c => c.shortcut === networkName); + if (!network) return; dispatch({ type: DISCOVERY.WAITING_FOR_DEVICE, device, - network, + network: networkName, }); - // get xpub from TREZOR - const response = await TrezorConnect.getPublicKey({ - device: { - path: device.path, - instance: device.instance, - state: device.state, - }, - path: networkData.bip44, - keepSession: true, // acquire and hold session - //useEmptyPassphrase: !device.instance, - useEmptyPassphrase: device.useEmptyPassphrase, - }); + let startAction: DiscoveryStartAction; - // handle TREZOR response error - if (!response.success) { + try { + if (network.type === 'ethereum') { + startAction = await dispatch(EthereumDiscoveryActions.begin(device, network)); + } else if (network.type === 'ripple') { + startAction = await dispatch(RippleDiscoveryActions.begin(device, network)); + } else { + throw new Error(`DiscoveryActions.begin: Unknown network type: ${network.type}`); + } + } catch (error) { dispatch({ type: NOTIFICATION.ADD, payload: { type: 'error', title: 'Discovery error', - message: response.payload.error, + message: error.message, cancelable: true, actions: [ { label: 'Try again', callback: () => { - dispatch(start(device, network)); + dispatch(start(device, networkName)); }, }, ], @@ -174,62 +165,27 @@ const begin = (device: TrezorDevice, network: string): AsyncAction => async (dis const discoveryProcess = getState().discovery.find(d => d.deviceState === device.state && d.network === network); if (dispatch(isProcessInterrupted(discoveryProcess))) return; - const basePath: Array = response.payload.path; - // send data to reducer - dispatch({ - type: DISCOVERY.START, - network, - device, - publicKey: response.payload.publicKey, - chainCode: response.payload.chainCode, - basePath, - }); + dispatch(startAction); // next iteration - dispatch(start(device, network)); + dispatch(start(device, networkName)); }; -const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch): Promise => { - const { completed } = discoveryProcess; - - const derivedKey = discoveryProcess.hdKey.derive(`m/${discoveryProcess.accountIndex}`); - const path = discoveryProcess.basePath.concat(discoveryProcess.accountIndex); - const publicAddress: string = EthereumjsUtil.publicToAddress(derivedKey.publicKey, true).toString('hex'); - const ethAddress: string = EthereumjsUtil.toChecksumAddress(publicAddress); - const { network } = discoveryProcess; +const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { + const { config } = getState().localStorage; + const network = config.networks.find(c => c.shortcut === discoveryProcess.network); + if (!network) return; - // TODO: check if address was created before + const { completed, accountIndex } = discoveryProcess; + let account: Account; try { - const account = await dispatch(BlockchainActions.discoverAccount(device, ethAddress, network)); - // check for interruption - if (dispatch(isProcessInterrupted(discoveryProcess))) return; - - // const accountIsEmpty = account.transactions <= 0 && account.nonce <= 0 && account.balance === '0'; - const accountIsEmpty = account.nonce <= 0 && account.balance === '0'; - if (!accountIsEmpty || (accountIsEmpty && completed) || (accountIsEmpty && discoveryProcess.accountIndex === 0)) { - dispatch({ - type: ACCOUNT.CREATE, - payload: { - index: discoveryProcess.accountIndex, - loaded: true, - network, - deviceID: device.features ? device.features.device_id : '0', - deviceState: device.state || '0', - addressPath: path, - address: ethAddress, - balance: account.balance, - nonce: account.nonce, - block: account.block, - transactions: account.transactions, - }, - }); - } - - if (accountIsEmpty) { - dispatch(finish(device, discoveryProcess)); - } else if (!completed) { - dispatch(discoverAccount(device, discoveryProcess)); + if (network.type === 'ethereum') { + account = await dispatch(EthereumDiscoveryActions.discoverAccount(device, discoveryProcess)); + } else if (network.type === 'ripple') { + account = await dispatch(RippleDiscoveryActions.discoverAccount(device, discoveryProcess)); + } else { + throw new Error(`DiscoveryActions.discoverAccount: Unknown network type: ${network.type}`); } } catch (error) { dispatch({ @@ -254,6 +210,23 @@ const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): Asy ], }, }); + return; + } + + if (dispatch(isProcessInterrupted(discoveryProcess))) return; + + const accountIsEmpty = account.empty; + if (!accountIsEmpty || (accountIsEmpty && completed) || (accountIsEmpty && accountIndex === 0)) { + dispatch({ + type: ACCOUNT.CREATE, + payload: account, + }); + } + + if (accountIsEmpty) { + dispatch(finish(device, discoveryProcess)); + } else if (!completed) { + dispatch(discoverAccount(device, discoveryProcess)); } }; @@ -269,7 +242,7 @@ const finish = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction useEmptyPassphrase: device.useEmptyPassphrase, }); - await dispatch(BlockchainActions.subscribe(discoveryProcess.network)); + // await dispatch(BlockchainActions.subscribe(discoveryProcess.network)); if (dispatch(isProcessInterrupted(discoveryProcess))) return; diff --git a/src/actions/ethereum/EthereumDiscoveryActions.js b/src/actions/ethereum/EthereumDiscoveryActions.js new file mode 100644 index 00000000..ec88fbd7 --- /dev/null +++ b/src/actions/ethereum/EthereumDiscoveryActions.js @@ -0,0 +1,90 @@ +/* @flow */ + +import TrezorConnect from 'trezor-connect'; +import EthereumjsUtil from 'ethereumjs-util'; +import * as DISCOVERY from 'actions/constants/discovery'; + +import type { + PromiseAction, + Dispatch, + TrezorDevice, + Network, + Account, +} from 'flowtype'; +import type { Discovery } from 'reducers/DiscoveryReducer'; +import * as BlockchainActions from '../BlockchainActions'; + +export type DiscoveryStartAction = { + type: typeof DISCOVERY.START, + networkType: 'ethereum', + network: Network, + device: TrezorDevice, + publicKey: string, + chainCode: string, + basePath: Array, +}; + +// first iteration +// generate public key for this account +// start discovery process +export const begin = (device: TrezorDevice, network: Network): PromiseAction => async (): Promise => { + // get xpub from TREZOR + const response = await TrezorConnect.getPublicKey({ + device: { + path: device.path, + instance: device.instance, + state: device.state, + }, + path: network.bip44, + keepSession: true, // acquire and hold session + //useEmptyPassphrase: !device.instance, + useEmptyPassphrase: device.useEmptyPassphrase, + network: network.name, + }); + + // handle TREZOR response error + if (!response.success) { + throw new Error(response.payload.error); + } + + const basePath: Array = response.payload.path; + + return { + type: DISCOVERY.START, + networkType: 'ethereum', + network, + device, + publicKey: response.payload.publicKey, + chainCode: response.payload.chainCode, + basePath, + }; +}; + +export const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): PromiseAction => async (dispatch: Dispatch): Promise => { + const derivedKey = discoveryProcess.hdKey.derive(`m/${discoveryProcess.accountIndex}`); + const path = discoveryProcess.basePath.concat(discoveryProcess.accountIndex); + const publicAddress: string = EthereumjsUtil.publicToAddress(derivedKey.publicKey, true).toString('hex'); + const ethAddress: string = EthereumjsUtil.toChecksumAddress(publicAddress); + const { network } = discoveryProcess; + + // TODO: check if address was created before + const account = await dispatch(BlockchainActions.discoverAccount(device, ethAddress, network)); + + // const accountIsEmpty = account.transactions <= 0 && account.nonce <= 0 && account.balance === '0'; + const empty = account.nonce <= 0 && account.balance === '0'; + + return { + index: discoveryProcess.accountIndex, + loaded: true, + network, + deviceID: device.features ? device.features.device_id : '0', + deviceState: device.state || '0', + addressPath: path, + address: ethAddress, + balance: account.balance, + nonce: account.nonce, + block: account.block, + transactions: account.transactions, + empty, + }; +}; \ No newline at end of file diff --git a/src/actions/ripple/RippleDiscoveryActions.js b/src/actions/ripple/RippleDiscoveryActions.js index 0af09faa..d1952e6a 100644 --- a/src/actions/ripple/RippleDiscoveryActions.js +++ b/src/actions/ripple/RippleDiscoveryActions.js @@ -2,166 +2,37 @@ import TrezorConnect from 'trezor-connect'; import * as DISCOVERY from 'actions/constants/discovery'; -import * as ACCOUNT from 'actions/constants/account'; -import * as NOTIFICATION from 'actions/constants/notification'; import type { - ThunkAction, - AsyncAction, PromiseAction, - PayloadAction, GetState, Dispatch, TrezorDevice, Network, + Account, } from 'flowtype'; -import type { Discovery, State } from 'reducers/DiscoveryReducer'; -import * as BlockchainActions from '../BlockchainActions'; +import type { Discovery } from 'reducers/DiscoveryReducer'; export type DiscoveryStartAction = { type: typeof DISCOVERY.START, - device: TrezorDevice, + networkType: 'ripple', network: Network, - publicKey: string, - chainCode: string, - basePath: Array, -} - -export type DiscoveryWaitingAction = { - type: typeof DISCOVERY.WAITING_FOR_DEVICE | typeof DISCOVERY.WAITING_FOR_BLOCKCHAIN, - device: TrezorDevice, - network: string, -} - -export type DiscoveryCompleteAction = { - type: typeof DISCOVERY.COMPLETE, device: TrezorDevice, - network: string, -} - -export type DiscoveryAction = { - type: typeof DISCOVERY.FROM_STORAGE, - payload: State -} | { - type: typeof DISCOVERY.STOP, - device: TrezorDevice -} | DiscoveryStartAction - | DiscoveryWaitingAction - | DiscoveryCompleteAction; - -// There are multiple async methods during discovery process (trezor-connect and blockchain actions) -// This method will check after each of async action if process was interrupted (for example by network change or device disconnect) -const isProcessInterrupted = (process?: Discovery): PayloadAction => (dispatch: Dispatch, getState: GetState): boolean => { - if (!process) { - return false; - } - const { deviceState, network } = process; - const discoveryProcess: ?Discovery = getState().discovery.find(d => d.deviceState === deviceState && d.network === network); - if (!discoveryProcess) return false; - return discoveryProcess.interrupted; -}; - -// Private action -// Called from "this.begin", "this.restore", "this.addAccount" -const start = (device: TrezorDevice, network: string, ignoreCompleted?: boolean): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const selected = getState().wallet.selectedDevice; - if (!selected) { - // TODO: throw error - console.error('Start discovery: no selected device', device); - return; - } if (selected.path !== device.path) { - console.error('Start discovery: requested device is not selected', device, selected); - return; - } if (!selected.state) { - console.warn("Start discovery: Selected device wasn't authenticated yet..."); - return; - } if (selected.connected && !selected.available) { - console.warn('Start discovery: Selected device is unavailable...'); - return; - } - - const { discovery } = getState(); - const discoveryProcess: ?Discovery = discovery.find(d => d.deviceState === device.state && d.network === network); - - if (!selected.connected && (!discoveryProcess || !discoveryProcess.completed)) { - dispatch({ - type: DISCOVERY.WAITING_FOR_DEVICE, - device, - network, - }); - return; - } - - const blockchain = getState().blockchain.find(b => b.shortcut === network); - if (blockchain && !blockchain.connected && (!discoveryProcess || !discoveryProcess.completed)) { - dispatch({ - type: DISCOVERY.WAITING_FOR_BLOCKCHAIN, - device, - network, - }); - return; - } - - if (!discoveryProcess) { - dispatch(begin(device, network)); - } else if (discoveryProcess.completed && !ignoreCompleted) { - dispatch({ - type: DISCOVERY.COMPLETE, - device, - network, - }); - } else if (discoveryProcess.interrupted || discoveryProcess.waitingForDevice || discoveryProcess.waitingForBlockchain) { - // discovery cycle was interrupted - // start from beginning - dispatch(begin(device, network)); - } else { - dispatch(discoverAccount(device, discoveryProcess)); - } }; -// first iteration -// generate public key for this account -// start discovery process -const begin = (device: TrezorDevice, networkName: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const { config } = getState().localStorage; - const network = config.networks.find(c => c.shortcut === networkName); - if (!network) return; - - dispatch({ - type: DISCOVERY.WAITING_FOR_DEVICE, - device, - network: networkName, - }); - - // check for interruption - // corner case: DISCOVERY.START wasn't called yet, but Discovery exists in reducer created by DISCOVERY.WAITING_FOR_DEVICE action - // this is why we need to get process instance directly from reducer - const discoveryProcess = getState().discovery.find(d => d.deviceState === device.state && d.network === network); - if (dispatch(isProcessInterrupted(discoveryProcess))) return; +export const begin = (device: TrezorDevice, network: Network): PromiseAction => async (): Promise => ({ + type: DISCOVERY.START, + networkType: 'ripple', + network, + device, +}); - //const basePath: Array = response.payload.path; - - // send data to reducer - dispatch({ - type: DISCOVERY.START, - network, - device, - publicKey: '', - chainCode: '', - basePath: [0, 0, 0], - }); - - // next iteration - dispatch(start(device, networkName)); -}; - - -const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { +export const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): PromiseAction => async (dispatch: Dispatch, getState: GetState): Promise => { const { config } = getState().localStorage; const network = config.networks.find(c => c.shortcut === discoveryProcess.network); - if (!network) return; + if (!network) throw new Error('Discovery network not found'); - const { completed, accountIndex } = discoveryProcess; + const { accountIndex } = discoveryProcess; const path = network.bip44.slice(0).replace('a', accountIndex.toString()); // $FlowIssue npm not released yet @@ -180,127 +51,26 @@ const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): Asy useEmptyPassphrase: device.useEmptyPassphrase, }); + // handle TREZOR response error if (!response.success) { - dispatch({ - type: DISCOVERY.STOP, - device, - }); - - dispatch({ - type: NOTIFICATION.ADD, - payload: { - type: 'error', - title: 'Account discovery error', - message: response.payload.error, - cancelable: true, - actions: [ - { - label: 'Try again', - callback: () => { - dispatch(start(device, discoveryProcess.network)); - }, - }, - ], - }, - }); - return; + throw new Error(response.payload.error); } - if (dispatch(isProcessInterrupted(discoveryProcess))) return; - const account = response.payload; - const accountIsEmpty = account.sequence <= 0 && account.balance === '0'; - if (!accountIsEmpty || (accountIsEmpty && completed) || (accountIsEmpty && discoveryProcess.accountIndex === 0)) { - dispatch({ - type: ACCOUNT.CREATE, - payload: { - index: discoveryProcess.accountIndex, - loaded: true, - network: network.shortcut, - deviceID: device.features ? device.features.device_id : '0', - deviceState: device.state || '0', - addressPath: account.path, - address: account.address, - balance: account.balance, - nonce: account.sequence, - block: account.block, - transactions: account.transactions, - }, - }); - } - - if (accountIsEmpty) { - dispatch(finish(device, discoveryProcess)); - } else if (!completed) { - dispatch(discoverAccount(device, discoveryProcess)); - } -}; - -const finish = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch): Promise => { - await TrezorConnect.getFeatures({ - device: { - path: device.path, - instance: device.instance, - state: device.state, - }, - keepSession: false, - // useEmptyPassphrase: !device.instance, - useEmptyPassphrase: device.useEmptyPassphrase, - }); - - // await dispatch(BlockchainActions.subscribe(discoveryProcess.network)); - - if (dispatch(isProcessInterrupted(discoveryProcess))) return; - - dispatch({ - type: DISCOVERY.COMPLETE, - device, - network: discoveryProcess.network, - }); -}; - -export const reconnect = (network: string): PromiseAction => async (dispatch: Dispatch): Promise => { - await dispatch(BlockchainActions.subscribe(network)); - dispatch(restore()); -}; - -// Called after DEVICE.CONNECT ('trezor-connect') or CONNECT.AUTH_DEVICE actions in WalletService -// OR after BlockchainSubscribe in this.reconnect -export const restore = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - // check if current url has "network" parameter - const urlParams = getState().router.location.state; - if (!urlParams.network) return; - - // make sure that "selectedDevice" exists - const selected = getState().wallet.selectedDevice; - if (!selected) return; - - // find discovery process for requested network - const discoveryProcess: ?Discovery = getState().discovery.find(d => d.deviceState === selected.state && d.network === urlParams.network); - - // if there was no process befor OR process was interrupted/waiting - const shouldStart = !discoveryProcess || (discoveryProcess.interrupted || discoveryProcess.waitingForDevice || discoveryProcess.waitingForBlockchain); - if (shouldStart) { - dispatch(start(selected, urlParams.network)); - } -}; - -export const stop = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const device: ?TrezorDevice = getState().wallet.selectedDevice; - if (!device) return; - - // get all uncompleted discovery processes which assigned to selected device - const discoveryProcesses = getState().discovery.filter(d => d.deviceState === device.state && !d.completed); - if (discoveryProcesses.length > 0) { - dispatch({ - type: DISCOVERY.STOP, - device, - }); - } -}; - -export const addAccount = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const selected = getState().wallet.selectedDevice; - if (!selected) return; - dispatch(start(selected, getState().router.location.state.network, true)); -}; + const empty = account.sequence <= 0 && account.balance === '0'; + + return { + index: discoveryProcess.accountIndex, + loaded: true, + network: network.shortcut, + deviceID: device.features ? device.features.device_id : '0', + deviceState: device.state || '0', + addressPath: account.path, + address: account.address, + balance: account.balance, + nonce: account.sequence, + block: account.block, + transactions: account.transactions, + empty, + }; +}; \ No newline at end of file diff --git a/src/reducers/AccountsReducer.js b/src/reducers/AccountsReducer.js index 48883d61..480fa76a 100644 --- a/src/reducers/AccountsReducer.js +++ b/src/reducers/AccountsReducer.js @@ -23,6 +23,7 @@ export type Account = { nonce: number; block: number; transactions: number; + empty: boolean; } export type State = Array;