/* @flow */ import TrezorConnect, { DEVICE, DEVICE_EVENT, UI_EVENT, TRANSPORT_EVENT, BLOCKCHAIN_EVENT, } from 'trezor-connect'; import { CONTEXT_NONE } from 'actions/constants/modal'; import urlConstants from 'constants/urls'; import * as CONNECT from 'actions/constants/TrezorConnect'; import * as NOTIFICATION from 'actions/constants/notification'; import { getDuplicateInstanceNumber } from 'reducers/utils'; import * as RouterActions from 'actions/RouterActions'; import * as deviceUtils from 'utils/device'; import * as buildUtils from 'utils/build'; import type { DeviceMessage, DeviceMessageType, UiMessage, UiMessageType, TransportMessage, TransportMessageType, BlockchainEvent, } from 'trezor-connect'; import type { Dispatch, GetState, Action, ThunkAction, AsyncAction, Device, TrezorDevice, } from 'flowtype'; export type TrezorConnectAction = | { type: typeof CONNECT.INITIALIZATION_ERROR, error: string, } | { type: typeof CONNECT.NETWORK_CHANGED, payload: { network: string, }, } | { type: typeof CONNECT.AUTH_DEVICE, device: TrezorDevice, state: string, } | { type: typeof CONNECT.DUPLICATE, device: TrezorDevice, } | { type: typeof CONNECT.REMEMBER_REQUEST, device: TrezorDevice, instances: Array, } | { type: typeof CONNECT.DISCONNECT_REQUEST, device: TrezorDevice, } | { type: typeof CONNECT.FORGET_REQUEST, device: TrezorDevice, } | { type: typeof CONNECT.FORGET, device: TrezorDevice, } | { type: typeof CONNECT.FORGET_SINGLE | typeof CONNECT.FORGET_SILENT, device: TrezorDevice, } | { type: typeof CONNECT.REMEMBER, device: TrezorDevice, } | { type: typeof CONNECT.TRY_TO_DUPLICATE, device: TrezorDevice, } | { type: typeof CONNECT.DEVICE_FROM_STORAGE, payload: Array, } | { type: typeof CONNECT.START_ACQUIRING | typeof CONNECT.STOP_ACQUIRING, } | { type: typeof CONNECT.REQUEST_WALLET_TYPE, device: TrezorDevice, } | { type: typeof CONNECT.RECEIVE_WALLET_TYPE | typeof CONNECT.UPDATE_WALLET_TYPE, device: TrezorDevice, hidden: boolean, }; declare var LOCAL: ?string; export const init = (): AsyncAction => async ( dispatch: Dispatch, getState: GetState ): Promise => { // set listeners TrezorConnect.on( DEVICE_EVENT, (event: DeviceMessage): void => { // post event to reducers const type: DeviceMessageType = event.type; // eslint-disable-line prefer-destructuring dispatch({ type, device: event.payload, }); } ); TrezorConnect.on( UI_EVENT, (event: UiMessage): void => { // post event to reducers const type: UiMessageType = event.type; // eslint-disable-line prefer-destructuring dispatch({ type, payload: event.payload, }); } ); TrezorConnect.on( TRANSPORT_EVENT, (event: TransportMessage): void => { // post event to reducers const type: TransportMessageType = event.type; // eslint-disable-line prefer-destructuring dispatch({ type, payload: event.payload, }); } ); // post event to reducers TrezorConnect.on( BLOCKCHAIN_EVENT, (event: BlockchainEvent): void => { dispatch(event); } ); if (buildUtils.isDev()) { // eslint-disable-next-line window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://sisyfos.trezor.io/connect/'; // eslint-disable-line no-underscore-dangle // window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://localhost:8088/'; // eslint-disable-line no-underscore-dangle window.TrezorConnect = TrezorConnect; } try { await TrezorConnect.init({ transportReconnect: true, debug: false, popup: false, webusb: true, pendingTransportEvent: getState().devices.length < 1, manifest: { email: 'info@trezor.io', appUrl: urlConstants.NEXT_WALLET, }, }); } catch (error) { dispatch({ type: CONNECT.INITIALIZATION_ERROR, error, }); } }; // called after backend was initialized // set listeners for connect/disconnect export const postInit = (): ThunkAction => (dispatch: Dispatch): void => { const handleDeviceConnect = (device: Device) => { dispatch(RouterActions.selectDevice(device)); }; TrezorConnect.off(DEVICE.CONNECT, handleDeviceConnect); TrezorConnect.off(DEVICE.CONNECT_UNACQUIRED, handleDeviceConnect); TrezorConnect.on(DEVICE.CONNECT, handleDeviceConnect); TrezorConnect.on(DEVICE.CONNECT_UNACQUIRED, handleDeviceConnect); // try to redirect to initial url if (!dispatch(RouterActions.setInitialUrl())) { // if initial redirection fails try to switch to first available device dispatch(RouterActions.selectFirstAvailableDevice()); } }; export const requestWalletType = (): AsyncAction => async ( dispatch: Dispatch, getState: GetState ): Promise => { const selected = getState().wallet.selectedDevice; if (!selected) return; const isDeviceReady = selected.connected && selected.features && !selected.state && selected.mode === 'normal' && selected.firmware !== 'required'; if (!isDeviceReady) return; if (selected.features && selected.features.passphrase_protection) { dispatch({ type: CONNECT.REQUEST_WALLET_TYPE, device: selected, }); } else { dispatch({ type: CONNECT.RECEIVE_WALLET_TYPE, device: selected, hidden: false, state: selected.state, }); } }; export const authorizeDevice = (): AsyncAction => async ( dispatch: Dispatch, getState: GetState ): Promise => { const selected = getState().wallet.selectedDevice; if (!selected) return; const isDeviceReady = selected.connected && selected.features && !selected.state && selected.mode === 'normal' && selected.firmware !== 'required'; if (!isDeviceReady) return; const response = await TrezorConnect.getDeviceState({ device: { path: selected.path, instance: selected.instance, state: selected.state, }, useEmptyPassphrase: selected.useEmptyPassphrase, }); if (response && response.success) { dispatch({ type: CONNECT.AUTH_DEVICE, device: selected, state: response.payload.state, }); } else { dispatch({ type: NOTIFICATION.ADD, payload: { devicePath: selected.path, type: 'error', title: 'Authentication error', message: response.payload.error, cancelable: false, actions: [ { label: 'Try again', callback: () => { dispatch({ type: NOTIFICATION.CLOSE, payload: { devicePath: selected.path }, }); dispatch(authorizeDevice()); }, }, ], }, }); } }; export const deviceDisconnect = (device: Device): AsyncAction => async ( dispatch: Dispatch, getState: GetState ): Promise => { if (device.features) { const instances = getState().devices.filter( d => d.features && device.features && d.state && !d.remember && d.features.device_id === device.features.device_id ); if (instances.length > 0) { const isSelected = deviceUtils.isSelectedDevice( getState().wallet.selectedDevice, device ); if (!isSelected && getState().modal.context !== CONTEXT_NONE) { dispatch({ type: CONNECT.FORGET_SILENT, device: instances[0], }); } else { dispatch({ type: CONNECT.REMEMBER_REQUEST, device: instances[0], instances, }); } } else { dispatch(RouterActions.selectFirstAvailableDevice()); } } else { dispatch(RouterActions.selectFirstAvailableDevice()); } }; export function reload(): AsyncAction { return async (): Promise => {}; } export function acquire(): AsyncAction { return async (dispatch: Dispatch, getState: GetState): Promise => { const selected: ?TrezorDevice = getState().wallet.selectedDevice; if (!selected) return; dispatch({ type: CONNECT.START_ACQUIRING, }); // this is the only place where useEmptyPassphrase should be used every time // the goal here is to acquire device and get his features // authentication (passphrase) is not needed here yet const response = await TrezorConnect.getFeatures({ device: { path: selected.path, }, useEmptyPassphrase: true, }); if (!response.success) { dispatch({ type: NOTIFICATION.ADD, payload: { type: 'error', title: 'Acquire device error', message: response.payload.error, cancelable: true, // actions: [ // { // label: 'Try again', // callback: () => { // dispatch(acquire()) // } // } // ] }, }); } dispatch({ type: CONNECT.STOP_ACQUIRING, }); }; } // called from Aside - device menu (forget single instance) export const forget = (device: TrezorDevice): Action => ({ type: CONNECT.FORGET_REQUEST, device, }); export const duplicateDeviceOld = (device: TrezorDevice): AsyncAction => async ( dispatch: Dispatch, getState: GetState ): Promise => { const instance: number = getDuplicateInstanceNumber(getState().devices, device); const extended: Object = { instance }; dispatch({ type: CONNECT.DUPLICATE, device: { ...device, ...extended }, }); }; export const duplicateDevice = (device: TrezorDevice): AsyncAction => async ( dispatch: Dispatch ): Promise => { dispatch({ type: CONNECT.REQUEST_WALLET_TYPE, device, }); };