/* @flow */ 'use strict'; import TrezorConnect from 'trezor-connect'; import { findSelectedDevice } from '../reducers/TrezorConnectReducer'; import * as DISCOVERY from './constants/discovery'; import * as ADDRESS from './constants/address'; import * as TOKEN from './constants/token'; import * as NOTIFICATION from './constants/notification'; import type { Discovery } from '../reducers/DiscoveryReducer'; import HDKey from 'hdkey'; import EthereumjsUtil from 'ethereumjs-util'; import { getNonceAsync, getBalanceAsync, getTokenBalanceAsync } from './Web3Actions'; export type DiscoveryAction = { type: typeof DISCOVERY.START, } | { type: typeof DISCOVERY.STOP, } | { type: typeof DISCOVERY.COMPLETE, }; export const start = (device: any, network: string, ignoreCompleted?: boolean): any => { return (dispatch, getState) => { const selected = findSelectedDevice(getState().connect); if (!selected) { // TODO: throw error console.error("Start discovery: no selected device", device) return; } else if (selected.path !== device.path) { console.error("Start discovery: requested device is not selected", device, selected) return; } else if (!selected.state) { console.warn("Start discovery: Selected device wasn't authenticated yet...") return; } else if (selected.connected && !selected.available) { console.warn("Start discovery: Selected device is unavailable...") return; } const discovery = getState().discovery; let discoveryProcess: ?Discovery = discovery.find(d => d.deviceState === device.state && d.network === network); if (!selected.connected && (!discoveryProcess || !discoveryProcess.completed)) { dispatch({ type: DISCOVERY.WAITING, device, network }); return; } if (!discoveryProcess) { dispatch( begin(device, network) ); return; } else { if (discoveryProcess.completed && !ignoreCompleted) { dispatch({ type: DISCOVERY.COMPLETE, device, network }); } else if (discoveryProcess.interrupted || discoveryProcess.waitingForDevice) { // discovery cycle was interrupted // start from beginning dispatch( begin(device, network) ); } else { dispatch( discoverAddress(device, discoveryProcess) ); } } } } const begin = (device: any, network: string) => { return async (dispatch, getState) => { const { config } = getState().localStorage; const coinToDiscover = config.coins.find(c => c.network === network); dispatch({ type: DISCOVERY.WAITING, 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 }); // handle TREZOR response error 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 let discoveryProcess: ?Discovery = getState().discovery.find(d => d.deviceState === device.state && d.network === network); if (discoveryProcess && discoveryProcess.interrupted) return; const basePath: Array = response.payload.path; const hdKey = new HDKey(); hdKey.publicKey = new Buffer(response.payload.publicKey, 'hex'); hdKey.chainCode = new Buffer(response.payload.chainCode, 'hex'); // send data to reducer dispatch({ type: DISCOVERY.START, network: coinToDiscover.network, device, xpub: response.payload.publicKey, basePath, hdKey, }); dispatch( start(device, network) ); } } const discoverAddress = (device: any, discoveryProcess: Discovery): any => { return async (dispatch, getState) => { 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; dispatch({ type: ADDRESS.CREATE, device, network, index: discoveryProcess.accountIndex, path, address: ethAddress }); // 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 }); 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); dispatch({ type: NOTIFICATION.ADD, payload: { type: 'error', title: 'Address validation error', message: `Addresses are different. TREZOR: ${ trezorAddress } HDKey: ${ ethAddress }`, cancelable: true, actions: [ { label: 'Try again', callback: () => { dispatch(start(device, discoveryProcess.network)) } } ] } }); return; } } else { // handle TREZOR communication error dispatch({ 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)) } } ] } }); return; } const web3instance = getState().web3.find(w3 => w3.network === network); const balance = await getBalanceAsync(web3instance.web3, ethAddress); if (discoveryProcess.interrupted) return; dispatch({ type: ADDRESS.SET_BALANCE, address: ethAddress, network, balance: web3instance.web3.fromWei(balance.toString(), 'ether') }); const userTokens = []; // const userTokens = [ // { symbol: 'T01', address: '0x58cda554935e4a1f2acbe15f8757400af275e084' }, // { symbol: 'Lahod', address: '0x3360d0ee34a49d9ac34dce88b000a2903f2806ee' }, // ]; for (let i = 0; i < userTokens.length; i++) { const tokenBalance = await getTokenBalanceAsync(web3instance.erc20, userTokens[i].address, ethAddress); if (discoveryProcess.interrupted) return; dispatch({ type: TOKEN.SET_BALANCE, tokenName: userTokens[i].symbol, ethAddress: ethAddress, tokenAddress: userTokens[i].address, balance: tokenBalance.toString() }) } const nonce = await getNonceAsync(web3instance.web3, ethAddress); if (discoveryProcess.interrupted) return; dispatch({ type: ADDRESS.SET_NONCE, address: ethAddress, network, nonce: nonce }); const addressIsEmpty = nonce < 1 && !balance.greaterThan(0); if (!addressIsEmpty) { dispatch( discoverAddress(device, discoveryProcess) ); } else { // release acquired sesssion await TrezorConnect.getFeatures({ device: { path: device.path, instance: device.instance, state: device.state }, keepSession: false }); if (discoveryProcess.interrupted) return; dispatch({ type: DISCOVERY.COMPLETE, device, network }); } } } export const restore = (): any => { return (dispatch, getState): void => { const selected = findSelectedDevice(getState().connect); if (selected && selected.connected && !selected.unacquired) { const discoveryProcess: ?Discovery = getState().discovery.find(d => d.deviceState === selected.state && d.waitingForDevice); if (discoveryProcess) { dispatch( start(selected, discoveryProcess.network) ); } } } } // 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 export const check = (): any => { return (dispatch, getState): void => { const selected = findSelectedDevice(getState().connect); 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) ); } } } } export const stop = (device: any): any => { // TODO: release devices session // corner case switch /eth to /etc (discovery start stop - should not be async) return { type: DISCOVERY.STOP, device } }