diff --git a/.flowconfig b/.flowconfig index 9b970efd..b92aa275 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1,4 +1,5 @@ [include] +.*/node_modules/hd-wallet/lib [ignore] .*/node_modules/fbjs/.* @@ -7,6 +8,9 @@ .*/node_modules/redux/.* .*/node_modules/react-router/.* .*/node_modules/oboe/test/.* +.*/node_modules/trezor-link/lib/lowlevel/webusb.js.flow +.*/node_modules/hd-wallet/lib/utils/stream.js.flow +.*/node_modules/protobufjs-old-fixed-webpack/src/bower.json .*/_old/.* .*/scripts/solidity/.* .*/build/.* @@ -19,7 +23,6 @@ ./src/flowtype/npm/react-router-dom_v4.x.x.js ; ./src/flowtype/npm/react-intl_v2.x.x.js // TODO: uncomment to get proper flow support for intl (needs some flow fixing) ./src/flowtype/npm/connected-react-router.js -./src/flowtype/npm/bignumber.js ./src/flowtype/npm/ethereum-types.js ./src/flowtype/npm/web3.js ./src/flowtype/css.js diff --git a/package.json b/package.json index e55673f6..56e2f584 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "dependencies": { "@babel/polyfill": "^7.2.5", "@hot-loader/react-dom": "16.8.6", - "bignumber.js": "8.0.2", + "bignumber.js": "9.0.0", "color-hash": "^1.0.3", "commander": "^2.19.0", "connected-react-router": "6.4.0", @@ -47,7 +47,6 @@ "ethereumjs-util": "^6.0.0", "express": "^4.16.4", "git-revision-webpack-plugin": "^3.0.3", - "hdkey": "^1.1.0", "history": "^4.7.2", "html-webpack-plugin": "^3.2.0", "jest-fetch-mock": "^2.1.0", @@ -81,7 +80,7 @@ "styled-components": "^4.1.3", "styled-normalize": "^8.0.6", "trezor-bridge-communicator": "1.0.2", - "trezor-connect": "7.0.2", + "trezor-connect": "8.1.0-beta.8", "trezor-ui-components": "^1.0.0-beta.20", "wallet-address-validator": "^0.2.4", "web3": "1.0.0-beta.36", @@ -124,7 +123,7 @@ "eslint-plugin-prettier": "^3.0.1", "eslint-plugin-react": "^7.12.4", "file-loader": "3.0.1", - "flow-bin": "0.90", + "flow-bin": "0.121.0", "jest": "^24.9.0", "prettier": "^1.16.4", "prettier-eslint": "^9.0.0", diff --git a/public/data/appConfig.json b/public/data/appConfig.json index 1703c331..776ba336 100644 --- a/public/data/appConfig.json +++ b/public/data/appConfig.json @@ -15,7 +15,7 @@ "name": "Ethereum", "symbol": "ETH", "shortcut": "eth", - "bip44": "m/44'/60'/0'/0", + "bip44": "m/44'/60'/0'/0/a", "chainId": 1, "defaultGasPrice": 64, "defaultGasLimit": 21000, @@ -26,8 +26,8 @@ "wss://eth2.trezor.io/geth" ], "explorer": { - "tx": "https://etherscan.io/tx/", - "address": "https://etherscan.io/address/" + "tx": "https://eth1.trezor.io/tx/", + "address": "https://eth1.trezor.io/address/" }, "hasSignVerify": true }, @@ -38,7 +38,7 @@ "symbol": "ETC", "shortcut": "etc", "chainId": 61, - "bip44": "m/44'/61'/0'/0", + "bip44": "m/44'/61'/0'/0/a", "defaultGasPrice": 64, "defaultGasLimit": 21000, "defaultGasLimitTokens": 200000, @@ -61,7 +61,7 @@ "symbol": "tROP", "shortcut": "trop", "chainId": 3, - "bip44": "m/44'/60'/0'/0", + "bip44": "m/44'/60'/0'/0/a", "defaultGasPrice": 64, "defaultGasLimit": 21000, "defaultGasLimitTokens": 200000, @@ -83,8 +83,8 @@ "wss://ropsten1.trezor.io/geth" ], "explorer": { - "tx": "https://ropsten.etherscan.io/tx/", - "address": "https://ropsten.etherscan.io/address/" + "tx": "https://ropsten1.trezor.io/tx/", + "address": "https://ropsten1.trezor.io/address/" }, "hasSignVerify": true }, diff --git a/src/actions/BlockchainActions.js b/src/actions/BlockchainActions.js index 1e5e12c7..cf38efa6 100644 --- a/src/actions/BlockchainActions.js +++ b/src/actions/BlockchainActions.js @@ -76,32 +76,31 @@ export const subscribe = (networkName: string): PromiseAction => async ( } }; -export const onBlockMined = ( - payload: $ElementType -): PromiseAction => async (dispatch: Dispatch, getState: GetState): Promise => { +export const onBlockMined = (payload: BlockchainBlock): PromiseAction => async ( + dispatch: Dispatch, + getState: GetState +): Promise => { const shortcut = payload.coin.shortcut.toLowerCase(); - const { block } = payload; - if (getState().router.location.state.network !== shortcut) return; - const { config } = getState().localStorage; const network = config.networks.find(c => c.shortcut === shortcut); if (!network) return; switch (network.type) { case 'ethereum': - await dispatch(EthereumBlockchainActions.onBlockMined(shortcut)); + await dispatch(EthereumBlockchainActions.onBlockMined(network)); break; case 'ripple': - await dispatch(RippleBlockchainActions.onBlockMined(shortcut, block)); + await dispatch(RippleBlockchainActions.onBlockMined(network)); break; default: break; } }; -export const onNotification = ( - payload: $ElementType -): PromiseAction => async (dispatch: Dispatch, getState: GetState): Promise => { +export const onNotification = (payload: BlockchainNotification): PromiseAction => async ( + dispatch: Dispatch, + getState: GetState +): Promise => { const shortcut = payload.coin.shortcut.toLowerCase(); const { config } = getState().localStorage; const network = config.networks.find(c => c.shortcut === shortcut); @@ -109,11 +108,10 @@ export const onNotification = ( switch (network.type) { case 'ethereum': - // this is not working until blockchain-link will start support blockbook backends - await dispatch(EthereumBlockchainActions.onNotification(payload)); + await dispatch(EthereumBlockchainActions.onNotification(payload, network)); break; case 'ripple': - await dispatch(RippleBlockchainActions.onNotification(payload)); + await dispatch(RippleBlockchainActions.onNotification(payload, network)); break; default: break; @@ -122,9 +120,10 @@ export const onNotification = ( // Handle BLOCKCHAIN.ERROR event from TrezorConnect // disconnect and remove Web3 websocket instance if exists -export const onError = ( - payload: $ElementType -): PromiseAction => async (dispatch: Dispatch, getState: GetState): Promise => { +export const onError = (payload: BlockchainError): PromiseAction => async ( + dispatch: Dispatch, + getState: GetState +): Promise => { const shortcut = payload.coin.shortcut.toLowerCase(); const { config } = getState().localStorage; const network = config.networks.find(c => c.shortcut === shortcut); diff --git a/src/actions/ImportAccountActions.js b/src/actions/ImportAccountActions.js index 91a16277..9e779279 100644 --- a/src/actions/ImportAccountActions.js +++ b/src/actions/ImportAccountActions.js @@ -4,10 +4,9 @@ import * as ACCOUNT from 'actions/constants/account'; import * as IMPORT from 'actions/constants/importAccount'; import * as NOTIFICATION from 'actions/constants/notification'; import type { AsyncAction, Account, TrezorDevice, Network, Dispatch, GetState } from 'flowtype'; -import * as BlockchainActions from 'actions/ethereum/BlockchainActions'; import * as LocalStorageActions from 'actions/LocalStorageActions'; import TrezorConnect from 'trezor-connect'; -import { toDecimalAmount } from 'utils/formatUtils'; +import { enhanceAccount } from 'utils/accountUtils'; export type ImportAccountAction = | { @@ -23,10 +22,7 @@ export type ImportAccountAction = const findIndex = (accounts: Array, network: Network, device: TrezorDevice): number => { return accounts.filter( - a => - a.imported === true && - a.network === network.shortcut && - a.deviceID === (device.features || {}).device_id + a => a.imported === true && a.network === network.shortcut && a.deviceID === device.id ).length; }; @@ -35,113 +31,22 @@ export const importAddress = ( network: Network, device: ?TrezorDevice ): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { - if (!device) return; + if (!device || !device.features) return; dispatch({ type: IMPORT.START, }); - let payload; - try { - if (network.type === 'ethereum') { - const account = await dispatch( - BlockchainActions.discoverAccount(device, address, network.shortcut) - ); - - const index = findIndex(getState().accounts, network, device); - - const empty = account.nonce <= 0 && account.balance === '0'; - payload = { - imported: true, - index, - network: network.shortcut, - deviceID: device.features ? device.features.device_id : '0', - deviceState: device.state || '0', - accountPath: account.path || [], - descriptor: account.descriptor, - - balance: account.balance, - availableBalance: account.balance, - block: account.block, - transactions: account.transactions, - empty, - - networkType: 'ethereum', - nonce: account.nonce, - }; - dispatch({ - type: ACCOUNT.CREATE, - payload, - }); - dispatch({ - type: IMPORT.SUCCESS, - }); - dispatch(LocalStorageActions.setImportedAccount(payload)); - dispatch({ - type: NOTIFICATION.ADD, - payload: { - variant: 'success', - title: 'The account has been successfully imported', - cancelable: true, - }, - }); - } else if (network.type === 'ripple') { - const response = await TrezorConnect.rippleGetAccountInfo({ - account: { - descriptor: address, - }, - coin: network.shortcut, - }); - - // handle TREZOR response error - if (!response.success) { - throw new Error(response.payload.error); - } - - const account = response.payload; - const empty = account.sequence <= 0 && account.balance === '0'; - const index = findIndex(getState().accounts, network, device); - - payload = { - imported: true, - index, - network: network.shortcut, - deviceID: device.features ? device.features.device_id : '0', - deviceState: device.state || '0', - accountPath: account.path || [], - descriptor: account.descriptor, - - balance: toDecimalAmount(account.balance, network.decimals), - availableBalance: toDecimalAmount(account.availableBalance, network.decimals), - block: account.block, - transactions: account.transactions, - empty, + const response = await TrezorConnect.getAccountInfo({ + descriptor: address, + coin: network.shortcut, + }); - networkType: 'ripple', - sequence: account.sequence, - reserve: toDecimalAmount(account.reserve, network.decimals), - }; - dispatch({ - type: ACCOUNT.CREATE, - payload, - }); - dispatch({ - type: IMPORT.SUCCESS, - }); - dispatch(LocalStorageActions.setImportedAccount(payload)); - dispatch({ - type: NOTIFICATION.ADD, - payload: { - variant: 'success', - title: 'The account has been successfully imported', - cancelable: true, - }, - }); - } - } catch (error) { + // handle TREZOR response error + if (!response.success) { dispatch({ type: IMPORT.FAIL, - error: error.message, + error: response.payload.error, }); dispatch({ @@ -149,9 +54,36 @@ export const importAddress = ( payload: { variant: 'error', title: 'Import account error', - message: error.message, + message: response.payload.error, cancelable: true, }, }); + + return; } + + const index = findIndex(getState().accounts, network, device); + const account = enhanceAccount(response.payload, { + imported: true, + index, + network, + device, + }); + + dispatch({ + type: ACCOUNT.CREATE, + payload: account, + }); + dispatch({ + type: IMPORT.SUCCESS, + }); + dispatch(LocalStorageActions.setImportedAccount(account)); + dispatch({ + type: NOTIFICATION.ADD, + payload: { + variant: 'success', + title: 'The account has been successfully imported', + cancelable: true, + }, + }); }; diff --git a/src/actions/LocalStorageActions.js b/src/actions/LocalStorageActions.js index 1148ed8e..34a8bf82 100644 --- a/src/actions/LocalStorageActions.js +++ b/src/actions/LocalStorageActions.js @@ -213,7 +213,8 @@ const loadJSON = (): AsyncAction => async (dispatch: Dispatch): Promise => } }; -const VERSION: string = '2'; +// version 3 since trezor-connect@8 implementation +const VERSION: string = '3'; const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => { // validate version @@ -454,9 +455,8 @@ export const removeImportedAccounts = (device: TrezorDevice): ThunkAction => ( const importedAccounts: ?Array = getImportedAccounts(); if (!importedAccounts) return; - const deviceId = device.features ? device.features.device_id : null; const filteredImportedAccounts = importedAccounts.filter( - account => account.deviceID !== deviceId + account => account.deviceID !== device.id ); storageUtils.remove(TYPE, KEY_IMPORTED_ACCOUNTS); filteredImportedAccounts.forEach(account => { diff --git a/src/actions/ModalActions.js b/src/actions/ModalActions.js index f665762c..5c2f5eea 100644 --- a/src/actions/ModalActions.js +++ b/src/actions/ModalActions.js @@ -33,14 +33,14 @@ export const onPinSubmit = (value: string): Action => { }; }; -export const onPassphraseSubmit = (passphrase: string): AsyncAction => async ( - dispatch: Dispatch, - getState: GetState -): Promise => { +export const onPassphraseSubmit = ( + passphrase: string, + passphraseOnDevice: boolean = false +): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { const { modal } = getState(); if (modal.context !== MODAL.CONTEXT_DEVICE) return; - if (passphrase === '') { + if (passphrase === '' && !passphraseOnDevice) { // set standard wallet type if passphrase is blank dispatch({ type: CONNECT.UPDATE_WALLET_TYPE, @@ -54,6 +54,7 @@ export const onPassphraseSubmit = (passphrase: string): AsyncAction => async ( payload: { value: passphrase, save: true, + passphraseOnDevice, }, }); @@ -144,7 +145,7 @@ export const onDeviceConnect = (device: Device): ThunkAction => ( device.features && modal.device && modal.device.features && - modal.device.features.device_id === device.features.device_id + modal.device.id === device.id ) { dispatch({ type: MODAL.CLOSE, diff --git a/src/actions/ReceiveActions.js b/src/actions/ReceiveActions.js index 7516d195..9d27fb8f 100644 --- a/src/actions/ReceiveActions.js +++ b/src/actions/ReceiveActions.js @@ -54,7 +54,7 @@ export const showUnverifiedAddress = (): Action => ({ }); //export const showAddress = (address_n: string): AsyncAction => { -export const showAddress = (path: Array): AsyncAction => async ( +export const showAddress = (path: string): AsyncAction => async ( dispatch: Dispatch, getState: GetState ): Promise => { diff --git a/src/actions/RouterActions.js b/src/actions/RouterActions.js index f8f10877..143a0f7d 100644 --- a/src/actions/RouterActions.js +++ b/src/actions/RouterActions.js @@ -65,14 +65,14 @@ export const paramsValidation = (params: RouterLocationState): PayloadAction d.features && - d.features.device_id === params.device && + d.id === params.device && d.instance === parseInt(params.deviceInstance, 10) ); } else { device = devices.find( d => ((!d.features || d.mode === 'bootloader') && d.path === params.device) || - (d.features && d.features.device_id === params.device) + (d.features && d.id === params.device) ); } @@ -218,21 +218,22 @@ const getDeviceUrl = (device: TrezorDevice | Device): PayloadAction => getState: GetState ): ?string => { let url: ?string; + const prefix = `/device/${device.id || device.path}`; if (!device.features) { - url = `/device/${device.path}/${device.type === 'unreadable' ? 'unreadable' : 'acquire'}`; + url = `${prefix}/${device.type === 'unreadable' ? 'unreadable' : 'acquire'}`; } else if (device.mode === 'bootloader') { // device in bootloader doesn't have device_id - url = `/device/${device.path}/bootloader`; + url = `${prefix}/bootloader`; } else if (device.mode === 'initialize') { - url = `/device/${device.features.device_id}/initialize`; + url = `${prefix}/initialize`; } else if (device.mode === 'seedless') { - url = `/device/${device.features.device_id}/seedless`; + url = `${prefix}/seedless`; } else if (device.firmware === 'required') { - url = `/device/${device.features.device_id}/firmware-update`; + url = `${prefix}/firmware-update`; } else if (typeof device.instance === 'number') { - url = `/device/${device.features.device_id}:${device.instance}`; + url = `${prefix}:${device.instance}`; } else { - url = `/device/${device.features.device_id}`; + url = `${prefix}`; // make sure that device is not TrezorDevice type if (!device.hasOwnProperty('ts')) { // it is device from trezor-connect triggered by DEVICE.CONNECT event @@ -379,10 +380,8 @@ export const gotoLandingPage = (): ThunkAction => (dispatch: Dispatch): void => export const gotoDeviceSettings = (device: TrezorDevice): ThunkAction => ( dispatch: Dispatch ): void => { - if (device.features) { - const devUrl: string = `${device.features.device_id}${ - device.instance ? `:${device.instance}` : '' - }`; + if (device.id) { + const devUrl: string = `${device.id}${device.instance ? `:${device.instance}` : ''}`; dispatch(goto(`/device/${devUrl}/settings`)); } }; @@ -403,8 +402,8 @@ export const gotoFirmwareUpdate = (): ThunkAction => ( getState: GetState ): void => { const { selectedDevice } = getState().wallet; - if (!selectedDevice || !selectedDevice.features) return; - const devUrl: string = `${selectedDevice.features.device_id}${ + if (!selectedDevice || !selectedDevice.id) return; + const devUrl: string = `${selectedDevice.id}${ selectedDevice.instance ? `:${selectedDevice.instance}` : '' }`; dispatch(goto(`/device/${devUrl}/firmware-update`)); @@ -415,8 +414,8 @@ export const gotoFirmwareUpdate = (): ThunkAction => ( */ export const gotoBackup = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { const { selectedDevice } = getState().wallet; - if (!selectedDevice || !selectedDevice.features) return; - const devUrl: string = `${selectedDevice.features.device_id}${ + if (!selectedDevice || !selectedDevice.id) return; + const devUrl: string = `${selectedDevice.id}${ selectedDevice.instance ? `:${selectedDevice.instance}` : '' }`; dispatch(goto(`/device/${devUrl}/backup`)); diff --git a/src/actions/SignVerifyActions.js b/src/actions/SignVerifyActions.js index a0722196..383592f5 100644 --- a/src/actions/SignVerifyActions.js +++ b/src/actions/SignVerifyActions.js @@ -39,7 +39,7 @@ export type SignVerifyAction = message: ?string, }; -const sign = (path: Array, message: string, hex: boolean = false): AsyncAction => async ( +const sign = (path: string, message: string, hex: boolean = false): AsyncAction => async ( dispatch: Dispatch, getState: GetState ): Promise => { diff --git a/src/actions/TokenActions.js b/src/actions/TokenActions.js index 409360c6..c1ba1538 100644 --- a/src/actions/TokenActions.js +++ b/src/actions/TokenActions.js @@ -1,7 +1,9 @@ /* @flow */ import * as TOKEN from 'actions/constants/token'; +import { toDecimalAmount } from 'utils/formatUtils'; +import type { TokenInfo } from 'trezor-connect'; import type { GetState, AsyncAction, Action, Dispatch } from 'flowtype'; import type { State, Token } from 'reducers/TokensReducer'; import type { Account } from 'reducers/AccountsReducer'; @@ -60,21 +62,19 @@ export const setBalance = ( ethAddress: string, balance: string ): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const newState: Array = [...getState().tokens]; - const token: ?Token = newState.find( - t => t.address === tokenAddress && t.ethAddress === ethAddress - ); - if (token) { - const others = newState.filter(t => t !== token); + const { tokens } = getState(); + const index = tokens.findIndex(t => t.address === tokenAddress && t.ethAddress === ethAddress); + if (index >= 0) { + const token = tokens[index]; + const payload = tokens.slice(); + payload[index] = { + ...token, + loaded: true, + balance, + }; dispatch({ type: TOKEN.SET_BALANCE, - payload: others.concat([ - { - ...token, - loaded: true, - balance, - }, - ]), + payload, }); } }; @@ -103,6 +103,27 @@ export const add = (token: NetworkToken, account: Account): AsyncAction => async dispatch(setBalance(token.address, account.descriptor, tokenBalance)); }; +export const createAccountTokens = (account: Account, tokens: TokenInfo[]): AsyncAction => async ( + dispatch: Dispatch +): Promise => { + tokens.forEach(t => { + dispatch({ + type: TOKEN.ADD, + payload: { + address: t.address, + balance: toDecimalAmount(t.balance || '0', t.decimals), + decimals: t.decimals, + deviceState: account.deviceState, + ethAddress: account.descriptor, + loaded: true, + name: t.name || '', + network: account.network, + symbol: t.symbol || '', + }, + }); + }); +}; + export const remove = (token: Token): Action => ({ type: TOKEN.REMOVE, token, diff --git a/src/actions/TrezorConnectActions.js b/src/actions/TrezorConnectActions.js index b80ff891..3c8c420d 100644 --- a/src/actions/TrezorConnectActions.js +++ b/src/actions/TrezorConnectActions.js @@ -17,16 +17,6 @@ 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, @@ -113,62 +103,32 @@ export const init = (): AsyncAction => async ( 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(DEVICE_EVENT, event => { + dispatch(event); + }); - 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(UI_EVENT, event => { + dispatch(event); + }); - 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, - }); - } - ); + TrezorConnect.on(TRANSPORT_EVENT, event => { + dispatch(event); + }); // post event to reducers - TrezorConnect.on( - BLOCKCHAIN_EVENT, - (event: BlockchainEvent): void => { - dispatch(event); - } - ); + TrezorConnect.on(BLOCKCHAIN_EVENT, event => { + dispatch(event); + }); + let connectSrc; if (buildUtils.isDev()) { - // eslint-disable-next-line - window.__TREZOR_CONNECT_SRC = - typeof LOCAL === 'string' - ? LOCAL - : 'https://connect.corp.sldev.cz/fix/v7-ripple-lib-error/'; // eslint-disable-line no-underscore-dangle - // window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://localhost:8088/'; // eslint-disable-line no-underscore-dangle + connectSrc = typeof LOCAL === 'string' ? LOCAL : undefined; window.TrezorConnect = TrezorConnect; } try { await TrezorConnect.init({ + connectSrc, transportReconnect: true, debug: false, popup: false, @@ -223,10 +183,18 @@ export const requestWalletType = (): AsyncAction => async ( if (!isDeviceReady) return; if (selected.features && selected.features.passphrase_protection) { - dispatch({ - type: CONNECT.REQUEST_WALLET_TYPE, - device: selected, - }); + if (selected.features.passphrase_always_on_device) { + dispatch({ + type: CONNECT.RECEIVE_WALLET_TYPE, + device: selected, + hidden: true, + }); + } else { + dispatch({ + type: CONNECT.REQUEST_WALLET_TYPE, + device: selected, + }); + } } else { dispatch({ type: CONNECT.RECEIVE_WALLET_TYPE, @@ -298,12 +266,7 @@ export const deviceDisconnect = (device: Device): AsyncAction => async ( ): 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 + d => d.features && device.features && d.state && !d.remember && d.id === device.id ); if (instances.length > 0) { const isSelected = deviceUtils.isSelectedDevice( @@ -401,8 +364,16 @@ export const duplicateDeviceOld = (device: TrezorDevice): AsyncAction => async ( export const duplicateDevice = (device: TrezorDevice): AsyncAction => async ( dispatch: Dispatch ): Promise => { - dispatch({ - type: CONNECT.REQUEST_WALLET_TYPE, - device, - }); + if (device.features && device.features.passphrase_always_on_device) { + dispatch({ + type: CONNECT.RECEIVE_WALLET_TYPE, + device, + hidden: true, + }); + } else { + dispatch({ + type: CONNECT.REQUEST_WALLET_TYPE, + device, + }); + } }; diff --git a/src/actions/TxActions.js b/src/actions/TxActions.js index 07f62753..6e947572 100644 --- a/src/actions/TxActions.js +++ b/src/actions/TxActions.js @@ -21,7 +21,7 @@ type EthereumTxRequest = { data: string, gasLimit: string, gasPrice: string, - nonce: number, + nonce: string, }; export const prepareEthereumTx = ( @@ -54,9 +54,6 @@ export const prepareEthereumTx = ( nonce: toHex(tx.nonce), gasLimit: toHex(tx.gasLimit), gasPrice: toHex(EthereumjsUnits.convert(tx.gasPrice, 'gwei', 'wei')), - r: '', - s: '', - v: '', }; }; diff --git a/src/actions/WalletActions.js b/src/actions/WalletActions.js index ed84b5e8..5dd0b33d 100644 --- a/src/actions/WalletActions.js +++ b/src/actions/WalletActions.js @@ -144,7 +144,7 @@ export const clearUnavailableDevicesData = (prevState: State, device: Device): T d => d.features && device.features && - d.features.device_id === device.features.device_id && + d.id === device.id && d.features.passphrase_protection !== device.features.passphrase_protection ); diff --git a/src/actions/Web3Actions.js b/src/actions/Web3Actions.js index 599bc1e8..3c6bcb6f 100644 --- a/src/actions/Web3Actions.js +++ b/src/actions/Web3Actions.js @@ -11,13 +11,11 @@ import * as ethUtils from 'utils/ethUtils'; import type { Dispatch, GetState, ThunkAction, PromiseAction } from 'flowtype'; -import type { EthereumAccount } from 'trezor-connect'; import type { Account } from 'reducers/AccountsReducer'; import type { Web3Instance } from 'reducers/Web3Reducer'; import type { Token } from 'reducers/TokensReducer'; import type { NetworkToken } from 'reducers/LocalStorageReducer'; import * as TokenActions from './TokenActions'; -import * as AccountsActions from './AccountsActions'; export type Web3UpdateBlockAction = { type: typeof WEB3.BLOCK_UPDATED, @@ -127,22 +125,22 @@ export const initWeb3 = ( web3.currentProvider.on('error', onEnd); }); -export const discoverAccount = ( - descriptor: string, - network: string -): PromiseAction => async (dispatch: Dispatch): Promise => { - const instance: Web3Instance = await dispatch(initWeb3(network)); - const balance = await instance.web3.eth.getBalance(descriptor); - const nonce = await instance.web3.eth.getTransactionCount(descriptor); - return { - descriptor, - transactions: 0, - block: 0, - balance: EthereumjsUnits.convert(balance, 'wei', 'ether'), - availableBalance: EthereumjsUnits.convert(balance, 'wei', 'ether'), - nonce, - }; -}; +// not used since connect@8 +// export const discoverAccount = (descriptor: string, network: string): PromiseAction => async ( +// dispatch: Dispatch +// ): Promise => { +// const instance: Web3Instance = await dispatch(initWeb3(network)); +// const balance = await instance.web3.eth.getBalance(descriptor); +// const nonce = await instance.web3.eth.getTransactionCount(descriptor); +// return { +// descriptor, +// transactions: 0, +// block: 0, +// balance: EthereumjsUnits.convert(balance, 'wei', 'ether'), +// availableBalance: EthereumjsUnits.convert(balance, 'wei', 'ether'), +// nonce, +// }; +// }; export const resolvePendingTransactions = (network: string): PromiseAction => async ( dispatch: Dispatch, @@ -151,24 +149,24 @@ export const resolvePendingTransactions = (network: string): PromiseAction const instance: Web3Instance = await dispatch(initWeb3(network)); const pending = getState().pending.filter(p => p.network === network); pending.forEach(async tx => { - const status = await instance.web3.eth.getTransaction(tx.hash); + const status = await instance.web3.eth.getTransaction(tx.txid); if (!status) { dispatch({ type: PENDING.TX_REJECTED, - hash: tx.hash, + hash: tx.txid, }); } else { - const receipt = await instance.web3.eth.getTransactionReceipt(tx.hash); + const receipt = await instance.web3.eth.getTransactionReceipt(tx.txid); if (receipt) { if (status.gas !== receipt.gasUsed) { dispatch({ type: PENDING.TX_TOKEN_ERROR, - hash: tx.hash, + hash: tx.txid, }); } dispatch({ type: PENDING.TX_RESOLVED, - hash: tx.hash, + hash: tx.txid, }); } } @@ -205,30 +203,31 @@ export const getTxInput = (): PromiseAction => async (dispatch: Dispatch): }; */ -export const updateAccount = ( - account: Account, - newAccount: EthereumAccount, - network: string -): PromiseAction => async (dispatch: Dispatch): Promise => { - const instance: Web3Instance = await dispatch(initWeb3(network)); - const balance = await instance.web3.eth.getBalance(account.descriptor); - const nonce = await instance.web3.eth.getTransactionCount(account.descriptor); - const empty = nonce <= 0 && balance === '0'; - dispatch( - AccountsActions.update({ - networkType: 'ethereum', - ...account, - ...newAccount, - empty, - nonce, - balance: EthereumjsUnits.convert(balance, 'wei', 'ether'), - availableBalance: EthereumjsUnits.convert(balance, 'wei', 'ether'), - }) - ); - - // update tokens for this account - dispatch(updateAccountTokens(account)); -}; +// not used since connect@8 +// export const updateAccount = ( +// account: Account, +// newAccount: any, +// network: string +// ): PromiseAction => async (dispatch: Dispatch): Promise => { +// const instance: Web3Instance = await dispatch(initWeb3(network)); +// const balance = await instance.web3.eth.getBalance(account.descriptor); +// const nonce = await instance.web3.eth.getTransactionCount(account.descriptor); +// const empty = nonce <= 0 && balance === '0'; +// dispatch( +// AccountsActions.update({ +// networkType: 'ethereum', +// ...account, +// ...newAccount, +// empty, +// nonce, +// balance: EthereumjsUnits.convert(balance, 'wei', 'ether'), +// availableBalance: EthereumjsUnits.convert(balance, 'wei', 'ether'), +// }) +// ); + +// // update tokens for this account +// dispatch(updateAccountTokens(account)); +// }; export const updateAccountTokens = (account: Account): PromiseAction => async ( dispatch: Dispatch, diff --git a/src/actions/ethereum/BlockchainActions.js b/src/actions/ethereum/BlockchainActions.js index 9ceb83e4..ac299de1 100644 --- a/src/actions/ethereum/BlockchainActions.js +++ b/src/actions/ethereum/BlockchainActions.js @@ -3,47 +3,14 @@ import TrezorConnect from 'trezor-connect'; import BigNumber from 'bignumber.js'; import * as PENDING from 'actions/constants/pendingTx'; +import * as AccountsActions from 'actions/AccountsActions'; +import * as Web3Actions from 'actions/Web3Actions'; +import { mergeAccount, enhanceTransaction } from 'utils/accountUtils'; -import type { TrezorDevice, Dispatch, GetState, PromiseAction } from 'flowtype'; -import type { EthereumAccount, BlockchainNotification } from 'trezor-connect'; +import type { Dispatch, GetState, PromiseAction, Network } from 'flowtype'; +import type { BlockchainNotification } from 'trezor-connect'; import type { Token } from 'reducers/TokensReducer'; import type { NetworkToken } from 'reducers/LocalStorageReducer'; -import * as Web3Actions from 'actions/Web3Actions'; -import * as AccountsActions from 'actions/AccountsActions'; - -export const discoverAccount = ( - device: TrezorDevice, - descriptor: string, - network: string -): PromiseAction => async (dispatch: Dispatch): Promise => { - // get data from connect - const txs = await TrezorConnect.ethereumGetAccountInfo({ - account: { - descriptor, - block: 0, - transactions: 0, - balance: '0', - availableBalance: '0', - nonce: 0, - }, - coin: network, - }); - - if (!txs.success) { - throw new Error(txs.payload.error); - } - - // blockbook web3 fallback - const web3account = await dispatch(Web3Actions.discoverAccount(descriptor, network)); - return { - descriptor, - transactions: txs.payload.transactions, - block: txs.payload.block, - balance: web3account.balance, - availableBalance: web3account.balance, - nonce: web3account.nonce, - }; -}; export const getTokenInfo = (input: string, network: string): PromiseAction => async ( dispatch: Dispatch @@ -103,9 +70,9 @@ export const subscribe = (network: string): PromiseAction => async ( dispatch: Dispatch, getState: GetState ): Promise => { - const accounts: Array = getState() + const accounts = getState() .accounts.filter(a => a.network === network) - .map(a => a.descriptor); // eslint-disable-line no-unused-vars + .map(a => ({ descriptor: a.descriptor })); const response = await TrezorConnect.blockchainSubscribe({ accounts, coin: network, @@ -115,7 +82,7 @@ export const subscribe = (network: string): PromiseAction => async ( await dispatch(Web3Actions.initWeb3(network)); }; -export const onBlockMined = (network: string): PromiseAction => async ( +export const onBlockMined = (network: Network): PromiseAction => async ( dispatch: Dispatch, getState: GetState ): Promise => { @@ -123,56 +90,52 @@ export const onBlockMined = (network: string): PromiseAction => async ( // check latest saved transaction blockhash against blockhheight // try to resolve pending transactions - await dispatch(Web3Actions.resolvePendingTransactions(network)); - - await dispatch(Web3Actions.updateGasPrice(network)); - - const accounts: Array = getState().accounts.filter(a => a.network === network); - if (accounts.length > 0) { - // find out which account changed - const response = await TrezorConnect.ethereumGetAccountInfo({ - accounts, - coin: network, - }); - - if (response.success) { - response.payload.forEach((a, i) => { - if (a.transactions > 0) { - // load additional data from Web3 (balance, nonce, tokens) - dispatch(Web3Actions.updateAccount(accounts[i], a, network)); - } else { - // there are no new txs, just update block - // TODO: There still could be internal transactions as a result of contract - // If that's the case, account balance won't be updated - // Currently waiting for deprecating web3 and utilising new blockbook - dispatch(AccountsActions.update({ ...accounts[i], block: a.block })); - - // HACK: since blockbook can't work with smart contracts for now - // try to update tokens balances added to this account using Web3 - dispatch(Web3Actions.updateAccountTokens(accounts[i])); - } - }); - } - } + await dispatch(Web3Actions.resolvePendingTransactions(network.shortcut)); + + await dispatch(Web3Actions.updateGasPrice(network.shortcut)); + + const accounts = getState().accounts.filter(a => a.network === network.shortcut); + if (accounts.length === 0) return; + const blockchain = getState().blockchain.find(b => b.shortcut === network.shortcut); + if (!blockchain) return; // flowtype fallback + + // find out which account changed + const bundle = accounts.map(a => ({ descriptor: a.descriptor, coin: network.shortcut })); + const response = await TrezorConnect.getAccountInfo({ bundle }); + + if (!response.success) return; + + response.payload.forEach((info, i) => { + dispatch( + AccountsActions.update(mergeAccount(info, accounts[i], network, blockchain.block)) + ); + dispatch(Web3Actions.updateAccountTokens(accounts[i])); + }); }; export const onNotification = ( - payload: $ElementType + payload: BlockchainNotification, + network: Network ): PromiseAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const { notification } = payload; - const account = getState().accounts.find(a => a.descriptor === notification.descriptor); - if (!account) return; - - if (!notification.blockHeight) { - dispatch({ - type: PENDING.ADD, - payload: { - ...notification, - deviceState: account.deviceState, - network: account.network, - }, - }); - } + const { descriptor, tx } = payload.notification; + const account = getState().accounts.find(a => a.descriptor === descriptor); + const blockchain = getState().blockchain.find(b => b.shortcut === network.shortcut); + if (!account || !blockchain) return; + dispatch({ + type: PENDING.ADD, + payload: enhanceTransaction(account, tx, network), + }); + + const response = await TrezorConnect.getAccountInfo({ + descriptor: account.descriptor, + coin: account.network, + }); + + if (!response.success) return; + + dispatch( + AccountsActions.update(mergeAccount(response.payload, account, network, blockchain.block)) + ); }; export const onError = (network: string): PromiseAction => async ( diff --git a/src/actions/ethereum/DiscoveryActions.js b/src/actions/ethereum/DiscoveryActions.js index d7b8bc03..f5634485 100644 --- a/src/actions/ethereum/DiscoveryActions.js +++ b/src/actions/ethereum/DiscoveryActions.js @@ -1,10 +1,9 @@ /* @flow */ import TrezorConnect from 'trezor-connect'; -import EthereumjsUtil from 'ethereumjs-util'; import * as DISCOVERY from 'actions/constants/discovery'; -import * as BlockchainActions from 'actions/ethereum/BlockchainActions'; - +// import { createAccountTokens } from 'actions/TokenActions'; +import { enhanceAccount } from 'utils/accountUtils'; import type { PromiseAction, Dispatch, GetState, TrezorDevice, Network, Account } from 'flowtype'; import type { Discovery } from 'reducers/DiscoveryReducer'; @@ -13,30 +12,41 @@ export type DiscoveryStartAction = { networkType: 'ethereum', network: Network, device: TrezorDevice, - publicKey: string, - chainCode: string, - basePath: Array, }; -// first iteration -// generate public key for this account -// start discovery process export const begin = ( device: TrezorDevice, network: Network -): PromiseAction => async (): Promise => { - // get xpub from TREZOR - const response = await TrezorConnect.getPublicKey({ +): PromiseAction => async (): Promise => ({ + type: DISCOVERY.START, + networkType: 'ethereum', + network, + device, +}); + +export const discoverAccount = ( + device: TrezorDevice, + discoveryProcess: Discovery +): PromiseAction => async (dispatch: Dispatch, getState: GetState): Promise => { + const { config } = getState().localStorage; + const network = config.networks.find(c => c.shortcut === discoveryProcess.network); + if (!network) throw new Error('Discovery network not found'); + + const { accountIndex } = discoveryProcess; + const path = network.bip44.slice(0).replace('a', accountIndex.toString()); + + const response = await TrezorConnect.getAccountInfo({ device: { path: device.path, instance: device.instance, state: device.state, }, - path: network.bip44, + path, + details: 'tokenBalances', + pageSize: 1, keepSession: true, // acquire and hold session - //useEmptyPassphrase: !device.instance, useEmptyPassphrase: device.useEmptyPassphrase, - network: network.name, + coin: network.shortcut, }); // handle TREZOR response error @@ -44,59 +54,15 @@ export const begin = ( throw new Error(response.payload.error); } - const basePath: Array = response.payload.path; - - return { - type: DISCOVERY.START, - networkType: 'ethereum', + const account = enhanceAccount(response.payload, { + index: discoveryProcess.accountIndex, network, device, - publicKey: response.payload.publicKey, - chainCode: response.payload.chainCode, - basePath, - }; -}; - -export const discoverAccount = ( - device: TrezorDevice, - discoveryProcess: Discovery -): PromiseAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const { config } = getState().localStorage; - const network = config.networks.find(c => c.shortcut === discoveryProcess.network); - if (!network) throw new Error('Discovery network not found'); - - 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); - - // TODO: check if address was created before - const account = await dispatch( - BlockchainActions.discoverAccount(device, ethAddress, network.shortcut) - ); - - // const accountIsEmpty = account.transactions <= 0 && account.nonce <= 0 && account.balance === '0'; - const empty = account.nonce <= 0 && account.balance === '0'; - - return { - imported: false, - index: discoveryProcess.accountIndex, - network: network.shortcut, - deviceID: device.features ? device.features.device_id : '0', - deviceState: device.state || '0', - accountPath: path, - descriptor: ethAddress, + }); - balance: account.balance, - availableBalance: account.balance, - block: account.block, - transactions: account.transactions, - empty, + // if (response.payload.tokens) { + // dispatch(createAccountTokens(account, response.payload.tokens)); + // } - networkType: 'ethereum', - nonce: account.nonce, - }; + return account; }; diff --git a/src/actions/ethereum/SendFormActions.js b/src/actions/ethereum/SendFormActions.js index 471c8ede..f4b9c07d 100644 --- a/src/actions/ethereum/SendFormActions.js +++ b/src/actions/ethereum/SendFormActions.js @@ -486,7 +486,7 @@ export const onGasPriceChange = (gasPrice: string): ThunkAction => ( ): void => { const state: State = getState().sendFormEthereum; // switch to custom fee level - let newSelectedFeeLevel = state.selectedFeeLevel; + let newSelectedFeeLevel; if (state.selectedFeeLevel.value !== 'Custom') newSelectedFeeLevel = state.feeLevels.find(f => f.value === 'Custom'); @@ -498,7 +498,7 @@ export const onGasPriceChange = (gasPrice: string): ThunkAction => ( untouched: false, touched: { ...state.touched, gasPrice: true }, gasPrice, - selectedFeeLevel: newSelectedFeeLevel, + selectedFeeLevel: newSelectedFeeLevel || state.selectedFeeLevel, }, }); }; @@ -673,9 +673,12 @@ export const onSend = (): AsyncAction => async ( const currentState: State = getState().sendFormEthereum; - const isToken: boolean = currentState.currency !== currentState.networkSymbol; - const pendingNonce: number = reducerUtils.getPendingSequence(pending); - const nonce = pendingNonce > 0 && pendingNonce >= account.nonce ? pendingNonce : account.nonce; + const isToken = currentState.currency !== currentState.networkSymbol; + const pendingNonce = new BigNumber(reducerUtils.getPendingSequence(pending)); + const nonce = + pendingNonce.gt(0) && pendingNonce.gt(account.nonce) + ? pendingNonce.toString() + : account.nonce; const txData = await dispatch( prepareEthereumTx({ @@ -727,12 +730,15 @@ export const onSend = (): AsyncAction => async ( return; } - txData.r = signedTransaction.payload.r; - txData.s = signedTransaction.payload.s; - txData.v = signedTransaction.payload.v; - try { - const serializedTx: string = await dispatch(serializeEthereumTx(txData)); + const serializedTx: string = await dispatch( + serializeEthereumTx({ + ...txData, + r: signedTransaction.payload.r, + s: signedTransaction.payload.s, + v: signedTransaction.payload.v, + }) + ); const push = await TrezorConnect.pushTransaction({ tx: serializedTx, coin: network.shortcut, @@ -746,58 +752,6 @@ export const onSend = (): AsyncAction => async ( dispatch({ type: SEND.TX_COMPLETE }); - // ugly blockbook workaround: - // since blockbook can't emit pending notifications - // need to trigger this event from here, where we know everything about this transaction - // blockchainNotification is 'trezor-connect' BlockchainLinkTransaction type - const fee = ValidationActions.calculateFee(currentState.gasLimit, currentState.gasPrice); - const blockchainNotification = { - type: 'send', - descriptor: account.descriptor, - inputs: [ - { - addresses: [account.descriptor], - amount: currentState.amount, - fee, - total: currentState.total, - }, - ], - outputs: [ - { - addresses: [currentState.address], - amount: currentState.amount, - }, - ], - hash: txid, - amount: currentState.amount, - fee, - total: currentState.total, - - sequence: nonce, - tokens: isToken - ? [ - { - name: currentState.currency, - shortcut: currentState.currency, - value: currentState.amount, - }, - ] - : undefined, - - blockHeight: 0, - blockHash: undefined, - timestamp: undefined, - }; - - dispatch( - BlockchainActions.onNotification({ - // $FlowIssue: missing coinInfo declaration - coin: {}, - notification: blockchainNotification, - }) - ); - // workaround end - // clear session storage dispatch(SessionStorageActions.clear()); diff --git a/src/actions/ripple/BlockchainActions.js b/src/actions/ripple/BlockchainActions.js index 8d19b9af..70d3ab87 100644 --- a/src/actions/ripple/BlockchainActions.js +++ b/src/actions/ripple/BlockchainActions.js @@ -4,7 +4,7 @@ import TrezorConnect from 'trezor-connect'; import * as BLOCKCHAIN from 'actions/constants/blockchain'; import * as PENDING from 'actions/constants/pendingTx'; import * as AccountsActions from 'actions/AccountsActions'; -import { toDecimalAmount } from 'utils/formatUtils'; +import { mergeAccount, enhanceTransaction } from 'utils/accountUtils'; import { observeChanges } from 'reducers/utils'; import type { BlockchainNotification } from 'trezor-connect'; @@ -20,10 +20,10 @@ import type { export const subscribe = (network: string): PromiseAction => async ( dispatch: Dispatch, getState: GetState -): Promise => { - const accounts: Array = getState() +) => { + const accounts = getState() .accounts.filter(a => a.network === network) - .map(a => a.descriptor); + .map(a => ({ descriptor: a.descriptor })); await TrezorConnect.blockchainSubscribe({ accounts, coin: network, @@ -47,161 +47,81 @@ export const getFeeLevels = (network: Network): PayloadAction => async ( +export const onBlockMined = (network: Network): PromiseAction => async ( dispatch: Dispatch, getState: GetState ): Promise => { - const blockchain = getState().blockchain.find(b => b.shortcut === networkShortcut); + const blockchain = getState().blockchain.find(b => b.shortcut === network.shortcut); if (!blockchain) return; // flowtype fallback // if last update was more than 5 minutes ago const now = new Date().getTime(); if (blockchain.feeTimestamp < now - 300000) { const feeRequest = await TrezorConnect.blockchainEstimateFee({ - coin: networkShortcut, + request: { + feeLevels: 'smart', + }, + coin: network.shortcut, }); if (feeRequest.success && observeChanges(blockchain.feeLevels, feeRequest.payload)) { // check if downloaded fee levels are different dispatch({ type: BLOCKCHAIN.UPDATE_FEE, - shortcut: networkShortcut, - feeLevels: feeRequest.payload, + shortcut: network.shortcut, + feeLevels: feeRequest.payload.levels.map(l => ({ + name: 'Normal', + value: l.feePerUnit, + })), }); } } // TODO: check for blockchain rollbacks here! - const accounts: Array = getState().accounts.filter(a => a.network === networkShortcut); + const accounts = getState().accounts.filter(a => a.network === network.shortcut); if (accounts.length === 0) return; - const { networks } = getState().localStorage.config; - const network = networks.find(c => c.shortcut === networkShortcut); - if (!network) return; - - // HACK: Since Connect always returns account.transactions as 0 - // we don't have info about new transactions for the account since last update. - // Untill there is a better solution compare accounts block. - // If we missed some blocks (wallet was offline) we'll update the account - // If we are update to date with the last block that means wallet was online - // and we would get Blockchain notification about new transaction if needed - accounts.forEach(async account => { - const missingBlocks = account.block !== block - 1; - if (!missingBlocks) { - // account was last updated on account.block, current block is +1, we didn't miss single block - // if there was new tx, blockchain notification would let us know - // so just update the block for the account - dispatch( - AccountsActions.update({ - ...account, - block, - }) - ); - } else { - // we missed some blocks (wallet was offline). get updated account info from connect - const response = await TrezorConnect.rippleGetAccountInfo({ - account: { - descriptor: account.descriptor, - }, - level: 'transactions', - coin: networkShortcut, - }); - if (!response.success) return; - - const updatedAccount = response.payload; - - // new txs - dispatch( - AccountsActions.update({ - ...account, - balance: toDecimalAmount(updatedAccount.balance, network.decimals), - availableBalance: toDecimalAmount( - updatedAccount.availableBalance, - network.decimals - ), - block: updatedAccount.block, - sequence: updatedAccount.sequence, - }) - ); - } + const bundle = accounts.map(a => ({ descriptor: a.descriptor, coin: network.shortcut })); + const response = await TrezorConnect.getAccountInfo({ bundle }); + + if (!response.success) return; + + response.payload.forEach((info, i) => { + dispatch( + AccountsActions.update(mergeAccount(info, accounts[i], network, blockchain.block)) + ); }); }; export const onNotification = ( - payload: $ElementType + payload: BlockchainNotification, + network: Network ): PromiseAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const { notification } = payload; - const account = getState().accounts.find(a => a.descriptor === notification.descriptor); - if (!account) return; - const { network } = getState().selectedAccount; - if (!network) return; // flowtype fallback + const { descriptor, tx } = payload.notification; + const account = getState().accounts.find(a => a.descriptor === descriptor); + const blockchain = getState().blockchain.find(b => b.shortcut === network.shortcut); + if (!account || !blockchain) return; - if (!notification.blockHeight) { + if (!tx.blockHeight) { dispatch({ type: PENDING.ADD, - payload: { - ...notification, - deviceState: account.deviceState, - network: account.network, - - amount: toDecimalAmount(notification.amount, network.decimals), - total: - notification.type === 'send' - ? toDecimalAmount(notification.total, network.decimals) - : toDecimalAmount(notification.amount, network.decimals), - fee: toDecimalAmount(notification.fee, network.decimals), - }, + payload: enhanceTransaction(account, tx, network), }); - - // todo: replace "send success" notification with link to explorer } else { dispatch({ type: PENDING.TX_RESOLVED, - hash: notification.hash, + hash: tx.txid, }); } - // In case of tx sent between two Trezor accounts there is a possibility that only 1 notification will be received - // therefore we need to find target account and update data for it as well - const accountsToUpdate = [account]; - const targetAddress = - notification.type === 'send' - ? notification.outputs[0].addresses[0] - : notification.inputs[0].addresses[0]; - - const targetAccount = getState().accounts.find(a => a.descriptor === targetAddress); - if (targetAccount) { - accountsToUpdate.push(targetAccount); - } + const response = await TrezorConnect.getAccountInfo({ + descriptor: account.descriptor, + coin: account.network, + }); - accountsToUpdate.forEach(async a => { - const response = await TrezorConnect.rippleGetAccountInfo({ - account: { - descriptor: a.descriptor, - from: a.block, - history: false, - }, - coin: a.network, - }); + if (!response.success) return; - if (response.success) { - const updatedAccount = response.payload; - const empty = updatedAccount.sequence <= 0 && updatedAccount.balance === '0'; - dispatch( - AccountsActions.update({ - networkType: 'ripple', - ...a, - balance: toDecimalAmount(updatedAccount.balance, network.decimals), - availableBalance: toDecimalAmount( - updatedAccount.availableBalance, - network.decimals - ), - block: updatedAccount.block, - sequence: updatedAccount.sequence, - reserve: toDecimalAmount(updatedAccount.reserve, network.decimals), - empty, - }) - ); - } - }); + dispatch( + AccountsActions.update(mergeAccount(response.payload, account, network, blockchain.block)) + ); }; diff --git a/src/actions/ripple/DiscoveryActions.js b/src/actions/ripple/DiscoveryActions.js index 6ae2a332..0683b3da 100644 --- a/src/actions/ripple/DiscoveryActions.js +++ b/src/actions/ripple/DiscoveryActions.js @@ -2,7 +2,7 @@ import TrezorConnect from 'trezor-connect'; import * as DISCOVERY from 'actions/constants/discovery'; -import { toDecimalAmount } from 'utils/formatUtils'; +import { enhanceAccount } from 'utils/accountUtils'; import type { PromiseAction, GetState, Dispatch, TrezorDevice, Network, Account } from 'flowtype'; import type { Discovery } from 'reducers/DiscoveryReducer'; @@ -35,16 +35,13 @@ export const discoverAccount = ( const { accountIndex } = discoveryProcess; const path = network.bip44.slice(0).replace('a', accountIndex.toString()); - const response = await TrezorConnect.rippleGetAccountInfo({ + const response = await TrezorConnect.getAccountInfo({ device: { path: device.path, instance: device.instance, state: device.state, }, - account: { - path, - block: 0, - }, + path, keepSession: true, // acquire and hold session useEmptyPassphrase: device.useEmptyPassphrase, coin: network.shortcut, @@ -55,26 +52,9 @@ export const discoverAccount = ( throw new Error(response.payload.error); } - const account = response.payload; - const empty = account.sequence <= 0 && account.balance === '0'; - - return { - imported: false, + return enhanceAccount(response.payload, { index: discoveryProcess.accountIndex, - network: network.shortcut, - deviceID: device.features ? device.features.device_id : '0', - deviceState: device.state || '0', - accountPath: account.path || [], - descriptor: account.descriptor, - - balance: toDecimalAmount(account.balance, network.decimals), - availableBalance: toDecimalAmount(account.availableBalance, network.decimals), - block: account.block, - transactions: account.transactions, - empty, - - networkType: 'ripple', - sequence: account.sequence, - reserve: toDecimalAmount(account.reserve, network.decimals), - }; + network, + device, + }); }; diff --git a/src/actions/ripple/SendFormActions.js b/src/actions/ripple/SendFormActions.js index fce5ef8b..eefacdfe 100644 --- a/src/actions/ripple/SendFormActions.js +++ b/src/actions/ripple/SendFormActions.js @@ -1,6 +1,7 @@ /* @flow */ import React from 'react'; import { FormattedMessage } from 'react-intl'; +import { Link } from 'trezor-ui-components'; import TrezorConnect from 'trezor-connect'; import * as NOTIFICATION from 'actions/constants/notification'; import * as SEND from 'actions/constants/send'; @@ -374,7 +375,7 @@ export const onFeeChange = (fee: string): ThunkAction => ( const state: State = getState().sendFormRipple; // switch to custom fee level - let newSelectedFeeLevel = state.selectedFeeLevel; + let newSelectedFeeLevel; if (state.selectedFeeLevel.value !== 'Custom') newSelectedFeeLevel = state.feeLevels.find(f => f.value === 'Custom'); @@ -385,7 +386,7 @@ export const onFeeChange = (fee: string): ThunkAction => ( ...state, untouched: false, touched: { ...state.touched, fee: true }, - selectedFeeLevel: newSelectedFeeLevel, + selectedFeeLevel: newSelectedFeeLevel || state.selectedFeeLevel, fee, }, }); @@ -501,7 +502,11 @@ export const onSend = (): AsyncAction => async ( payload: { variant: 'success', title: , - message: txid, + message: ( + + + + ), cancelable: true, actions: [], }, diff --git a/src/actions/ripple/SendFormValidationActions.js b/src/actions/ripple/SendFormValidationActions.js index 33ddcc06..dbe651bf 100644 --- a/src/actions/ripple/SendFormValidationActions.js +++ b/src/actions/ripple/SendFormValidationActions.js @@ -173,16 +173,14 @@ const addressBalanceValidation = ($state: State): PromiseAction => async ( if (!network) return; let minAmount: string = '0'; - const response = await TrezorConnect.rippleGetAccountInfo({ - account: { - descriptor: $state.address, - }, + const response = await TrezorConnect.getAccountInfo({ + descriptor: $state.address, coin: network.shortcut, }); if (response.success) { - const empty = response.payload.sequence <= 0 && response.payload.balance === '0'; - if (empty) { - minAmount = toDecimalAmount(response.payload.reserve, network.decimals); + if (response.payload.empty) { + const reserve = response.payload.misc ? response.payload.misc.reserve : '0'; + minAmount = toDecimalAmount(reserve || '0', network.decimals); } } diff --git a/src/components/Header/index.js b/src/components/Header/index.js index 622bc178..6f10760b 100644 --- a/src/components/Header/index.js +++ b/src/components/Header/index.js @@ -5,7 +5,7 @@ import { Header } from 'trezor-ui-components'; import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom'; -import type { toggleSidebar as toggleSidebarType } from 'actions/WalletActions'; +import { toggleSidebar as toggleSidebarAction } from 'actions/WalletActions'; import l10nMessages from './index.messages'; import LanguagePicker from './components/LanguagePicker/Container'; @@ -13,7 +13,7 @@ import LanguagePicker from './components/LanguagePicker/Container'; type MyProps = { sidebarEnabled?: boolean, sidebarOpened?: ?boolean, - toggleSidebar?: toggleSidebarType, + toggleSidebar?: typeof toggleSidebarAction, }; const MyHeader = ({ sidebarEnabled, sidebarOpened, toggleSidebar }: MyProps) => ( diff --git a/src/components/Transaction/index.js b/src/components/Transaction/index.js index 2e035b2a..2ede36df 100644 --- a/src/components/Transaction/index.js +++ b/src/components/Transaction/index.js @@ -63,7 +63,7 @@ const Value = styled.div` text-align: right; color: ${colors.GREEN_SECONDARY}; - &.send { + &.sent { color: ${colors.ERROR_PRIMARY}; } `; @@ -77,28 +77,27 @@ const Fee = styled.div` `; const TransactionItem = ({ tx, network }: Props) => { - const url = `${network.explorer.tx}${tx.hash}`; - const date = typeof tx.timestamp === 'string' ? tx.timestamp : undefined; // TODO: format date - const addresses = (tx.type === 'send' ? tx.outputs : tx.inputs).reduce( - (arr, item) => arr.concat(item.addresses), - [] - ); - - const operation = tx.type === 'send' ? '-' : '+'; - const amount = tx.tokens ? ( - tx.tokens.map(t => ( - + const url = `${network.explorer.tx}${tx.txid}`; + const date = typeof tx.blockTime === 'number' ? tx.blockTime : undefined; // TODO: format date + const addresses = tx.targets.reduce((arr, item) => arr.concat(item.addresses), []); + + const operation = tx.type === 'sent' ? '-' : '+'; + const amount = + tx.tokens.length > 0 ? ( + tx.tokens.map(t => ( + + {operation} + {t.amount} {t.symbol} + + )) + ) : ( + {operation} - {t.value} {t.shortcut} + {tx.amount} {network.symbol} - )) - ) : ( - - {operation} - {tx.total} {network.symbol} - - ); - const fee = tx.tokens && tx.type === 'send' ? `${tx.fee} ${network.symbol}` : undefined; + ); + const fee = + tx.tokens.length > 0 && tx.type === 'sent' ? `${tx.fee} ${network.symbol}` : undefined; return ( @@ -113,7 +112,7 @@ const TransactionItem = ({ tx, network }: Props) => { ))} {!tx.blockHeight && ( - Transaction hash: {tx.hash} + Transaction hash: {tx.txid} )} diff --git a/src/components/images/DeviceIcon/index.js b/src/components/images/DeviceIcon/index.js index 8ae997ad..853c8886 100644 --- a/src/components/images/DeviceIcon/index.js +++ b/src/components/images/DeviceIcon/index.js @@ -30,20 +30,10 @@ const DeviceIcon = ({ color = COLORS.TEXT_SECONDARY, hoverColor, onClick, - ...rest }: Props) => { const majorVersion = device.features ? device.features.major_version : 2; const icon = getDeviceIcon(majorVersion); - return ( - - ); + return ; }; DeviceIcon.propTypes = { diff --git a/src/components/modals/index.js b/src/components/modals/index.js index 955fc467..522b2607 100644 --- a/src/components/modals/index.js +++ b/src/components/modals/index.js @@ -16,6 +16,7 @@ import Pin from 'components/modals/pin/Pin'; import InvalidPin from 'components/modals/pin/Invalid'; import Passphrase from 'components/modals/passphrase/Passphrase'; import PassphraseType from 'components/modals/passphrase/Type'; +import PassphraseOnDevice from 'components/modals/passphrase/OnDevice'; import ConfirmSignTx from 'components/modals/confirm/SignTx'; import ConfirmAction from 'components/modals/confirm/Action'; import ConfirmUnverifiedAddress from 'components/modals/confirm/UnverifiedAddress'; @@ -97,6 +98,10 @@ const getDeviceContextModal = (props: Props) => { } } + case UI.REQUEST_PASSPHRASE_ON_DEVICE: + case 'ButtonRequest_PassphraseEntry': + return ; + case 'ButtonRequest_ProtectCall': return ; diff --git a/src/components/modals/passphrase/OnDevice/index.js b/src/components/modals/passphrase/OnDevice/index.js new file mode 100644 index 00000000..23e9df77 --- /dev/null +++ b/src/components/modals/passphrase/OnDevice/index.js @@ -0,0 +1,43 @@ +/* @flow */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; + +import { H5, P, colors } from 'trezor-ui-components'; +import DeviceIcon from 'components/images/DeviceIcon'; + +import type { TrezorDevice } from 'flowtype'; + +type Props = { + device: TrezorDevice, +}; + +const Wrapper = styled.div` + max-width: 360px; + padding: 30px 48px; +`; + +const StyledDeviceIcon = styled(DeviceIcon)` + margin-bottom: 10px; +`; + +const Header = styled.div``; + +const PassphraseType = (props: Props) => ( + +
+ +
Enter passphrase on {props.device.label} device
+

