1
0
mirror of https://github.com/trezor/trezor-wallet synced 2025-02-21 04:22:04 +00:00
trezor-wallet/src/actions/DiscoveryActions.js

405 lines
13 KiB
JavaScript
Raw Normal View History

/* @flow */
import React from 'react';
import { FormattedMessage } from 'react-intl';
import TrezorConnect, { UI } from 'trezor-connect';
2019-04-23 15:02:25 +00:00
import * as BLOCKCHAIN_ACTION from 'actions/constants/blockchain';
2018-08-14 13:18:16 +00:00
import * as DISCOVERY from 'actions/constants/discovery';
import * as ACCOUNT from 'actions/constants/account';
import * as NOTIFICATION from 'actions/constants/notification';
2018-10-04 08:51:33 +00:00
2018-08-14 14:06:34 +00:00
import type {
2018-10-04 08:51:33 +00:00
ThunkAction,
AsyncAction,
PromiseAction,
PayloadAction,
GetState,
Dispatch,
TrezorDevice,
Account,
2018-08-14 14:06:34 +00:00
} from 'flowtype';
import type { Discovery, State } from 'reducers/DiscoveryReducer';
import l10nMessages from 'components/notifications/Context/actions.messages';
import l10nCommonMessages from 'views/common.messages';
import * as BlockchainActions from './BlockchainActions';
2018-11-29 20:02:56 +00:00
import * as EthereumDiscoveryActions from './ethereum/DiscoveryActions';
import * as RippleDiscoveryActions from './ripple/DiscoveryActions';
2018-04-16 21:19:50 +00:00
2019-03-04 12:33:02 +00:00
export type DiscoveryStartAction =
| EthereumDiscoveryActions.DiscoveryStartAction
| RippleDiscoveryActions.DiscoveryStartAction;
2018-04-23 10:20:15 +00:00
export type DiscoveryWaitingAction = {
2019-03-04 12:33:02 +00:00
type:
| typeof DISCOVERY.WAITING_FOR_DEVICE
| typeof DISCOVERY.WAITING_FOR_BLOCKCHAIN
| typeof DISCOVERY.FIRMWARE_NOT_SUPPORTED
| typeof DISCOVERY.FIRMWARE_OUTDATED,
2018-04-23 10:20:15 +00:00
device: TrezorDevice,
network: string,
2019-03-04 12:33:02 +00:00
};
2018-04-23 10:20:15 +00:00
export type DiscoveryCompleteAction = {
2018-04-16 21:19:50 +00:00
type: typeof DISCOVERY.COMPLETE,
2018-04-23 10:20:15 +00:00
device: TrezorDevice,
network: string,
2019-03-04 12:33:02 +00:00
};
export type DiscoveryAction =
| {
type: typeof DISCOVERY.FROM_STORAGE,
payload: State,
}
| {
type: typeof DISCOVERY.STOP,
device: TrezorDevice,
}
| DiscoveryStartAction
| DiscoveryWaitingAction
| DiscoveryCompleteAction;
2018-07-30 10:52:13 +00:00
2018-10-04 08:51:33 +00:00
// 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)
2019-03-04 12:33:02 +00:00
const isProcessInterrupted = (process?: Discovery): PayloadAction<boolean> => (
dispatch: Dispatch,
getState: GetState
): boolean => {
2018-10-04 08:51:33 +00:00
if (!process) {
return false;
}
const { deviceState, network } = process;
2019-03-04 12:33:02 +00:00
const discoveryProcess: ?Discovery = getState().discovery.find(
d => d.deviceState === deviceState && d.network === network
);
2018-10-04 08:51:33 +00:00
if (!discoveryProcess) return false;
return discoveryProcess.interrupted;
};
// Private action
// Called from "this.begin", "this.restore", "this.addAccount"
2019-03-04 12:33:02 +00:00
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;
2019-03-04 12:33:02 +00:00
}
if (selected.path !== device.path) {
console.error('Start discovery: requested device is not selected', device, selected);
return;
2019-03-04 12:33:02 +00:00
}
if (!selected.state) {
console.warn("Start discovery: Selected device wasn't authenticated yet...");
return;
2019-03-04 12:33:02 +00:00
}
if (selected.connected && !selected.available) {
console.warn('Start discovery: Selected device is unavailable...');
return;
}
2018-09-23 07:13:09 +00:00
const { discovery } = getState();
2019-03-04 12:33:02 +00:00
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;
}
2018-10-15 13:44:10 +00:00
const blockchain = getState().blockchain.find(b => b.shortcut === network);
2018-09-13 12:40:41 +00:00
if (blockchain && !blockchain.connected && (!discoveryProcess || !discoveryProcess.completed)) {
dispatch({
type: DISCOVERY.WAITING_FOR_BLOCKCHAIN,
device,
network,
});
return;
}
if (!discoveryProcess) {
2018-09-23 07:13:09 +00:00
dispatch(begin(device, network));
} else if (discoveryProcess.completed && !ignoreCompleted) {
dispatch({
type: DISCOVERY.COMPLETE,
device,
network,
});
2019-03-04 12:33:02 +00:00
} else if (
discoveryProcess.interrupted ||
discoveryProcess.waitingForDevice ||
discoveryProcess.waitingForBlockchain
) {
// discovery cycle was interrupted
// start from beginning
dispatch(begin(device, network));
} else {
dispatch(discoverAccount(device, discoveryProcess));
}
};
2018-07-30 10:52:13 +00:00
// first iteration
// generate public key for this account
// start discovery process
2019-03-04 12:33:02 +00:00
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;
2018-07-30 10:52:13 +00:00
dispatch({
type: DISCOVERY.WAITING_FOR_DEVICE,
device,
network: networkName,
});
let startAction: DiscoveryStartAction;
try {
2018-12-11 14:08:02 +00:00
switch (network.type) {
case 'ethereum':
startAction = await dispatch(EthereumDiscoveryActions.begin(device, network));
break;
case 'ripple':
startAction = await dispatch(RippleDiscoveryActions.begin(device, network));
break;
default:
throw new Error(`DiscoveryActions.begin: Unknown network type: ${network.type}`);
}
} catch (error) {
2019-05-15 16:03:29 +00:00
console.error(error);
dispatch({
type: NOTIFICATION.ADD,
payload: {
variant: 'error',
title: <FormattedMessage {...l10nMessages.TR_ACCOUNT_DISCOVERY_ERROR} />,
message: error.message,
cancelable: true,
actions: [
{
label: <FormattedMessage {...l10nCommonMessages.TR_TRY_AGAIN} />,
callback: () => {
dispatch(start(device, networkName));
},
},
],
},
});
return;
}
2018-09-12 14:59:31 +00:00
// check for interruption
2018-10-04 08:51:33 +00:00
// 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
2019-03-04 12:33:02 +00:00
const discoveryProcess = getState().discovery.find(
d => d.deviceState === device.state && d.network === network
);
2018-10-04 08:51:33 +00:00
if (dispatch(isProcessInterrupted(discoveryProcess))) return;
2018-09-12 14:59:31 +00:00
// send data to reducer
dispatch(startAction);
// next iteration
dispatch(start(device, networkName));
};
2018-07-30 10:52:13 +00:00
2019-03-04 12:33:02 +00:00
const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (
dispatch: Dispatch,
getState: GetState
): Promise<void> => {
const { config } = getState().localStorage;
const network = config.networks.find(c => c.shortcut === discoveryProcess.network);
if (!network) return;
2018-07-30 10:52:13 +00:00
const { completed, accountIndex } = discoveryProcess;
let account: Account;
try {
2018-12-11 14:08:02 +00:00
switch (network.type) {
case 'ethereum':
2019-03-04 12:33:02 +00:00
account = await dispatch(
EthereumDiscoveryActions.discoverAccount(device, discoveryProcess)
);
2018-12-11 14:08:02 +00:00
break;
case 'ripple':
2019-03-04 12:33:02 +00:00
account = await dispatch(
RippleDiscoveryActions.discoverAccount(device, discoveryProcess)
);
2018-12-11 14:08:02 +00:00
break;
default:
2019-03-04 12:33:02 +00:00
throw new Error(
`DiscoveryActions.discoverAccount: Unknown network type: ${network.type}`
);
}
} catch (error) {
// handle not supported firmware error
if (error.message === UI.FIRMWARE_NOT_SUPPORTED) {
dispatch({
type: DISCOVERY.FIRMWARE_NOT_SUPPORTED,
device,
network: discoveryProcess.network,
});
return;
}
// handle outdated firmware error
2019-02-26 11:37:26 +00:00
if (error.message === UI.FIRMWARE_OLD) {
dispatch({
type: DISCOVERY.FIRMWARE_OUTDATED,
device,
network: discoveryProcess.network,
});
return;
}
2019-05-15 16:03:29 +00:00
console.error(error);
dispatch({
type: DISCOVERY.STOP,
2018-09-23 07:13:09 +00:00
device,
});
dispatch({
2018-07-30 10:52:13 +00:00
type: NOTIFICATION.ADD,
payload: {
variant: 'error',
title: <FormattedMessage {...l10nMessages.TR_ACCOUNT_DISCOVERY_ERROR} />,
message: error.message,
2018-07-30 10:52:13 +00:00
cancelable: true,
actions: [
{
label: <FormattedMessage {...l10nCommonMessages.TR_TRY_AGAIN} />,
2018-07-30 10:52:13 +00:00
callback: () => {
dispatch(start(device, discoveryProcess.network));
},
},
],
},
});
return;
}
if (dispatch(isProcessInterrupted(discoveryProcess))) return;
const accountIsEmpty = account.empty;
2019-03-04 12:33:02 +00:00
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));
}
};
2019-03-04 12:33:02 +00:00
const finish = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (
dispatch: Dispatch
): Promise<void> => {
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));
2018-10-04 08:51:33 +00:00
if (dispatch(isProcessInterrupted(discoveryProcess))) return;
dispatch({
type: DISCOVERY.COMPLETE,
device,
network: discoveryProcess.network,
});
2018-09-23 07:13:09 +00:00
};
2019-04-23 15:02:25 +00:00
export const reconnect = (network: string, timeout: number = 30): PromiseAction<void> => async (
2019-03-04 12:33:02 +00:00
dispatch: Dispatch
): Promise<void> => {
2019-04-23 15:02:25 +00:00
// Runs two promises.
// First promise is a subscribe action which will never resolve in case of completely lost connection to the backend
// That's why there is a second promise that rejects after the specified timeout.
return Promise.race([
dispatch(BlockchainActions.subscribe(network)),
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)),
])
.catch(() => {
// catch error from first promises that rejects (most likely timeout)
dispatch({
type: BLOCKCHAIN_ACTION.FAIL_SUBSCRIBE,
shortcut: network,
});
})
.then(() => {
// dispatch restore when subscribe promise resolves
dispatch(restore());
});
2018-09-23 07:13:09 +00:00
};
2018-09-13 12:40:41 +00:00
2018-10-04 08:51:33 +00:00
// Called after DEVICE.CONNECT ('trezor-connect') or CONNECT.AUTH_DEVICE actions in WalletService
// OR after BlockchainSubscribe in this.reconnect
2018-07-30 10:52:13 +00:00
export const restore = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
2018-10-04 08:51:33 +00:00
// check if current url has "network" parameter
const urlParams = getState().router.location.state;
if (!urlParams.network) return;
// make sure that "selectedDevice" exists
2018-07-30 10:52:13 +00:00
const selected = getState().wallet.selectedDevice;
2018-10-04 08:51:33 +00:00
if (!selected) return;
// find discovery process for requested network
2019-03-04 12:33:02 +00:00
const discoveryProcess: ?Discovery = getState().discovery.find(
d => d.deviceState === selected.state && d.network === urlParams.network
);
2018-10-04 08:51:33 +00:00
// if there was no process befor OR process was interrupted/waiting
2019-03-04 12:33:02 +00:00
const shouldStart =
!discoveryProcess ||
(discoveryProcess.interrupted ||
discoveryProcess.waitingForDevice ||
discoveryProcess.waitingForBlockchain);
2018-10-04 08:51:33 +00:00
if (shouldStart) {
dispatch(start(selected, urlParams.network));
}
2018-07-30 10:52:13 +00:00
};
2018-10-04 08:51:33 +00:00
export const stop = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
const device: ?TrezorDevice = getState().wallet.selectedDevice;
if (!device) return;
2018-07-30 10:52:13 +00:00
2018-10-04 08:51:33 +00:00
// get all uncompleted discovery processes which assigned to selected device
2019-03-04 12:33:02 +00:00
const discoveryProcesses = getState().discovery.filter(
d => d.deviceState === device.state && !d.completed
);
2018-10-04 08:51:33 +00:00
if (discoveryProcesses.length > 0) {
dispatch({
type: DISCOVERY.STOP,
device,
});
}
2018-07-30 10:52:13 +00:00
};
2018-10-04 08:51:33 +00:00
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));
};