2018-04-11 10:59:44 +00:00
|
|
|
/* @flow */
|
|
|
|
|
|
|
|
import TrezorConnect from 'trezor-connect';
|
2018-07-30 10:52:13 +00:00
|
|
|
import HDKey from 'hdkey';
|
|
|
|
import EthereumjsUtil from 'ethereumjs-util';
|
2018-08-14 13:18:16 +00:00
|
|
|
import * as DISCOVERY from 'actions/constants/discovery';
|
|
|
|
import * as ACCOUNT from 'actions/constants/account';
|
|
|
|
import * as TOKEN from 'actions/constants/token';
|
|
|
|
import * as NOTIFICATION from 'actions/constants/notification';
|
2018-07-30 10:52:13 +00:00
|
|
|
import * as AccountsActions from './AccountsActions';
|
2018-04-11 10:59:44 +00:00
|
|
|
|
|
|
|
import { getNonceAsync, getBalanceAsync, getTokenBalanceAsync } from './Web3Actions';
|
2018-04-23 10:20:15 +00:00
|
|
|
import { setBalance as setTokenBalance } from './TokenActions';
|
2018-04-11 10:59:44 +00:00
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
import type {
|
|
|
|
ThunkAction, AsyncAction, Action, GetState, Dispatch, TrezorDevice,
|
2018-08-14 12:56:47 +00:00
|
|
|
} from 'flowtype';
|
2018-07-30 10:52:13 +00:00
|
|
|
|
2018-08-14 13:18:16 +00:00
|
|
|
import type { Discovery, State } from 'reducers/DiscoveryReducer';
|
2018-04-16 21:19:50 +00:00
|
|
|
|
|
|
|
export type DiscoveryAction = {
|
2018-04-23 10:20:15 +00:00
|
|
|
type: typeof DISCOVERY.FROM_STORAGE,
|
|
|
|
payload: State
|
|
|
|
} | DiscoveryStartAction
|
|
|
|
| DiscoveryWaitingAction
|
|
|
|
| DiscoveryStopAction
|
|
|
|
| DiscoveryCompleteAction;
|
|
|
|
|
|
|
|
export type DiscoveryStartAction = {
|
2018-04-16 21:19:50 +00:00
|
|
|
type: typeof DISCOVERY.START,
|
2018-04-23 10:20:15 +00:00
|
|
|
device: TrezorDevice,
|
|
|
|
network: string,
|
2018-05-07 14:02:10 +00:00
|
|
|
publicKey: string,
|
|
|
|
chainCode: string,
|
2018-04-23 10:20:15 +00:00
|
|
|
basePath: Array<number>,
|
|
|
|
}
|
|
|
|
|
|
|
|
export type DiscoveryWaitingAction = {
|
2018-05-07 11:25:46 +00:00
|
|
|
type: typeof DISCOVERY.WAITING_FOR_DEVICE | typeof DISCOVERY.WAITING_FOR_BACKEND,
|
2018-04-23 10:20:15 +00:00
|
|
|
device: TrezorDevice,
|
|
|
|
network: string
|
|
|
|
}
|
|
|
|
|
|
|
|
export type DiscoveryStopAction = {
|
2018-04-16 21:19:50 +00:00
|
|
|
type: typeof DISCOVERY.STOP,
|
2018-04-23 10:20:15 +00:00
|
|
|
device: TrezorDevice
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2018-04-16 21:19:50 +00:00
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
export 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;
|
|
|
|
}
|
2018-05-07 11:25:46 +00:00
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
const web3 = getState().web3.find(w3 => w3.network === network);
|
|
|
|
if (!web3) {
|
|
|
|
console.error('Start discovery: Web3 does not exist', network);
|
|
|
|
return;
|
|
|
|
}
|
2018-04-11 10:59:44 +00:00
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
if (!web3.web3.currentProvider.isConnected()) {
|
|
|
|
console.error('Start discovery: Web3 is not connected', network);
|
|
|
|
dispatch({
|
|
|
|
type: DISCOVERY.WAITING_FOR_BACKEND,
|
|
|
|
device,
|
|
|
|
network,
|
|
|
|
});
|
|
|
|
return;
|
2018-04-11 10:59:44 +00:00
|
|
|
}
|
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
const discovery: State = getState().discovery;
|
|
|
|
const discoveryProcess: ?Discovery = discovery.find(d => d.deviceState === device.state && d.network === network);
|
2018-04-11 10:59:44 +00:00
|
|
|
|
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
if (!selected.connected && (!discoveryProcess || !discoveryProcess.completed)) {
|
2018-04-11 10:59:44 +00:00
|
|
|
dispatch({
|
2018-05-07 11:25:46 +00:00
|
|
|
type: DISCOVERY.WAITING_FOR_DEVICE,
|
2018-04-11 10:59:44 +00:00
|
|
|
device,
|
2018-07-30 10:52:13 +00:00
|
|
|
network,
|
2018-04-11 10:59:44 +00:00
|
|
|
});
|
2018-07-30 10:52:13 +00:00
|
|
|
return;
|
|
|
|
}
|
2018-04-11 10:59:44 +00:00
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
if (!discoveryProcess) {
|
|
|
|
dispatch(begin(device, network));
|
|
|
|
} else if (discoveryProcess.completed && !ignoreCompleted) {
|
|
|
|
dispatch({
|
|
|
|
type: DISCOVERY.COMPLETE,
|
|
|
|
device,
|
|
|
|
network,
|
2018-04-11 10:59:44 +00:00
|
|
|
});
|
2018-07-30 10:52:13 +00:00
|
|
|
} else if (discoveryProcess.interrupted || discoveryProcess.waitingForDevice) {
|
|
|
|
// discovery cycle was interrupted
|
|
|
|
// start from beginning
|
|
|
|
dispatch(begin(device, network));
|
|
|
|
} else {
|
|
|
|
dispatch(discoverAccount(device, discoveryProcess));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const begin = (device: TrezorDevice, network: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
|
|
|
const { config } = getState().localStorage;
|
|
|
|
const coinToDiscover = config.coins.find(c => c.network === network);
|
|
|
|
if (!coinToDiscover) return;
|
|
|
|
|
|
|
|
dispatch({
|
|
|
|
type: DISCOVERY.WAITING_FOR_DEVICE,
|
|
|
|
device,
|
|
|
|
network,
|
|
|
|
});
|
|
|
|
|
|
|
|
// get xpub from TREZOR
|
|
|
|
const response = await TrezorConnect.getPublicKey({
|
|
|
|
device: {
|
|
|
|
path: device.path,
|
|
|
|
instance: device.instance,
|
|
|
|
state: device.state,
|
|
|
|
},
|
|
|
|
path: coinToDiscover.bip44,
|
|
|
|
keepSession: true, // acquire and hold session
|
|
|
|
useEmptyPassphrase: !device.instance,
|
|
|
|
});
|
2018-04-11 10:59:44 +00:00
|
|
|
|
|
|
|
// handle TREZOR response error
|
2018-07-30 10:52:13 +00:00
|
|
|
if (!response.success) {
|
|
|
|
// TODO: check message
|
|
|
|
console.warn('DISCOVERY ERROR', response);
|
|
|
|
dispatch({
|
|
|
|
type: NOTIFICATION.ADD,
|
|
|
|
payload: {
|
|
|
|
type: 'error',
|
|
|
|
title: 'Discovery error',
|
|
|
|
message: response.payload.error,
|
|
|
|
cancelable: true,
|
|
|
|
actions: [
|
|
|
|
{
|
|
|
|
label: 'Try again',
|
|
|
|
callback: () => {
|
|
|
|
dispatch(start(device, network));
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// check for interruption
|
|
|
|
const discoveryProcess: ?Discovery = getState().discovery.find(d => d.deviceState === device.state && d.network === network);
|
|
|
|
if (discoveryProcess && discoveryProcess.interrupted) return;
|
|
|
|
|
|
|
|
const basePath: Array<number> = response.payload.path;
|
|
|
|
|
|
|
|
// send data to reducer
|
|
|
|
dispatch({
|
|
|
|
type: DISCOVERY.START,
|
|
|
|
network: coinToDiscover.network,
|
|
|
|
device,
|
|
|
|
publicKey: response.payload.publicKey,
|
|
|
|
chainCode: response.payload.chainCode,
|
|
|
|
basePath,
|
|
|
|
});
|
|
|
|
|
|
|
|
dispatch(start(device, network));
|
|
|
|
};
|
|
|
|
|
|
|
|
const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
|
|
|
const completed: boolean = discoveryProcess.completed;
|
|
|
|
discoveryProcess.completed = false;
|
|
|
|
|
|
|
|
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.network;
|
|
|
|
|
|
|
|
// TODO: check if address was created before
|
|
|
|
|
|
|
|
// verify address with TREZOR
|
|
|
|
const verifyAddress = await TrezorConnect.ethereumGetAddress({
|
|
|
|
device: {
|
|
|
|
path: device.path,
|
|
|
|
instance: device.instance,
|
|
|
|
state: device.state,
|
|
|
|
},
|
|
|
|
path,
|
|
|
|
showOnTrezor: false,
|
|
|
|
keepSession: true,
|
|
|
|
useEmptyPassphrase: !device.instance,
|
|
|
|
});
|
|
|
|
if (discoveryProcess.interrupted) return;
|
|
|
|
|
|
|
|
// TODO: with block-book (Martin)
|
|
|
|
// const discoveryA = await TrezorConnect.accountDiscovery({
|
|
|
|
// device: {
|
|
|
|
// path: device.path,
|
|
|
|
// instance: device.instance,
|
|
|
|
// state: device.state
|
|
|
|
// },
|
|
|
|
// });
|
|
|
|
// if (discoveryProcess.interrupted) return;
|
|
|
|
|
|
|
|
if (verifyAddress && verifyAddress.success) {
|
|
|
|
//const trezorAddress: string = '0x' + verifyAddress.payload.address;
|
|
|
|
const trezorAddress: string = EthereumjsUtil.toChecksumAddress(verifyAddress.payload.address);
|
|
|
|
if (trezorAddress !== ethAddress) {
|
|
|
|
// throw inconsistent state error
|
|
|
|
console.warn('Inconsistent state', trezorAddress, ethAddress);
|
|
|
|
|
2018-04-11 10:59:44 +00:00
|
|
|
dispatch({
|
|
|
|
type: NOTIFICATION.ADD,
|
|
|
|
payload: {
|
|
|
|
type: 'error',
|
2018-07-30 10:52:13 +00:00
|
|
|
title: 'Address validation error',
|
|
|
|
message: `Addresses are different. TREZOR: ${trezorAddress} HDKey: ${ethAddress}`,
|
2018-04-11 10:59:44 +00:00
|
|
|
cancelable: true,
|
|
|
|
actions: [
|
|
|
|
{
|
|
|
|
label: 'Try again',
|
|
|
|
callback: () => {
|
2018-07-30 10:52:13 +00:00
|
|
|
dispatch(start(device, discoveryProcess.network));
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
});
|
2018-04-11 10:59:44 +00:00
|
|
|
return;
|
|
|
|
}
|
2018-07-30 10:52:13 +00:00
|
|
|
} else {
|
|
|
|
// handle TREZOR communication error
|
2018-04-11 10:59:44 +00:00
|
|
|
dispatch({
|
2018-07-30 10:52:13 +00:00
|
|
|
type: NOTIFICATION.ADD,
|
|
|
|
payload: {
|
|
|
|
type: 'error',
|
|
|
|
title: 'Address validation error',
|
|
|
|
message: verifyAddress.payload.error,
|
|
|
|
cancelable: true,
|
|
|
|
actions: [
|
|
|
|
{
|
|
|
|
label: 'Try again',
|
|
|
|
callback: () => {
|
|
|
|
dispatch(start(device, discoveryProcess.network));
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
2018-04-11 10:59:44 +00:00
|
|
|
});
|
2018-07-30 10:52:13 +00:00
|
|
|
return;
|
2018-04-11 10:59:44 +00:00
|
|
|
}
|
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
const web3instance = getState().web3.find(w3 => w3.network === network);
|
|
|
|
if (!web3instance) return;
|
2018-04-11 10:59:44 +00:00
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
const balance = await getBalanceAsync(web3instance.web3, ethAddress);
|
|
|
|
if (discoveryProcess.interrupted) return;
|
|
|
|
const nonce: number = await getNonceAsync(web3instance.web3, ethAddress);
|
|
|
|
if (discoveryProcess.interrupted) return;
|
2018-05-22 09:40:32 +00:00
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
const addressIsEmpty = nonce < 1 && !balance.greaterThan(0);
|
2018-04-11 10:59:44 +00:00
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
if (!addressIsEmpty || (addressIsEmpty && completed) || (addressIsEmpty && discoveryProcess.accountIndex === 0)) {
|
|
|
|
dispatch({
|
|
|
|
type: ACCOUNT.CREATE,
|
|
|
|
device,
|
|
|
|
network,
|
|
|
|
index: discoveryProcess.accountIndex,
|
|
|
|
path,
|
|
|
|
address: ethAddress,
|
|
|
|
});
|
|
|
|
dispatch(
|
|
|
|
AccountsActions.setBalance(ethAddress, network, device.state || 'undefined', web3instance.web3.fromWei(balance.toString(), 'ether')),
|
|
|
|
);
|
|
|
|
dispatch(AccountsActions.setNonce(ethAddress, network, device.state || 'undefined', nonce));
|
2018-04-11 10:59:44 +00:00
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
if (!completed) { dispatch(discoverAccount(device, discoveryProcess)); }
|
|
|
|
}
|
2018-04-11 10:59:44 +00:00
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
if (addressIsEmpty) {
|
|
|
|
// release acquired sesssion
|
|
|
|
await TrezorConnect.getFeatures({
|
2018-04-11 10:59:44 +00:00
|
|
|
device: {
|
|
|
|
path: device.path,
|
|
|
|
instance: device.instance,
|
2018-07-30 10:52:13 +00:00
|
|
|
state: device.state,
|
2018-04-11 10:59:44 +00:00
|
|
|
},
|
2018-07-30 10:52:13 +00:00
|
|
|
keepSession: false,
|
2018-05-22 10:54:30 +00:00
|
|
|
useEmptyPassphrase: !device.instance,
|
2018-04-11 10:59:44 +00:00
|
|
|
});
|
|
|
|
if (discoveryProcess.interrupted) return;
|
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
dispatch({
|
|
|
|
type: DISCOVERY.COMPLETE,
|
|
|
|
device,
|
|
|
|
network,
|
|
|
|
});
|
2018-04-11 10:59:44 +00:00
|
|
|
}
|
2018-07-30 10:52:13 +00:00
|
|
|
};
|
2018-04-11 10:59:44 +00:00
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
export const restore = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
|
|
|
const selected = getState().wallet.selectedDevice;
|
2018-04-11 10:59:44 +00:00
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
if (selected && selected.connected && selected.features) {
|
|
|
|
const discoveryProcess: ?Discovery = getState().discovery.find(d => d.deviceState === selected.state && d.waitingForDevice);
|
|
|
|
if (discoveryProcess) {
|
|
|
|
dispatch(start(selected, discoveryProcess.network));
|
2018-04-11 10:59:44 +00:00
|
|
|
}
|
|
|
|
}
|
2018-07-30 10:52:13 +00:00
|
|
|
};
|
2018-04-11 10:59:44 +00:00
|
|
|
|
2018-05-11 19:01:43 +00:00
|
|
|
// TODO: rename method to something intuitive
|
2018-04-11 10:59:44 +00:00
|
|
|
// there is no discovery process but it should be
|
|
|
|
// this is possible race condition when "network" was changed in url but device was not authenticated yet
|
|
|
|
// try to start discovery after CONNECT.AUTH_DEVICE action
|
2018-07-30 10:52:13 +00:00
|
|
|
export const check = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
|
|
|
const selected = getState().wallet.selectedDevice;
|
|
|
|
if (!selected) return;
|
|
|
|
|
|
|
|
const urlParams = getState().router.location.state;
|
|
|
|
if (urlParams.network) {
|
|
|
|
const discoveryProcess: ?Discovery = getState().discovery.find(d => d.deviceState === selected.state && d.network === urlParams.network);
|
|
|
|
if (!discoveryProcess) {
|
|
|
|
dispatch(start(selected, urlParams.network));
|
2018-04-11 10:59:44 +00:00
|
|
|
}
|
|
|
|
}
|
2018-07-30 10:52:13 +00:00
|
|
|
};
|
2018-04-11 10:59:44 +00:00
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
export const stop = (device: TrezorDevice): Action => ({
|
|
|
|
type: DISCOVERY.STOP,
|
|
|
|
device,
|
|
|
|
});
|