+ If you enter a wrong passphrase, you will not unlock the desired hidden wallet. +

+
+
+); + +PassphraseType.propTypes = { + device: PropTypes.object.isRequired, +}; + +export default PassphraseType; diff --git a/src/components/modals/passphrase/Passphrase/index.js b/src/components/modals/passphrase/Passphrase/index.js index 4377ce47..116a4f8c 100644 --- a/src/components/modals/passphrase/Passphrase/index.js +++ b/src/components/modals/passphrase/Passphrase/index.js @@ -73,6 +73,10 @@ const LinkButton = styled(Button)` } `; +const OnDeviceButton = styled(Button)` + margin: 10px 0 10px 0; +`; + class Passphrase extends PureComponent { constructor(props: Props) { super(props); @@ -169,7 +173,10 @@ class Passphrase extends PureComponent { })); } - submitPassphrase(shouldLeavePassphraseBlank: boolean = false) { + submitPassphrase( + shouldLeavePassphraseBlank: boolean = false, + passphraseOnDevice: boolean = false + ) { const { onPassphraseSubmit } = this.props; const passphrase = this.state.passphraseInputValue; @@ -181,7 +188,7 @@ class Passphrase extends PureComponent { isPassphraseHidden: true, }); - onPassphraseSubmit(shouldLeavePassphraseBlank ? '' : passphrase); + onPassphraseSubmit(shouldLeavePassphraseBlank ? '' : passphrase, passphraseOnDevice); } handleKeyPress(event: KeyboardEvent) { @@ -209,6 +216,13 @@ class Passphrase extends PureComponent { ); } + const { device } = this.props; + const onDeviceOffer = !!( + device.features && + device.features.capabilities && + device.features.capabilities.includes('Capability_PassphraseEntry') + ); + return (
@@ -270,6 +284,11 @@ class Passphrase extends PureComponent { + {onDeviceOffer && ( + this.submitPassphrase(false, true)}> + Enter passphrase on device + + )}