1
0
mirror of https://github.com/trezor/trezor-wallet synced 2024-11-30 20:28:09 +00:00

split discovery actions to coin specific files

This commit is contained in:
Szymon Lesisz 2018-11-23 14:46:24 +01:00
parent 198dd4e9c7
commit 0918403024
4 changed files with 175 additions and 341 deletions

View File

@ -1,7 +1,6 @@
/* @flow */ /* @flow */
import TrezorConnect from 'trezor-connect'; import TrezorConnect from 'trezor-connect';
import EthereumjsUtil from 'ethereumjs-util';
import * as DISCOVERY from 'actions/constants/discovery'; import * as DISCOVERY from 'actions/constants/discovery';
import * as ACCOUNT from 'actions/constants/account'; import * as ACCOUNT from 'actions/constants/account';
import * as NOTIFICATION from 'actions/constants/notification'; import * as NOTIFICATION from 'actions/constants/notification';
@ -14,29 +13,25 @@ import type {
GetState, GetState,
Dispatch, Dispatch,
TrezorDevice, TrezorDevice,
Account,
} from 'flowtype'; } from 'flowtype';
import type { Discovery, State } from 'reducers/DiscoveryReducer'; import type { Discovery, State } from 'reducers/DiscoveryReducer';
import * as BlockchainActions from './BlockchainActions'; import * as BlockchainActions from './BlockchainActions';
import * as EthereumDiscoveryActions from './ethereum/EthereumDiscoveryActions';
import * as RippleDiscoveryActions from './ripple/RippleDiscoveryActions';
export type DiscoveryStartAction = { export type DiscoveryStartAction = EthereumDiscoveryActions.DiscoveryStartAction | RippleDiscoveryActions.DiscoveryStartAction;
type: typeof DISCOVERY.START,
device: TrezorDevice,
network: string,
publicKey: string,
chainCode: string,
basePath: Array<number>,
}
export type DiscoveryWaitingAction = { export type DiscoveryWaitingAction = {
type: typeof DISCOVERY.WAITING_FOR_DEVICE | typeof DISCOVERY.WAITING_FOR_BLOCKCHAIN, type: typeof DISCOVERY.WAITING_FOR_DEVICE | typeof DISCOVERY.WAITING_FOR_BLOCKCHAIN,
device: TrezorDevice, device: TrezorDevice,
network: string network: string,
} }
export type DiscoveryCompleteAction = { export type DiscoveryCompleteAction = {
type: typeof DISCOVERY.COMPLETE, type: typeof DISCOVERY.COMPLETE,
device: TrezorDevice, device: TrezorDevice,
network: string network: string,
} }
export type DiscoveryAction = { export type DiscoveryAction = {
@ -122,44 +117,40 @@ const start = (device: TrezorDevice, network: string, ignoreCompleted?: boolean)
// first iteration // first iteration
// generate public key for this account // generate public key for this account
// start discovery process // start discovery process
const begin = (device: TrezorDevice, network: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => { const begin = (device: TrezorDevice, networkName: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const { config } = getState().localStorage; const { config } = getState().localStorage;
const networkData = config.networks.find(c => c.shortcut === network); const network = config.networks.find(c => c.shortcut === networkName);
if (!networkData) return; if (!network) return;
dispatch({ dispatch({
type: DISCOVERY.WAITING_FOR_DEVICE, type: DISCOVERY.WAITING_FOR_DEVICE,
device, device,
network, network: networkName,
}); });
// get xpub from TREZOR let startAction: DiscoveryStartAction;
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,
});
// handle TREZOR response error try {
if (!response.success) { 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({ dispatch({
type: NOTIFICATION.ADD, type: NOTIFICATION.ADD,
payload: { payload: {
type: 'error', type: 'error',
title: 'Discovery error', title: 'Discovery error',
message: response.payload.error, message: error.message,
cancelable: true, cancelable: true,
actions: [ actions: [
{ {
label: 'Try again', label: 'Try again',
callback: () => { 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); const discoveryProcess = getState().discovery.find(d => d.deviceState === device.state && d.network === network);
if (dispatch(isProcessInterrupted(discoveryProcess))) return; if (dispatch(isProcessInterrupted(discoveryProcess))) return;
const basePath: Array<number> = response.payload.path;
// send data to reducer // send data to reducer
dispatch({ dispatch(startAction);
type: DISCOVERY.START,
network,
device,
publicKey: response.payload.publicKey,
chainCode: response.payload.chainCode,
basePath,
});
// next iteration // next iteration
dispatch(start(device, network)); dispatch(start(device, networkName));
}; };
const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch): Promise<void> => { const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const { completed } = discoveryProcess; const { config } = getState().localStorage;
const network = config.networks.find(c => c.shortcut === discoveryProcess.network);
if (!network) return;
const derivedKey = discoveryProcess.hdKey.derive(`m/${discoveryProcess.accountIndex}`); const { completed, accountIndex } = discoveryProcess;
const path = discoveryProcess.basePath.concat(discoveryProcess.accountIndex); let account: Account;
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
try { try {
const account = await dispatch(BlockchainActions.discoverAccount(device, ethAddress, network)); if (network.type === 'ethereum') {
// check for interruption account = await dispatch(EthereumDiscoveryActions.discoverAccount(device, discoveryProcess));
if (dispatch(isProcessInterrupted(discoveryProcess))) return; } else if (network.type === 'ripple') {
account = await dispatch(RippleDiscoveryActions.discoverAccount(device, discoveryProcess));
// const accountIsEmpty = account.transactions <= 0 && account.nonce <= 0 && account.balance === '0'; } else {
const accountIsEmpty = account.nonce <= 0 && account.balance === '0'; throw new Error(`DiscoveryActions.discoverAccount: Unknown network type: ${network.type}`);
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));
} }
} catch (error) { } catch (error) {
dispatch({ 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, useEmptyPassphrase: device.useEmptyPassphrase,
}); });
await dispatch(BlockchainActions.subscribe(discoveryProcess.network)); // await dispatch(BlockchainActions.subscribe(discoveryProcess.network));
if (dispatch(isProcessInterrupted(discoveryProcess))) return; if (dispatch(isProcessInterrupted(discoveryProcess))) return;

View File

@ -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<number>,
};
// first iteration
// generate public key for this account
// start discovery process
export const begin = (device: TrezorDevice, network: Network): PromiseAction<DiscoveryStartAction> => async (): Promise<DiscoveryStartAction> => {
// 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<number> = 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<Account> => async (dispatch: Dispatch): Promise<Account> => {
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,
};
};

View File

@ -2,166 +2,37 @@
import TrezorConnect from 'trezor-connect'; import TrezorConnect from 'trezor-connect';
import * as DISCOVERY from 'actions/constants/discovery'; import * as DISCOVERY from 'actions/constants/discovery';
import * as ACCOUNT from 'actions/constants/account';
import * as NOTIFICATION from 'actions/constants/notification';
import type { import type {
ThunkAction,
AsyncAction,
PromiseAction, PromiseAction,
PayloadAction,
GetState, GetState,
Dispatch, Dispatch,
TrezorDevice, TrezorDevice,
Network, Network,
Account,
} from 'flowtype'; } from 'flowtype';
import type { Discovery, State } from 'reducers/DiscoveryReducer'; import type { Discovery } from 'reducers/DiscoveryReducer';
import * as BlockchainActions from '../BlockchainActions';
export type DiscoveryStartAction = { export type DiscoveryStartAction = {
type: typeof DISCOVERY.START, type: typeof DISCOVERY.START,
device: TrezorDevice, networkType: 'ripple',
network: Network, network: Network,
publicKey: string,
chainCode: string,
basePath: Array<number>,
}
export type DiscoveryWaitingAction = {
type: typeof DISCOVERY.WAITING_FOR_DEVICE | typeof DISCOVERY.WAITING_FOR_BLOCKCHAIN,
device: TrezorDevice, 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<boolean> => (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 export const begin = (device: TrezorDevice, network: Network): PromiseAction<DiscoveryStartAction> => async (): Promise<DiscoveryStartAction> => ({
// Called from "this.begin", "this.restore", "this.addAccount" type: DISCOVERY.START,
const start = (device: TrezorDevice, network: string, ignoreCompleted?: boolean): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { networkType: 'ripple',
const selected = getState().wallet.selectedDevice; network,
if (!selected) { device,
// 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(); export const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): PromiseAction<Account> => async (dispatch: Dispatch, getState: GetState): Promise<Account> => {
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<void> => {
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;
//const basePath: Array<number> = 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<void> => {
const { config } = getState().localStorage; const { config } = getState().localStorage;
const network = config.networks.find(c => c.shortcut === discoveryProcess.network); 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()); const path = network.bip44.slice(0).replace('a', accountIndex.toString());
// $FlowIssue npm not released yet // $FlowIssue npm not released yet
@ -180,127 +51,26 @@ const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): Asy
useEmptyPassphrase: device.useEmptyPassphrase, useEmptyPassphrase: device.useEmptyPassphrase,
}); });
// handle TREZOR response error
if (!response.success) { if (!response.success) {
dispatch({ throw new Error(response.payload.error);
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;
} }
if (dispatch(isProcessInterrupted(discoveryProcess))) return;
const account = response.payload; const account = response.payload;
const accountIsEmpty = account.sequence <= 0 && account.balance === '0'; const empty = 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) { return {
dispatch(finish(device, discoveryProcess)); index: discoveryProcess.accountIndex,
} else if (!completed) { loaded: true,
dispatch(discoverAccount(device, discoveryProcess)); network: network.shortcut,
} deviceID: device.features ? device.features.device_id : '0',
}; deviceState: device.state || '0',
addressPath: account.path,
const finish = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch): Promise<void> => { address: account.address,
await TrezorConnect.getFeatures({ balance: account.balance,
device: { nonce: account.sequence,
path: device.path, block: account.block,
instance: device.instance, transactions: account.transactions,
state: device.state, empty,
}, };
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<void> => async (dispatch: Dispatch): Promise<void> => {
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));
};

View File

@ -23,6 +23,7 @@ export type Account = {
nonce: number; nonce: number;
block: number; block: number;
transactions: number; transactions: number;
empty: boolean;
} }
export type State = Array<Account>; export type State = Array<Account>;