From 11bf91b093ced19347dc7876cb5714cbb63d3396 Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Mon, 30 Mar 2020 20:46:07 +0200 Subject: [PATCH] update SendForm and PendingTx using trezor-connect@8 --- src/actions/SignVerifyActions.js | 2 +- src/actions/TxActions.js | 5 +- src/actions/Web3Actions.js | 93 +++++++------- src/actions/ethereum/SendFormActions.js | 78 +++--------- src/actions/ripple/SendFormActions.js | 11 +- .../ripple/SendFormValidationActions.js | 12 +- src/components/Transaction/index.js | 43 ++++--- src/reducers/PendingTxReducer.js | 6 +- src/reducers/utils/index.js | 25 ++-- src/utils/accountUtils.js | 114 ++++++++++++++++++ .../components/PendingTransactions/index.js | 2 +- .../views/Account/Send/ethereum/index.js | 15 ++- 12 files changed, 232 insertions(+), 174 deletions(-) create mode 100644 src/utils/accountUtils.js 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/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/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/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/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/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/reducers/PendingTxReducer.js b/src/reducers/PendingTxReducer.js index 02bd2591..d8af632d 100644 --- a/src/reducers/PendingTxReducer.js +++ b/src/reducers/PendingTxReducer.js @@ -17,12 +17,12 @@ const add = (state: State, payload: Transaction): State => { const removeByDeviceState = (state: State, deviceState: ?string): State => state.filter(tx => tx.deviceState !== deviceState); -const removeByHash = (state: State, hash: string): State => state.filter(tx => tx.hash !== hash); +const removeByHash = (state: State, hash: string): State => state.filter(tx => tx.txid !== hash); const reject = (state: State, hash: string): State => state.map(tx => { - if (tx.hash === hash && !tx.rejected) { - return { ...tx, rejected: true }; + if (tx.txid === hash && !tx.rejected) { + return Object.assign({}, { rejected: true }, tx); } return tx; }); diff --git a/src/reducers/utils/index.js b/src/reducers/utils/index.js index e14862e9..8840dd4c 100644 --- a/src/reducers/utils/index.js +++ b/src/reducers/utils/index.js @@ -27,11 +27,7 @@ export const getSelectedDevice = (state: State): ?TrezorDevice => { if (d.mode === 'bootloader' && d.path === locationState.device) { return true; } - if ( - d.features && - d.features.device_id === locationState.device && - d.instance === instance - ) { + if (d.features && d.id === locationState.device && d.instance === instance) { return true; } return false; @@ -46,7 +42,7 @@ export const findDevice = ( ): ?TrezorDevice => devices.find(d => { // TODO: && (instance && d.instance === instance) - if (d.features && d.features.device_id === deviceId && d.state === deviceState) { + if (d.features && d.id === deviceId && d.state === deviceState) { return true; } return false; @@ -60,9 +56,7 @@ export const getDuplicateInstanceNumber = ( // find device(s) with the same features.device_id // and sort them by instance number const affectedDevices: Array = devices - .filter( - d => d.features && device.features && d.features.device_id === device.features.device_id - ) + .filter(d => d.features && device.features && d.id === device.id) .sort((a, b) => { if (!a.instance) { return -1; @@ -92,8 +86,7 @@ export const getSelectedAccount = (state: State): ?Account => { return state.accounts.find( a => a.imported === isImported && - (a.deviceState === device.state || - (a.imported && a.deviceID === (device.features || {}).device_id)) && + (a.deviceState === device.state || (a.imported && a.deviceID === device.id)) && a.index === index && a.network === locationState.network ); @@ -129,8 +122,8 @@ export const getAccountPendingTx = ( export const getPendingSequence = (pending: Array): number => pending.reduce((value: number, tx: Transaction): number => { - if (tx.rejected) return value; - return Math.max(value, tx.sequence + 1); + if (!tx.ethereumSpecific || tx.rejected) return value; + return Math.max(value, tx.ethereumSpecific.nonce + 1); }, 0); export const getPendingAmount = ( @@ -139,7 +132,7 @@ export const getPendingAmount = ( token: boolean = false ): BigNumber => pending.reduce((value: BigNumber, tx: Transaction): BigNumber => { - if (tx.type !== 'send') return value; + if (tx.type !== 'sent') return value; if (!token) { // regular transactions // add fees from token txs and amount from regular txs @@ -147,9 +140,9 @@ export const getPendingAmount = ( } if (tx.tokens) { // token transactions - const allTokens = tx.tokens.filter(t => t.shortcut === currency); + const allTokens = tx.tokens.filter(t => t.symbol === currency); const tokensValue: BigNumber = allTokens.reduce( - (tv, t) => new BigNumber(value).plus(t.value), + (tv, t) => new BigNumber(value).plus(t.amount), new BigNumber('0') ); return new BigNumber(value).plus(tokensValue); diff --git a/src/utils/accountUtils.js b/src/utils/accountUtils.js new file mode 100644 index 00000000..51eb6985 --- /dev/null +++ b/src/utils/accountUtils.js @@ -0,0 +1,114 @@ +/* @flow */ +import { toDecimalAmount } from 'utils/formatUtils'; +import type { AccountInfo, AccountTransaction } from 'trezor-connect'; +import type { Account, Transaction, Network, TrezorDevice } from 'flowtype'; + +// Merge fresh AccountInfo into existing Account +export const mergeAccount = ( + info: AccountInfo, + account: Account, + network: Network, + block: number +): Account => { + if (account.networkType === 'ethereum') { + const nonce = info.misc && info.misc.nonce ? info.misc.nonce : '0'; + return { + networkType: 'ethereum', + ...account, + balance: toDecimalAmount(info.balance, network.decimals), + availableBalance: toDecimalAmount(info.availableBalance, network.decimals), + block, + transactions: info.history.total, + empty: account.empty, + nonce, + }; + } + + if (account.networkType === 'ripple') { + const sequence = info.misc && info.misc.sequence ? info.misc.sequence : 0; + const reserve = info.misc && info.misc.reserve ? info.misc.reserve : '0'; + return { + ...account, + balance: toDecimalAmount(info.balance, network.decimals), + availableBalance: toDecimalAmount(info.availableBalance, network.decimals), + block, + empty: info.empty, + + networkType: 'ripple', + sequence, + reserve: toDecimalAmount(reserve || '0', network.decimals), + }; + } + + return account; +}; + +type EnhanceAccountOptions = { + index: number, + network: Network, + device: TrezorDevice, + imported?: boolean, + block?: number, +}; + +// Create Account from AccountInfo +export const enhanceAccount = (account: AccountInfo, options: EnhanceAccountOptions): Account => { + if (options.network.type === 'ethereum') { + const nonce = account.misc && account.misc.nonce ? account.misc.nonce : '0'; + return { + imported: !!options.imported, + index: options.index, + network: options.network.shortcut, + deviceID: options.device.id || '0', + deviceState: options.device.state || '0', + accountPath: account.path, + descriptor: account.descriptor, + + balance: toDecimalAmount(account.balance, options.network.decimals), + availableBalance: toDecimalAmount(account.availableBalance, options.network.decimals), + block: options.block || 0, + transactions: account.history.total, + empty: account.empty, + + networkType: 'ethereum', + nonce, + }; + } + + const sequence = account.misc && account.misc.sequence ? account.misc.sequence : 0; + const reserve = account.misc && account.misc.reserve ? account.misc.reserve : '0'; + return { + imported: !!options.imported, + index: options.index, + network: options.network.shortcut, + deviceID: options.device.id || '0', + deviceState: options.device.state || '0', + accountPath: account.path, + descriptor: account.descriptor, + + balance: toDecimalAmount(account.balance, options.network.decimals), + availableBalance: toDecimalAmount(account.availableBalance, options.network.decimals), + block: options.block || 0, + transactions: 0, + empty: account.empty, + + networkType: 'ripple', + sequence, + reserve: toDecimalAmount(reserve, options.network.decimals), + }; +}; + +export const enhanceTransaction = ( + account: Account, + tx: AccountTransaction, + network: Network +): Transaction => { + return { + ...tx, + descriptor: account.descriptor, + deviceState: account.deviceState, + network: account.network, + amount: toDecimalAmount(tx.amount, network.decimals), + fee: toDecimalAmount(tx.fee, network.decimals), + }; +}; diff --git a/src/views/Wallet/views/Account/Send/components/PendingTransactions/index.js b/src/views/Wallet/views/Account/Send/components/PendingTransactions/index.js index 6d0ab40b..66fffac0 100644 --- a/src/views/Wallet/views/Account/Send/components/PendingTransactions/index.js +++ b/src/views/Wallet/views/Account/Send/components/PendingTransactions/index.js @@ -31,7 +31,7 @@ const PendingTransactions = (props: Props) => { There are no pending transactions )} {pending.map(tx => ( - + ))} ); diff --git a/src/views/Wallet/views/Account/Send/ethereum/index.js b/src/views/Wallet/views/Account/Send/ethereum/index.js index 723bef68..7d0ff924 100644 --- a/src/views/Wallet/views/Account/Send/ethereum/index.js +++ b/src/views/Wallet/views/Account/Send/ethereum/index.js @@ -521,14 +521,13 @@ const AccountSend = (props: Props) => { )} - {props.selectedAccount.pending.length > 0 || - (account.imported && ( - - ))} + {props.selectedAccount.pending.length > 0 && ( + + )} ); };