From acf0db75b317e773a02c1203e949aa3f4d89a041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Thu, 24 May 2018 10:30:09 +0200 Subject: [PATCH 1/4] Porting to new live-common with changes in Account/Operation --- package.json | 2 +- src/bridge/EthereumJSBridge.js | 51 +++++++++--------- src/bridge/RippleJSBridge.js | 54 ++++++++++--------- src/bridge/makeMockBridge.js | 9 ++-- src/components/CurrentAddress/stories.js | 2 +- src/components/CurrentAddressForAccount.js | 9 +--- src/components/DeviceCheckAddress.js | 12 ++--- src/components/EnsureDeviceApp/index.js | 8 +-- .../OperationsList/ConfirmationCheck.js | 12 +++-- src/components/OperationsList/Operation.js | 33 ++++++------ src/components/modals/OperationDetails.js | 11 ++-- .../modals/Send/04-step-confirmation.js | 2 +- .../accounts/scanAccountsOnDevice.js | 48 ++++++++++++----- .../signAndBroadcastTransaction/btc.js | 2 +- static/i18n/en/operationsList.yml | 4 +- static/i18n/fr/operationsList.yml | 4 +- yarn.lock | 6 +-- 17 files changed, 145 insertions(+), 124 deletions(-) diff --git a/package.json b/package.json index d50c20e0..b0a307c1 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@ledgerhq/hw-transport": "^4.12.0", "@ledgerhq/hw-transport-node-hid": "^4.12.0", "@ledgerhq/ledger-core": "^1.2.0", - "@ledgerhq/live-common": "^2.7.5", + "@ledgerhq/live-common": "2.8.0-beta.2", "axios": "^0.18.0", "babel-runtime": "^6.26.0", "bcryptjs": "^2.4.3", diff --git a/src/bridge/EthereumJSBridge.js b/src/bridge/EthereumJSBridge.js index f902cf0f..2433d6dd 100644 --- a/src/bridge/EthereumJSBridge.js +++ b/src/bridge/EthereumJSBridge.js @@ -24,15 +24,17 @@ const EditFees = ({ account, onChange, value }: EditProps) => ( /> ) -const toAccountOperation = (account: Account) => (tx: Tx): Operation => { - const sending = account.address.toLowerCase() === tx.from.toLowerCase() +const toAccountOperation = (account: Account) => (tx: Tx): $Exact => { + const sending = account.freshAddress.toLowerCase() === tx.from.toLowerCase() + const receiving = account.freshAddress.toLowerCase() === tx.to.toLowerCase() + const type = sending && receiving ? 'SELF' : sending ? 'OUT' : 'IN' return { id: tx.hash, hash: tx.hash, - address: sending ? tx.to : tx.from, - amount: (sending ? -1 : 1) * tx.value, - blockHeight: (tx.block && tx.block.height) || 0, // FIXME will be optional field - blockHash: (tx.block && tx.block.hash) || '', // FIXME will be optional field + type, + value: tx.value, + blockHeight: tx.block && tx.block.height, + blockHash: tx.block && tx.block.hash, accountId: account.id, senders: [tx.from], recipients: [tx.to], @@ -56,7 +58,7 @@ const paginateMoreTransactions = async ( ): Promise => { const api = apiForCurrency(account.currency) const { txs } = await api.getTransactions( - account.address, + account.freshAddress, acc.length ? acc[acc.length - 1].blockHash : undefined, ) if (txs.length === 0) return acc @@ -93,7 +95,7 @@ const EthereumBridge: WalletBridge = { async function stepAddress( index, - { address, path }, + { address, path: freshAddressPath }, isStandard, ): { account?: Account, complete?: boolean } { const balance = await api.getAccountBalance(address) @@ -103,6 +105,9 @@ const EthereumBridge: WalletBridge = { const { txs } = await api.getTransactions(address) if (finished) return { complete: true } + const path = freshAddressPath // FIXME + const freshAddress = address + if (txs.length === 0) { // this is an empty account if (isStandard) { @@ -110,21 +115,20 @@ const EthereumBridge: WalletBridge = { // first zero account will emit one account as opportunity to create a new account.. const currentBlock = await fetchCurrentBlock(currency) const accountId = `${currency.id}_${address}` - const account: Account = { + const account: $Exact = { id: accountId, xpub: '', path, // FIXME we probably not want the address path in the account.path - walletPath: String(index), + freshAddress, + freshAddressPath, name: 'New Account', - isSegwit: false, - address, - addresses: [{ str: address, path }], balance, blockHeight: currentBlock.height, archived: true, index, currency, operations: [], + pendingOperations: [], unit: currency.units[0], lastSyncDate: new Date(), } @@ -137,21 +141,20 @@ const EthereumBridge: WalletBridge = { } const accountId = `${currency.id}_${address}` - const account: Account = { + const account: $Exact = { id: accountId, xpub: '', path, // FIXME we probably not want the address path in the account.path - walletPath: String(index), + freshAddress, + freshAddressPath, name: address.slice(32), - isSegwit: false, - address, - addresses: [{ str: address, path }], balance, blockHeight: currentBlock.height, archived: true, index, currency, operations: [], + pendingOperations: [], unit: currency.units[0], lastSyncDate: new Date(), } @@ -166,9 +169,9 @@ const EthereumBridge: WalletBridge = { for (const derivation of derivations) { const isStandard = last === derivation for (let index = 0; index < 255; index++) { - const path = derivation({ currency, x: index, segwit: false }) + const freshAddressPath = derivation({ currency, x: index, segwit: false }) const res = await getAddressCommand - .send({ currencyId: currency.id, devicePath: deviceId, path }) + .send({ currencyId: currency.id, devicePath: deviceId, path: freshAddressPath }) .toPromise() const r = await stepAddress(index, res, isStandard) if (r.account) next(r.account) @@ -188,7 +191,7 @@ const EthereumBridge: WalletBridge = { return { unsubscribe } }, - synchronize({ address, blockHeight, currency }, { next, complete, error }) { + synchronize({ freshAddress, blockHeight, currency }, { next, complete, error }) { let unsubscribed = false const api = apiForCurrency(currency) async function main() { @@ -198,9 +201,9 @@ const EthereumBridge: WalletBridge = { if (block.height === blockHeight) { complete() } else { - const balance = await api.getAccountBalance(address) + const balance = await api.getAccountBalance(freshAddress) if (unsubscribed) return - const { txs } = await api.getTransactions(address) + const { txs } = await api.getTransactions(freshAddress) if (unsubscribed) return next(a => { const currentOps = a.operations @@ -277,7 +280,7 @@ const EthereumBridge: WalletBridge = { signAndBroadcast: async (a, t, deviceId) => { const api = apiForCurrency(a.currency) - const nonce = await api.getAccountNonce(a.address) + const nonce = await api.getAccountNonce(a.freshAddress) const transaction = await signTransactionCommand .send({ diff --git a/src/bridge/RippleJSBridge.js b/src/bridge/RippleJSBridge.js index d238aff7..b44e8ca1 100644 --- a/src/bridge/RippleJSBridge.js +++ b/src/bridge/RippleJSBridge.js @@ -122,19 +122,18 @@ const txToOperation = (account: Account) => ({ outcome: { deliveredAmount, ledgerVersion, timestamp }, specification: { source, destination }, }: Tx): Operation => { - const sending = source.address === account.address - const amount = - (sending ? -1 : 1) * (deliveredAmount ? parseAPICurrencyObject(deliveredAmount) : 0) - const op: Operation = { + const type = source.address === account.freshAddress ? 'OUT' : 'IN' + const value = deliveredAmount ? parseAPICurrencyObject(deliveredAmount) : 0 + const op: $Exact = { id, hash: id, accountId: account.id, - blockHash: '', - address: sending ? destination.address : source.address, - amount, + type, + value, + blockHash: null, blockHeight: ledgerVersion, - senders: [sending ? destination.address : source.address], - recipients: [!sending ? destination.address : source.address], + senders: [source.address], + recipients: [destination.address], date: new Date(timestamp), } return op @@ -159,9 +158,11 @@ const RippleJSBridge: WalletBridge = { const derivations = getDerivations(currency) for (const derivation of derivations) { for (let index = 0; index < 255; index++) { - const path = derivation({ currency, x: index, segwit: false }) + const freshAddressPath = derivation({ currency, x: index, segwit: false }) + const path = freshAddressPath + // FIXME^ we need the account path, not the address path const { address } = await await getAddress - .send({ currencyId: currency.id, devicePath: deviceId, path }) + .send({ currencyId: currency.id, devicePath: deviceId, path: freshAddressPath }) .toPromise() if (finished) return @@ -176,6 +177,9 @@ const RippleJSBridge: WalletBridge = { } } + // fresh address is address. ripple never changes. + const freshAddress = address + if (!info) { // account does not exist in Ripple server // we are generating a new account locally @@ -183,16 +187,15 @@ const RippleJSBridge: WalletBridge = { id: accountId, xpub: '', path, - walletPath: '', name: 'New Account', - isSegwit: false, - address, - addresses: [address], + freshAddress, + freshAddressPath, balance: 0, blockHeight: maxLedgerVersion, index, currency, operations: [], + pendingOperations: [], unit: currency.units[0], archived: true, lastSyncDate: new Date(), @@ -212,20 +215,19 @@ const RippleJSBridge: WalletBridge = { }) if (finished) return - const account: Account = { + const account: $Exact = { id: accountId, xpub: '', path, - walletPath: '', name: address.slice(0, 8), - isSegwit: false, - address, - addresses: [address], + freshAddress, + freshAddressPath, balance, blockHeight: maxLedgerVersion, index, currency, operations: [], + pendingOperations: [], unit: currency.units[0], archived: true, lastSyncDate: new Date(), @@ -247,7 +249,7 @@ const RippleJSBridge: WalletBridge = { return { unsubscribe } }, - synchronize({ currency, address, blockHeight }, { next, error, complete }) { + synchronize({ currency, freshAddress, blockHeight }, { next, error, complete }) { let finished = false const unsubscribe = () => { finished = true @@ -266,7 +268,7 @@ const RippleJSBridge: WalletBridge = { let info try { - info = await api.getAccountInfo(address) + info = await api.getAccountInfo(freshAddress) } catch (e) { if (e.message !== 'actNotFound') { throw e @@ -282,12 +284,12 @@ const RippleJSBridge: WalletBridge = { const balance = parseAPIValue(info.xrpBalance) if (isNaN(balance) || !isFinite(balance)) { - throw new Error(`Ripple: invalid balance=${balance} for address ${address}`) + throw new Error(`Ripple: invalid balance=${balance} for address ${freshAddress}`) } next(a => ({ ...a, balance })) - const transactions = await api.getTransactions(address, { + const transactions = await api.getTransactions(freshAddress, { minLedgerVersion: Math.max(blockHeight, minLedgerVersion), maxLedgerVersion, }) @@ -362,7 +364,7 @@ const RippleJSBridge: WalletBridge = { const amount = formatAPICurrencyXRP(t.amount) const payment = { source: { - address: a.address, + address: a.freshAddress, amount, }, destination: { @@ -375,7 +377,7 @@ const RippleJSBridge: WalletBridge = { fee: formatAPICurrencyXRP(t.fee).value, } - const prepared = await api.preparePayment(a.address, payment, instruction) + const prepared = await api.preparePayment(a.freshAddress, payment, instruction) const transaction = await signTransaction .send({ diff --git a/src/bridge/makeMockBridge.js b/src/bridge/makeMockBridge.js index 1bc2eef9..87931642 100644 --- a/src/bridge/makeMockBridge.js +++ b/src/bridge/makeMockBridge.js @@ -4,6 +4,7 @@ import { genAddingOperationsInAccount, genOperation, } from '@ledgerhq/live-common/lib/mock/account' +import { getOperationAmountNumber } from '@ledgerhq/live-common/lib/helpers/operation' import Prando from 'prando' import type { Operation } from '@ledgerhq/live-common/lib/types' import type { WalletBridge } from './types' @@ -53,7 +54,7 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> { account = { ...account } account.blockHeight++ for (const op of ops) { - account.balance += op.amount + account.balance += getOperationAmountNumber(op) } return account }) @@ -149,8 +150,10 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> { signAndBroadcast: async (account, t) => { const rng = new Prando() const op = genOperation(account, account.operations, account.currency, rng) - op.amount = -t.amount - op.address = t.recipient + op.type = 'OUT' + op.value = t.amount + op.senders = [account.freshAddress] + op.recipients = [t.recipient] op.blockHeight = account.blockHeight op.date = new Date() broadcasted[account.id] = (broadcasted[account.id] || []).concat(op) diff --git a/src/components/CurrentAddress/stories.js b/src/components/CurrentAddress/stories.js index bc665a32..86ceae97 100644 --- a/src/components/CurrentAddress/stories.js +++ b/src/components/CurrentAddress/stories.js @@ -13,7 +13,7 @@ const stories = storiesOf('Components', module) stories.add('CurrentAddress', () => ( + return } diff --git a/src/components/DeviceCheckAddress.js b/src/components/DeviceCheckAddress.js index 0ee2a324..80108ded 100644 --- a/src/components/DeviceCheckAddress.js +++ b/src/components/DeviceCheckAddress.js @@ -42,23 +42,17 @@ class CheckAddress extends PureComponent { verifyAddress = async ({ device, account }: { device: Device, account: Account }) => { try { - // TODO: this will work only for BTC-like accounts - const freshAddress = account.addresses[0] - if (!freshAddress) { - throw new Error('Account doesnt have fresh addresses') - } - const { address } = await getAddress .send({ currencyId: account.currency.id, devicePath: device.path, - path: freshAddress.path, - segwit: account.isSegwit, + path: account.freshAddressPath, + segwit: !!account.isSegwit, verify: true, }) .toPromise() - if (address !== freshAddress.str) { + if (address !== account.freshAddress) { throw new Error('Confirmed address is different') } diff --git a/src/components/EnsureDeviceApp/index.js b/src/components/EnsureDeviceApp/index.js index 0a5addea..17af125d 100644 --- a/src/components/EnsureDeviceApp/index.js +++ b/src/components/EnsureDeviceApp/index.js @@ -102,9 +102,9 @@ class EnsureDeviceApp extends PureComponent { options = { devicePath: deviceSelected.path, currencyId: account.currency.id, - path: account.path, - accountAddress: account.address, - segwit: account.path.startsWith("49'"), // TODO: store segwit info in account + path: account.freshAddressPath, + accountAddress: account.freshAddress, + segwit: !!account.isSegwit, } } else if (currency) { options = { @@ -118,7 +118,7 @@ class EnsureDeviceApp extends PureComponent { try { const { address } = await getAddress.send(options).toPromise() - if (account && account.address !== address) { + if (account && account.freshAddress !== address) { throw new Error('Account address is different than device address') } this.handleStatusChange(this.state.deviceStatus, 'success') diff --git a/src/components/OperationsList/ConfirmationCheck.js b/src/components/OperationsList/ConfirmationCheck.js index 2b99d38e..cdd7c62c 100644 --- a/src/components/OperationsList/ConfirmationCheck.js +++ b/src/components/OperationsList/ConfirmationCheck.js @@ -3,6 +3,8 @@ import React from 'react' import styled from 'styled-components' +import type { OperationType } from '@ledgerhq/live-common/lib/types' + import { rgba } from 'styles/helpers' import type { T } from 'types/common' @@ -16,14 +18,14 @@ import Tooltip from 'components/base/Tooltip' const Container = styled(Box).attrs({ bg: p => - p.isConfirmed ? rgba(p.type === 'from' ? p.marketColor : p.theme.colors.grey, 0.2) : 'none', - color: p => (p.type === 'from' ? p.marketColor : p.theme.colors.grey), + p.isConfirmed ? rgba(p.type === 'IN' ? p.marketColor : p.theme.colors.grey, 0.2) : 'none', + color: p => (p.type === 'IN' ? p.marketColor : p.theme.colors.grey), align: 'center', justify: 'center', })` border: ${p => !p.isConfirmed - ? `1px solid ${p.type === 'from' ? p.marketColor : rgba(p.theme.colors.grey, 0.2)}` + ? `1px solid ${p.type === 'IN' ? p.marketColor : rgba(p.theme.colors.grey, 0.2)}` : 0}; border-radius: 50%; position: relative; @@ -55,14 +57,14 @@ const ConfirmationCheck = ({ confirmations: number, minConfirmations: number, t: T, - type: 'to' | 'from', + type: OperationType, withTooltip?: boolean, }) => { const isConfirmed = confirmations >= minConfirmations const renderContent = () => ( - {type === 'from' ? : } + {type === 'IN' ? : } {!isConfirmed && ( diff --git a/src/components/OperationsList/Operation.js b/src/components/OperationsList/Operation.js index e4d3d079..202f6779 100644 --- a/src/components/OperationsList/Operation.js +++ b/src/components/OperationsList/Operation.js @@ -7,8 +7,9 @@ import { createStructuredSelector } from 'reselect' import moment from 'moment' import noop from 'lodash/noop' import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react' +import { getOperationAmountNumber } from '@ledgerhq/live-common/lib/helpers/operation' -import type { Account, Operation as OperationType } from '@ledgerhq/live-common/lib/types' +import type { Account, Operation } from '@ledgerhq/live-common/lib/types' import type { T } from 'types/common' @@ -103,15 +104,15 @@ const Cell = styled(Box).attrs({ type Props = { account: Account, currencySettings: *, - onAccountClick: Function, - onOperationClick: Function, + onAccountClick: (account: Account) => void, + onOperationClick: ({ operation: Operation, account: Account, marketColor: string }) => void, marketIndicator: string, t: T, - op: OperationType, + op: Operation, // FIXME rename it operation withAccount: boolean, } -class Operation extends PureComponent { +class OperationComponent extends PureComponent { static defaultProps = { onAccountClick: noop, onOperationClick: noop, @@ -132,8 +133,8 @@ class Operation extends PureComponent { const { unit, currency } = account const time = moment(op.date) const Icon = getCryptoCurrencyIcon(account.currency) - const isNegative = op.amount < 0 - const type = !isNegative ? 'from' : 'to' + const amount = getOperationAmountNumber(op) + const isNegative = amount < 0 const marketColor = getMarketColor({ marketIndicator, @@ -141,12 +142,12 @@ class Operation extends PureComponent { }) return ( - onOperationClick({ operation: op, account, type, marketColor })}> + onOperationClick({ operation: op, account, marketColor })}> @@ -154,7 +155,7 @@ class Operation extends PureComponent { - {t(`operationsList:${type}`)} + {t(`operationsList:${op.type}`)} {time.format('HH:mm')} @@ -185,24 +186,24 @@ class Operation extends PureComponent { )} -
+
@@ -212,4 +213,4 @@ class Operation extends PureComponent { } } -export default connect(mapStateToProps)(Operation) +export default connect(mapStateToProps)(OperationComponent) diff --git a/src/components/modals/OperationDetails.js b/src/components/modals/OperationDetails.js index 4d6a972f..350a91d4 100644 --- a/src/components/modals/OperationDetails.js +++ b/src/components/modals/OperationDetails.js @@ -6,6 +6,7 @@ import { shell } from 'electron' import { translate } from 'react-i18next' import styled from 'styled-components' import moment from 'moment' +import { getOperationAmountNumber } from '@ledgerhq/live-common/lib/helpers/operation' import type { Account, Operation } from '@ledgerhq/live-common/lib/types' import type { T } from 'types/common' @@ -61,18 +62,18 @@ type Props = { t: T, operation: Operation, account: Account, - type: 'from' | 'to', - onClose: Function, + onClose: () => void, currencySettings: *, marketColor: string, } const OperationDetails = connect(mapStateToProps)((props: Props) => { - const { t, type, onClose, operation, account, marketColor, currencySettings } = props - const { id, hash, amount, date, senders, recipients } = operation + const { t, onClose, operation, account, marketColor, currencySettings } = props + const { id, hash, date, senders, recipients, type } = operation + const amount = getOperationAmountNumber(operation) const { name, unit, currency } = account - const confirmations = account.blockHeight - operation.blockHeight + const confirmations = operation.blockHeight ? account.blockHeight - operation.blockHeight : 0 const isConfirmed = confirmations >= currencySettings.minConfirmations return ( diff --git a/src/components/modals/Send/04-step-confirmation.js b/src/components/modals/Send/04-step-confirmation.js index b571da79..7085fe71 100644 --- a/src/components/modals/Send/04-step-confirmation.js +++ b/src/components/modals/Send/04-step-confirmation.js @@ -53,7 +53,7 @@ function StepConfirmation(props: Props) { {t(`${tPrefix}.title`)} {multiline(t(`${tPrefix}.text`))} - {txValidated || ''} + {txValidated || ''} ) } diff --git a/src/internals/accounts/scanAccountsOnDevice.js b/src/internals/accounts/scanAccountsOnDevice.js index 3dbbed7b..029f2f2a 100644 --- a/src/internals/accounts/scanAccountsOnDevice.js +++ b/src/internals/accounts/scanAccountsOnDevice.js @@ -14,7 +14,7 @@ import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currenc import type Transport from '@ledgerhq/hw-transport' -import type { AccountRaw } from '@ledgerhq/live-common/lib/types' +import type { AccountRaw, OperationRaw, OperationType } from '@ledgerhq/live-common/lib/types' import type { NJSAccount, NJSOperation } from '@ledgerhq/ledger-core/src/ledgercore_doc' type Props = { @@ -211,6 +211,7 @@ async function buildAccountRaw({ // $FlowFixMe ops: NJSOperation[], }): Promise { + /* const balanceByDay = ops.length ? await getBalanceByDaySinceOperation({ njsAccount, @@ -218,6 +219,7 @@ async function buildAccountRaw({ core, }) : {} + */ const njsBalance = await njsAccount.getBalance() const balance = njsBalance.toLong() @@ -244,23 +246,28 @@ async function buildAccountRaw({ path: `${accountPath}/${njsAddress.getDerivationPath()}`, })) + if (addresses.length === 0) { + throw new Error('no addresses found') + } + + const { str: freshAddress, path: freshAddressPath } = addresses[0] + const operations = ops.map(op => buildOperationRaw({ core, op, xpub })) const rawAccount: AccountRaw = { - id: xpub, + id: xpub, // FIXME for account id you might want to prepend the crypto currency id to this because it's not gonna be unique. xpub, - path: accountPath, - walletPath, + path: walletPath, name: `Account ${accountIndex}${isSegwit ? ' (segwit)' : ''}`, // TODO: placeholder name? isSegwit, - address: bitcoinAddress, - addresses, + freshAddress, + freshAddressPath, balance, blockHeight, archived: false, index: accountIndex, - balanceByDay, operations, + pendingOperations: [], currencyId, unitMagnitude: jsCurrency.units[0].magnitude, lastSyncDate: new Date().toISOString(), @@ -269,31 +276,45 @@ async function buildAccountRaw({ return rawAccount } -function buildOperationRaw({ core, op, xpub }: { core: Object, op: NJSOperation, xpub: string }) { +function buildOperationRaw({ + core, + op, + xpub, +}: { + core: Object, + op: NJSOperation, + xpub: string, +}): OperationRaw { const id = op.getUid() const bitcoinLikeOperation = op.asBitcoinLikeOperation() const bitcoinLikeTransaction = bitcoinLikeOperation.getTransaction() const hash = bitcoinLikeTransaction.getHash() const operationType = op.getOperationType() - const absoluteAmount = op.getAmount().toLong() + const value = op.getAmount().toLong() + + const OperationTypeMap: { [_: $Keys]: OperationType } = { + [core.OPERATION_TYPES.SEND]: 'OUT', + [core.OPERATION_TYPES.RECEIVE]: 'IN', + } // if transaction is a send, amount becomes negative - const amount = operationType === core.OPERATION_TYPES.SEND ? -absoluteAmount : absoluteAmount + const type = OperationTypeMap[operationType] return { id, hash, - address: '', + type, + value, senders: op.getSenders(), recipients: op.getRecipients(), blockHeight: op.getBlockHeight(), - blockHash: '', + blockHash: null, accountId: xpub, date: op.getDate().toISOString(), - amount, } } +/* async function getBalanceByDaySinceOperation({ njsAccount, njsOperation, @@ -336,3 +357,4 @@ function areSameDay(date1: Date, date2: Date): boolean { date1.getDate() === date2.getDate() ) } +*/ diff --git a/src/internals/accounts/signAndBroadcastTransaction/btc.js b/src/internals/accounts/signAndBroadcastTransaction/btc.js index 7afae5c6..ae7e3b4f 100644 --- a/src/internals/accounts/signAndBroadcastTransaction/btc.js +++ b/src/internals/accounts/signAndBroadcastTransaction/btc.js @@ -37,7 +37,7 @@ export default async function signAndBroadcastTransactionBTCLike( const WALLET_IDENTIFIER = await getWalletIdentifier({ hwApp, - isSegwit: account.isSegwit, + isSegwit: !!account.isSegwit, currencyId: account.currencyId, devicePath: deviceId, }) diff --git a/static/i18n/en/operationsList.yml b/static/i18n/en/operationsList.yml index c8edd0b6..dc1283d2 100644 --- a/static/i18n/en/operationsList.yml +++ b/static/i18n/en/operationsList.yml @@ -2,8 +2,8 @@ date: Date account: Account address: Address amount: Amount -from: Receive funds -to: Sent funds +IN: Receive funds +OUT: Sent funds showMore: Show more confirmed: Confirmed notConfirmed: Not confirmed diff --git a/static/i18n/fr/operationsList.yml b/static/i18n/fr/operationsList.yml index 28f59435..9bd75144 100644 --- a/static/i18n/fr/operationsList.yml +++ b/static/i18n/fr/operationsList.yml @@ -3,8 +3,8 @@ date: Date account: Compte address: Adresse amount: Montant -from: Fonds reçus -to: Fonds envoyés +IN: Fonds reçus +OUT: Fonds envoyés showMore: Voir plus confirmed: Confirmée notConfirmed: Non confirmée diff --git a/yarn.lock b/yarn.lock index 07b88e4b..e4481499 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1480,9 +1480,9 @@ npm "^5.7.1" prebuild-install "^2.2.2" -"@ledgerhq/live-common@^2.7.5": - version "2.7.5" - resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-2.7.5.tgz#5434bf2e708aaca471be4ca823e613cf27ba700c" +"@ledgerhq/live-common@2.8.0-beta.2": + version "2.8.0-beta.2" + resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-2.8.0-beta.2.tgz#942c457ee01add58752698fd525f45abdcf4597b" dependencies: axios "^0.18.0" invariant "^2.2.2" From 10fea56ca2d810d0ae3d65c0ee516bed6d39da80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Thu, 24 May 2018 16:40:51 +0200 Subject: [PATCH 2/4] Implement optimistic update on Operation & bugfixes --- package.json | 2 +- src/api/Ethereum.js | 20 +- src/api/Fees.js | 4 +- src/api/Ledger.js | 34 ++ src/bridge/EthereumJSBridge.js | 51 +- src/bridge/LibcoreBridge.js | 11 +- src/bridge/RippleJSBridge.js | 39 +- src/bridge/makeMockBridge.js | 4 +- src/bridge/types.js | 14 +- src/components/DeviceSignTransaction.js | 10 +- .../OperationsList/ConfirmationCheck.js | 8 +- src/components/OperationsList/Operation.js | 21 +- src/components/modals/OperationDetails.js | 9 +- .../modals/Send/03-step-verification.js | 8 +- .../modals/Send/04-step-confirmation.js | 17 +- .../modals/Send/ConfirmationFooter.js | 7 +- src/components/modals/Send/SendModalBody.js | 42 +- yarn.lock | 455 +++++++----------- 18 files changed, 408 insertions(+), 348 deletions(-) diff --git a/package.json b/package.json index b0a307c1..49757fc7 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@ledgerhq/hw-transport": "^4.12.0", "@ledgerhq/hw-transport-node-hid": "^4.12.0", "@ledgerhq/ledger-core": "^1.2.0", - "@ledgerhq/live-common": "2.8.0-beta.2", + "@ledgerhq/live-common": "^2.8.1", "axios": "^0.18.0", "babel-runtime": "^6.26.0", "bcryptjs": "^2.4.3", diff --git a/src/api/Ethereum.js b/src/api/Ethereum.js index 9d6e260d..716accad 100644 --- a/src/api/Ethereum.js +++ b/src/api/Ethereum.js @@ -1,7 +1,7 @@ // @flow import axios from 'axios' import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types' -import { blockchainBaseURL } from './Ledger' +import { blockchainBaseURL, userFriendlyError } from './Ledger' export type Block = { height: number } // TODO more fields actually export type Tx = { @@ -34,7 +34,7 @@ export type API = { txs: Tx[], }>, getCurrentBlock: () => Promise, - getAccountNonce: (address: string) => Promise, + getAccountNonce: (address: string) => Promise, broadcastTransaction: (signedTransaction: string) => Promise, getAccountBalance: (address: string) => Promise, } @@ -44,25 +44,27 @@ export const apiForCurrency = (currency: CryptoCurrency): API => { return { async getTransactions(address, blockHash) { - const { data } = await axios.get(`${baseURL}/addresses/${address}/transactions`, { - params: { blockHash, noToken: 1 }, - }) + const { data } = await userFriendlyError( + axios.get(`${baseURL}/addresses/${address}/transactions`, { + params: { blockHash, noToken: 1 }, + }), + ) return data }, async getCurrentBlock() { - const { data } = await axios.get(`${baseURL}/blocks/current`) + const { data } = await userFriendlyError(axios.get(`${baseURL}/blocks/current`)) return data }, async getAccountNonce(address) { - const { data } = await axios.get(`${baseURL}/addresses/${address}/nonce`) + const { data } = await userFriendlyError(axios.get(`${baseURL}/addresses/${address}/nonce`)) return data[0].nonce }, async broadcastTransaction(tx) { - const { data } = await axios.post(`${baseURL}/transactions/send`, { tx }) + const { data } = await userFriendlyError(axios.post(`${baseURL}/transactions/send`, { tx })) return data.result }, async getAccountBalance(address) { - const { data } = await axios.get(`${baseURL}/addresses/${address}/balance`) + const { data } = await userFriendlyError(axios.get(`${baseURL}/addresses/${address}/balance`)) return data[0].balance }, } diff --git a/src/api/Fees.js b/src/api/Fees.js index 00e4278b..1ce1b83b 100644 --- a/src/api/Fees.js +++ b/src/api/Fees.js @@ -1,14 +1,14 @@ // @flow import axios from 'axios' import type { Currency } from '@ledgerhq/live-common/lib/types' -import { blockchainBaseURL } from './Ledger' +import { blockchainBaseURL, userFriendlyError } from './Ledger' export type Fees = { [_: string]: number, } export const getEstimatedFees = async (currency: Currency): Promise => { - const { data, status } = await axios.get(`${blockchainBaseURL(currency)}/fees`) + const { data, status } = await userFriendlyError(axios.get(`${blockchainBaseURL(currency)}/fees`)) if (data) { return data } diff --git a/src/api/Ledger.js b/src/api/Ledger.js index d8ee1ad0..76c66191 100644 --- a/src/api/Ledger.js +++ b/src/api/Ledger.js @@ -14,5 +14,39 @@ export const currencyToFeeTicker = (currency: Currency) => { return mapping[currency.id] || tickerLowerCase } +export const userFriendlyError = (p: Promise): Promise => + p.catch(error => { + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + const { data } = error.response + if (data && typeof data.error === 'string') { + const msg = data.error || data.message + if (typeof msg === 'string') { + const m = msg.match(/^JsDefined\((.*)\)$/) + if (m) { + try { + const { message } = JSON.parse(m[1]) + if (typeof message === 'string') { + throw new Error(message) + } + } catch (e) { + console.log(e) + } + } + throw new Error(msg) + } + } + console.log('Ledger API: HTTP status', error.response.status, 'data: ', error.response.data) + throw new Error('A problem occurred with Ledger Servers. Please try again later.') + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + throw new Error('Your network is down. Please try again later.') + } + throw error + }) + export const blockchainBaseURL = (currency: Currency) => `${BASE_URL}blockchain/v2/${currencyToFeeTicker(currency)}` diff --git a/src/bridge/EthereumJSBridge.js b/src/bridge/EthereumJSBridge.js index 2433d6dd..4cc6d826 100644 --- a/src/bridge/EthereumJSBridge.js +++ b/src/bridge/EthereumJSBridge.js @@ -2,6 +2,7 @@ import React from 'react' import EthereumKind from 'components/FeesField/EthereumKind' import throttle from 'lodash/throttle' +import uniqBy from 'lodash/uniqBy' import type { Account, Operation } from '@ledgerhq/live-common/lib/types' import { apiForCurrency } from 'api/Ethereum' import type { Tx } from 'api/Ethereum' @@ -29,7 +30,7 @@ const toAccountOperation = (account: Account) => (tx: Tx): $Exact => const receiving = account.freshAddress.toLowerCase() === tx.to.toLowerCase() const type = sending && receiving ? 'SELF' : sending ? 'OUT' : 'IN' return { - id: tx.hash, + id: `${account.id}-${tx.hash}-${type}`, hash: tx.hash, type, value: tx.value, @@ -47,9 +48,9 @@ function isRecipientValid(currency, recipient) { } function mergeOps(existing: Operation[], newFetched: Operation[]) { - const ids = existing.map(o => o.id) - const all = existing.concat(newFetched.filter(o => !ids.includes(o.id))) - return all.sort((a, b) => a.date - b.date) + const ids = newFetched.map(o => o.id) + const all = newFetched.concat(existing.filter(o => !ids.includes(o.id))) + return uniqBy(all.sort((a, b) => a.date - b.date), 'id') } const paginateMoreTransactions = async ( @@ -66,7 +67,7 @@ const paginateMoreTransactions = async ( } const fetchCurrentBlock = (perCurrencyId => currency => { - if (perCurrencyId[currency.id]) return perCurrencyId[currency.id] + if (perCurrencyId[currency.id]) return perCurrencyId[currency.id]() const api = apiForCurrency(currency) const f = throttle( () => @@ -77,7 +78,7 @@ const fetchCurrentBlock = (perCurrencyId => currency => { 5000, ) perCurrencyId[currency.id] = f - return f + return f() })({}) const EthereumBridge: WalletBridge = { @@ -113,7 +114,6 @@ const EthereumBridge: WalletBridge = { if (isStandard) { if (newAccountCount === 0) { // first zero account will emit one account as opportunity to create a new account.. - const currentBlock = await fetchCurrentBlock(currency) const accountId = `${currency.id}_${address}` const account: $Exact = { id: accountId, @@ -158,7 +158,7 @@ const EthereumBridge: WalletBridge = { unit: currency.units[0], lastSyncDate: new Date(), } - account.operations = txs.map(toAccountOperation(account)) + account.operations = mergeOps([], txs.map(toAccountOperation(account))) return { account } } @@ -205,6 +205,8 @@ const EthereumBridge: WalletBridge = { if (unsubscribed) return const { txs } = await api.getTransactions(freshAddress) if (unsubscribed) return + const nonce = await api.getAccountNonce(freshAddress) + if (unsubscribed) return next(a => { const currentOps = a.operations const newOps = txs.map(toAccountOperation(a)) @@ -219,8 +221,15 @@ const EthereumBridge: WalletBridge = { return a } const operations = mergeOps(currentOps, newOps) + const pendingOperations = a.pendingOperations.filter( + o => + o.transactionSequenceNumber && + o.transactionSequenceNumber >= nonce && + !operations.some(op => o.hash === op.hash), + ) return { ...a, + pendingOperations, operations, balance, blockHeight: block.height, @@ -234,6 +243,7 @@ const EthereumBridge: WalletBridge = { } } main() + return { unsubscribe() { unsubscribed = true @@ -291,10 +301,31 @@ const EthereumBridge: WalletBridge = { }) .toPromise() - const result = await api.broadcastTransaction(transaction) + const hash = await api.broadcastTransaction(transaction) - return result + return { + id: `${a.id}-${hash}-OUT`, + hash, + type: 'OUT', + value: t.amount, + blockHeight: null, + blockHash: null, + accountId: a.id, + senders: [a.freshAddress], + recipients: [t.recipient], + transactionSequenceNumber: nonce, + date: new Date(), + } }, + + addPendingOperation: (account, operation) => ({ + ...account, + pendingOperations: [operation].concat( + account.pendingOperations.filter( + o => o.transactionSequenceNumber === operation.transactionSequenceNumber, + ), + ), + }), } export default EthereumBridge diff --git a/src/bridge/LibcoreBridge.js b/src/bridge/LibcoreBridge.js index b6eb3de3..6c4f17d4 100644 --- a/src/bridge/LibcoreBridge.js +++ b/src/bridge/LibcoreBridge.js @@ -44,7 +44,16 @@ const LibcoreBridge: WalletBridge = { switch (msg.type) { case 'account.sync.progress': { next(a => a) - // use next(), to actually emit account updates..... + // FIXME TODO: use next(), to actually emit account updates..... + // - need to sync the balance + // - need to sync block height & block hash + // - need to sync operations. + // - once all that, need to set lastSyncDate to new Date() + + // - when you implement addPendingOperation you also here need to: + // - if there were pendingOperations that are now in operations, remove them as well. + // - if there are pendingOperations that is older than a threshold (that depends on blockchain speed typically) + // then we probably should trash them out? it's a complex question for UI break } case 'account.sync.fail': { diff --git a/src/bridge/RippleJSBridge.js b/src/bridge/RippleJSBridge.js index b44e8ca1..440c72d6 100644 --- a/src/bridge/RippleJSBridge.js +++ b/src/bridge/RippleJSBridge.js @@ -119,6 +119,7 @@ type Tx = { const txToOperation = (account: Account) => ({ id, + sequence, outcome: { deliveredAmount, ledgerVersion, timestamp }, specification: { source, destination }, }: Tx): Operation => { @@ -135,6 +136,7 @@ const txToOperation = (account: Account) => ({ senders: [source.address], recipients: [destination.address], date: new Date(timestamp), + transactionSequenceNumber: sequence, } return op } @@ -299,9 +301,18 @@ const RippleJSBridge: WalletBridge = { next(a => { const newOps = transactions.map(txToOperation(a)) const operations = mergeOps(a.operations, newOps) + const [last] = operations + const pendingOperations = a.pendingOperations.filter( + o => + last && + last.transactionSequenceNumber && + o.transactionSequenceNumber && + o.transactionSequenceNumber > last.transactionSequenceNumber, + ) return { ...a, operations, + pendingOperations, blockHeight: maxLedgerVersion, lastSyncDate: new Date(), } @@ -394,11 +405,37 @@ const RippleJSBridge: WalletBridge = { throw new Error(submittedPayment.resultMessage) } - return computeBinaryTransactionHash(transaction) + const hash = computeBinaryTransactionHash(transaction) + + return { + id: `${a.id}-${hash}-OUT`, + hash, + accountId: a.id, + type: 'OUT', + value: t.amount, + blockHash: null, + blockHeight: null, + senders: [a.freshAddress], + recipients: [t.recipient], + date: new Date(), + // we probably can't get it so it's a predictive value + transactionSequenceNumber: + (a.operations.length > 0 ? a.operations[0].transactionSequenceNumber : 0) + + a.pendingOperations.length, + } } finally { api.disconnect() } }, + + addPendingOperation: (account, operation) => ({ + ...account, + pendingOperations: [operation].concat( + account.pendingOperations.filter( + o => o.transactionSequenceNumber === operation.transactionSequenceNumber, + ), + ), + }), } export default RippleJSBridge diff --git a/src/bridge/makeMockBridge.js b/src/bridge/makeMockBridge.js index 87931642..f41b6daf 100644 --- a/src/bridge/makeMockBridge.js +++ b/src/bridge/makeMockBridge.js @@ -152,12 +152,14 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> { const op = genOperation(account, account.operations, account.currency, rng) op.type = 'OUT' op.value = t.amount + op.blockHash = null + op.blockHeight = null op.senders = [account.freshAddress] op.recipients = [t.recipient] op.blockHeight = account.blockHeight op.date = new Date() broadcasted[account.id] = (broadcasted[account.id] || []).concat(op) - return op.id + return { ...op } }, } } diff --git a/src/bridge/types.js b/src/bridge/types.js index 716ba35d..c400fc8c 100644 --- a/src/bridge/types.js +++ b/src/bridge/types.js @@ -1,6 +1,6 @@ // @flow -import type { Account, Currency } from '@ledgerhq/live-common/lib/types' +import type { Account, Operation, Currency } from '@ledgerhq/live-common/lib/types' // a WalletBridge is implemented on renderer side. // this is an abstraction on top of libcore / ethereumjs / ripple js / ... @@ -93,10 +93,18 @@ export interface WalletBridge { * finalize the transaction by * - signing it with the ledger device * - broadcasting it to network - * - retrieve and return the id related to this transaction (typically a tx id hash) + * - retrieve and return the optimistic Operation that this transaction is likely to create in the future * * NOTE: in future, when transaction balance is close to account.balance, we could wipe it all at this level... * to implement that, we might want to have special logic `account.balance-transaction.amount < dust` but not sure where this should leave (i would say on UI side because we need to inform user visually). */ - signAndBroadcast(account: Account, transaction: Transaction, deviceId: DeviceId): Promise; + signAndBroadcast( + account: Account, + transaction: Transaction, + deviceId: DeviceId, + ): Promise; + + // Implement an optimistic response for signAndBroadcast. + // you likely should add the operation in account.pendingOperations but maybe you want to clean it (because maybe some are replaced / cancelled by this one?) + addPendingOperation?: (account: Account, optimisticOperation: Operation) => Account; } diff --git a/src/components/DeviceSignTransaction.js b/src/components/DeviceSignTransaction.js index 31801aac..91ccaa31 100644 --- a/src/components/DeviceSignTransaction.js +++ b/src/components/DeviceSignTransaction.js @@ -1,11 +1,11 @@ // @flow import { PureComponent } from 'react' -import type { Account } from '@ledgerhq/live-common/lib/types' +import type { Account, Operation } from '@ledgerhq/live-common/lib/types' import type { Device } from 'types/common' import type { WalletBridge } from 'bridge/types' type Props = { - onSuccess: (txid: string) => void, + onOperationBroadcasted: (op: Operation) => void, render: ({ error: ?Error }) => React$Node, device: Device, account: Account, @@ -32,10 +32,10 @@ class DeviceSignTransaction extends PureComponent { unmount = false sign = async () => { - const { device, account, transaction, bridge, onSuccess } = this.props + const { device, account, transaction, bridge, onOperationBroadcasted } = this.props try { - const txid = await bridge.signAndBroadcast(account, transaction, device.path) - onSuccess(txid) + const optimisticOperation = await bridge.signAndBroadcast(account, transaction, device.path) + onOperationBroadcasted(optimisticOperation) } catch (error) { console.warn(error) this.setState({ error }) diff --git a/src/components/OperationsList/ConfirmationCheck.js b/src/components/OperationsList/ConfirmationCheck.js index cdd7c62c..599f8ae1 100644 --- a/src/components/OperationsList/ConfirmationCheck.js +++ b/src/components/OperationsList/ConfirmationCheck.js @@ -46,22 +46,18 @@ const WrapperClock = styled(Box).attrs({ const ConfirmationCheck = ({ marketColor, - confirmations, - minConfirmations, + isConfirmed, t, type, withTooltip, ...props }: { marketColor: string, - confirmations: number, - minConfirmations: number, + isConfirmed: boolean, t: T, type: OperationType, withTooltip?: boolean, }) => { - const isConfirmed = confirmations >= minConfirmations - const renderContent = () => ( {type === 'IN' ? : } diff --git a/src/components/OperationsList/Operation.js b/src/components/OperationsList/Operation.js index 202f6779..c46606fc 100644 --- a/src/components/OperationsList/Operation.js +++ b/src/components/OperationsList/Operation.js @@ -11,7 +11,7 @@ import { getOperationAmountNumber } from '@ledgerhq/live-common/lib/helpers/oper import type { Account, Operation } from '@ledgerhq/live-common/lib/types' -import type { T } from 'types/common' +import type { T, CurrencySettings } from 'types/common' import { currencySettingsForAccountSelector, marketIndicatorSelector } from 'reducers/settings' import { rgba, getMarketColor } from 'styles/helpers' @@ -33,7 +33,7 @@ const ACCOUNT_COL_SIZE = 150 const AMOUNT_COL_SIZE = 150 const CONFIRMATION_COL_SIZE = 44 -const OperationRaw = styled(Box).attrs({ +const OperationRow = styled(Box).attrs({ horizontal: true, alignItems: 'center', })` @@ -103,7 +103,7 @@ const Cell = styled(Box).attrs({ type Props = { account: Account, - currencySettings: *, + currencySettings: CurrencySettings, onAccountClick: (account: Account) => void, onOperationClick: ({ operation: Operation, account: Account, marketColor: string }) => void, marketIndicator: string, @@ -135,19 +135,26 @@ class OperationComponent extends PureComponent { const Icon = getCryptoCurrencyIcon(account.currency) const amount = getOperationAmountNumber(op) const isNegative = amount < 0 + const isOptimistic = op.blockHeight === null + const isConfirmed = + (op.blockHeight ? account.blockHeight - op.blockHeight : 0) > currencySettings.confirmationsNb const marketColor = getMarketColor({ marketIndicator, isNegative, }) + // FIXME each cell in a component + return ( - onOperationClick({ operation: op, account, marketColor })}> + onOperationClick({ operation: op, account, marketColor })} + > @@ -208,7 +215,7 @@ class OperationComponent extends PureComponent { /> - + ) } } diff --git a/src/components/modals/OperationDetails.js b/src/components/modals/OperationDetails.js index 350a91d4..5c3d8917 100644 --- a/src/components/modals/OperationDetails.js +++ b/src/components/modals/OperationDetails.js @@ -9,7 +9,7 @@ import moment from 'moment' import { getOperationAmountNumber } from '@ledgerhq/live-common/lib/helpers/operation' import type { Account, Operation } from '@ledgerhq/live-common/lib/types' -import type { T } from 'types/common' +import type { T, CurrencySettings } from 'types/common' import { MODAL_OPERATION_DETAILS } from 'config/constants' @@ -63,7 +63,7 @@ type Props = { operation: Operation, account: Account, onClose: () => void, - currencySettings: *, + currencySettings: CurrencySettings, marketColor: string, } @@ -74,7 +74,7 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => { const { name, unit, currency } = account const confirmations = operation.blockHeight ? account.blockHeight - operation.blockHeight : 0 - const isConfirmed = confirmations >= currencySettings.minConfirmations + const isConfirmed = confirmations >= currencySettings.confirmationsNb return ( Operation details @@ -82,8 +82,7 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => { , transaction: *, - onValidate: Function, + onOperationBroadcasted: (op: Operation) => void, t: T, } -export default ({ account, device, bridge, transaction, onValidate, t }: Props) => ( +export default ({ account, device, bridge, transaction, onOperationBroadcasted, t }: Props) => ( {multiline(t('send:steps.verification.warning'))} {t('send:steps.verification.body')} @@ -51,7 +51,7 @@ export default ({ account, device, bridge, transaction, onValidate, t }: Props) device={device} transaction={transaction} bridge={bridge} - onSuccess={onValidate} + onOperationBroadcasted={onOperationBroadcasted} render={({ error }) => ( // FIXME we really really REALLY should use error for the display. otherwise we are completely blind on error cases.. diff --git a/src/components/modals/Send/04-step-confirmation.js b/src/components/modals/Send/04-step-confirmation.js index 7085fe71..c897ad35 100644 --- a/src/components/modals/Send/04-step-confirmation.js +++ b/src/components/modals/Send/04-step-confirmation.js @@ -1,6 +1,7 @@ // @flow import React from 'react' import styled from 'styled-components' +import type { Operation } from '@ledgerhq/live-common/lib/types' import IconCheckCircle from 'icons/CheckCircle' import IconExclamationCircleThin from 'icons/ExclamationCircleThin' @@ -36,15 +37,17 @@ const Text = styled(Box).attrs({ ` type Props = { - txValidated: ?string, + optimisticOperation: ?Operation, t: T, } function StepConfirmation(props: Props) { - const { t, txValidated } = props - const Icon = txValidated ? IconCheckCircle : IconExclamationCircleThin - const iconColor = txValidated ? colors.positiveGreen : colors.alertRed - const tPrefix = txValidated ? 'send:steps.confirmation.success' : 'send:steps.confirmation.error' + const { t, optimisticOperation } = props + const Icon = optimisticOperation ? IconCheckCircle : IconExclamationCircleThin + const iconColor = optimisticOperation ? colors.positiveGreen : colors.alertRed + const tPrefix = optimisticOperation + ? 'send:steps.confirmation.success' + : 'send:steps.confirmation.error' return ( @@ -53,7 +56,9 @@ function StepConfirmation(props: Props) { {t(`${tPrefix}.title`)} {multiline(t(`${tPrefix}.text`))} - {txValidated || ''} + + {optimisticOperation ? optimisticOperation.hash : ''} + ) } diff --git a/src/components/modals/Send/ConfirmationFooter.js b/src/components/modals/Send/ConfirmationFooter.js index 072a1472..085bb6e6 100644 --- a/src/components/modals/Send/ConfirmationFooter.js +++ b/src/components/modals/Send/ConfirmationFooter.js @@ -1,23 +1,24 @@ // @flow import React from 'react' +import type { Operation } from '@ledgerhq/live-common/lib/types' import Button from 'components/base/Button' import { ModalFooter } from 'components/base/Modal' import type { T } from 'types/common' export default ({ t, - txValidated, + optimisticOperation, onClose, onGoToFirstStep, }: { t: T, - txValidated: ?string, + optimisticOperation: ?Operation, onClose: () => void, onGoToFirstStep: () => void, }) => ( - {txValidated ? ( + {optimisticOperation ? ( // TODO: actually go to operations details