diff --git a/package.json b/package.json index d50c20e0..f6c8464f 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,8 @@ "@ledgerhq/hw-app-xrp": "^4.12.0", "@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/ledger-core": "^1.2.1", + "@ledgerhq/live-common": "^2.9.1", "axios": "^0.18.0", "babel-runtime": "^6.26.0", "bcryptjs": "^2.4.3", @@ -90,6 +90,7 @@ "ripple-lib": "^1.0.0-beta.0", "rxjs": "^6.2.0", "rxjs-compat": "^6.1.0", + "semaphore": "^1.1.0", "smooth-scrollbar": "^8.2.7", "source-map": "0.7.2", "source-map-support": "^0.5.4", 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 f902cf0f..38a2142e 100644 --- a/src/bridge/EthereumJSBridge.js +++ b/src/bridge/EthereumJSBridge.js @@ -2,6 +2,8 @@ import React from 'react' import EthereumKind from 'components/FeesField/EthereumKind' import throttle from 'lodash/throttle' +import flatMap from 'lodash/flatMap' +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' @@ -24,20 +26,43 @@ const EditFees = ({ account, onChange, value }: EditProps) => ( /> ) -const toAccountOperation = (account: Account) => (tx: Tx): Operation => { - const sending = account.address.toLowerCase() === tx.from.toLowerCase() - 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 - accountId: account.id, - senders: [tx.from], - recipients: [tx.to], - date: new Date(tx.received_at), +// in case of a SELF send, 2 ops are returned. +const txToOps = (account: Account) => (tx: Tx): Operation[] => { + const freshAddress = account.freshAddress.toLowerCase() + const from = tx.from.toLowerCase() + const to = tx.to.toLowerCase() + const sending = freshAddress === from + const receiving = freshAddress === to + const ops = [] + if (sending) { + ops.push({ + id: `${account.id}-${tx.hash}-OUT`, + hash: tx.hash, + type: 'OUT', + value: tx.value + tx.gas_price * tx.gas_used, + blockHeight: tx.block && tx.block.height, + blockHash: tx.block && tx.block.hash, + accountId: account.id, + senders: [tx.from], + recipients: [tx.to], + date: new Date(tx.received_at), + }) } + if (receiving) { + ops.push({ + id: `${account.id}-${tx.hash}-IN`, + hash: tx.hash, + type: 'IN', + value: tx.value, + blockHeight: tx.block && tx.block.height, + blockHash: tx.block && tx.block.hash, + accountId: account.id, + senders: [tx.from], + recipients: [tx.to], + date: new Date(tx.received_at), + }) + } + return ops } function isRecipientValid(currency, recipient) { @@ -45,26 +70,13 @@ 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 paginateMoreTransactions = async ( - account: Account, - acc: Operation[], -): Promise => { - const api = apiForCurrency(account.currency) - const { txs } = await api.getTransactions( - account.address, - acc.length ? acc[acc.length - 1].blockHash : undefined, - ) - if (txs.length === 0) return acc - return mergeOps(acc, txs.map(toAccountOperation(account))) + 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 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( () => @@ -75,7 +87,7 @@ const fetchCurrentBlock = (perCurrencyId => currency => { 5000, ) perCurrencyId[currency.id] = f - return f + return f() })({}) const EthereumBridge: WalletBridge = { @@ -93,38 +105,39 @@ const EthereumBridge: WalletBridge = { async function stepAddress( index, - { address, path }, + { address, path: freshAddressPath }, isStandard, ): { account?: Account, complete?: boolean } { const balance = await api.getAccountBalance(address) if (finished) return { complete: true } const currentBlock = await fetchCurrentBlock(currency) if (finished) return { complete: true } - const { txs } = await api.getTransactions(address) + let { 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) { 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: 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,25 +150,34 @@ 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(), } - account.operations = txs.map(toAccountOperation(account)) + for (let i = 0; i < 50; i++) { + const api = apiForCurrency(account.currency) + const { block } = txs[txs.length - 1] + if (!block) break + const next = await api.getTransactions(account.freshAddress, block.hash) + if (next.txs.length === 0) break + txs = txs.concat(next.txs) + } + txs.reverse() + account.operations = mergeOps([], flatMap(txs, txToOps(account))) + console.log(account) return { account } } @@ -166,9 +188,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 +210,7 @@ const EthereumBridge: WalletBridge = { return { unsubscribe } }, - synchronize({ address, blockHeight, currency }, { next, complete, error }) { + synchronize({ freshAddress, blockHeight, currency, operations }, { next, complete, error }) { let unsubscribed = false const api = apiForCurrency(currency) async function main() { @@ -198,26 +220,30 @@ const EthereumBridge: WalletBridge = { if (block.height === blockHeight) { complete() } else { - const balance = await api.getAccountBalance(address) + const blockHash = operations.length > 0 ? operations[0].blockHash : undefined + const { txs } = await api.getTransactions(freshAddress, blockHash) + if (unsubscribed) return + if (txs.length === 0) { + complete() + return + } + const balance = await api.getAccountBalance(freshAddress) if (unsubscribed) return - const { txs } = await api.getTransactions(address) + const nonce = await api.getAccountNonce(freshAddress) if (unsubscribed) return next(a => { const currentOps = a.operations - const newOps = txs.map(toAccountOperation(a)) - const { length: newLength } = newOps - const { length } = currentOps - if ( - // still empty - (length === 0 && newLength === 0) || - // latest is still same - (length > 0 && newLength > 0 && currentOps[0].id === newOps[0].id) - ) { - return a - } + const newOps = flatMap(txs, txToOps(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, @@ -231,6 +257,7 @@ const EthereumBridge: WalletBridge = { } } main() + return { unsubscribe() { unsubscribed = true @@ -238,10 +265,7 @@ const EthereumBridge: WalletBridge = { } }, - pullMoreOperations: async account => { - const operations = await paginateMoreTransactions(account, account.operations) - return a => ({ ...a, operations }) - }, + pullMoreOperations: () => Promise.resolve(a => a), // NOT IMPLEMENTED isRecipientValid: (currency, recipient) => Promise.resolve(isRecipientValid(currency, recipient)), @@ -270,14 +294,15 @@ const EthereumBridge: WalletBridge = { // $FlowFixMe EditFees, + // FIXME gasPrice calc is wrong... need to multiply with gasLimit I guess ? + canBeSpent: (a, t) => Promise.resolve(t.amount + t.gasPrice <= a.balance), getTotalSpent: (a, t) => Promise.resolve(t.amount + t.gasPrice), - getMaxAmount: (a, t) => Promise.resolve(a.balance - t.gasPrice), 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({ @@ -288,10 +313,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/EthereumMockJSBridge.js b/src/bridge/EthereumMockJSBridge.js deleted file mode 100644 index bca51ca9..00000000 --- a/src/bridge/EthereumMockJSBridge.js +++ /dev/null @@ -1,22 +0,0 @@ -// @flow -import React from 'react' -import EthereumKind from 'components/FeesField/EthereumKind' -import type { EditProps } from './types' -import makeMockBridge from './makeMockBridge' - -const EditFees = ({ account, onChange, value }: EditProps<*>) => ( - { - onChange({ ...value, gasPrice }) - }} - gasPrice={value.gasPrice} - account={account} - /> -) - -export default makeMockBridge({ - extraInitialTransactionProps: () => ({ gasPrice: 0 }), - EditFees, - getTotalSpent: (a, t) => Promise.resolve(t.amount + t.gasPrice), - getMaxAmount: (a, t) => Promise.resolve(a.balance - t.gasPrice), -}) diff --git a/src/bridge/LibcoreBridge.js b/src/bridge/LibcoreBridge.js index b6eb3de3..f95d8911 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': { @@ -150,9 +159,11 @@ const LibcoreBridge: WalletBridge = { isValidTransaction: (a, t) => (t.amount > 0 && t.recipient && true) || false, - getTotalSpent: (a, t) => Promise.resolve(t.amount + t.feePerByte), + canBeSpent: (a, t) => Promise.resolve(t.amount <= a.balance), // FIXME - getMaxAmount: (a, t) => Promise.resolve(a.balance - t.feePerByte), + getTotalSpent: (a, t) => Promise.resolve(t.amount), // FIXME + + getMaxAmount: (a, _t) => Promise.resolve(a.balance), // FIXME signAndBroadcast: (account, transaction, deviceId) => { const rawAccount = encodeAccount(account) diff --git a/src/bridge/RippleJSBridge.js b/src/bridge/RippleJSBridge.js index d238aff7..51c66b0d 100644 --- a/src/bridge/RippleJSBridge.js +++ b/src/bridge/RippleJSBridge.js @@ -2,6 +2,7 @@ import React from 'react' import bs58check from 'ripple-bs58check' import { computeBinaryTransactionHash } from 'ripple-hashes' +import throttle from 'lodash/throttle' import type { Account, Operation } from '@ledgerhq/live-common/lib/types' import { getDerivations } from 'helpers/derivations' import getAddress from 'commands/getAddress' @@ -119,27 +120,54 @@ type Tx = { const txToOperation = (account: Account) => ({ id, - outcome: { deliveredAmount, ledgerVersion, timestamp }, + sequence, + outcome: { fee, 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' + let value = deliveredAmount ? parseAPICurrencyObject(deliveredAmount) : 0 + if (type === 'OUT') { + const feeValue = parseAPIValue(fee) + if (!isNaN(feeValue)) { + value += feeValue + } + } + + 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), + transactionSequenceNumber: sequence, } return op } +const getServerInfo = (perCurrencyId => currency => { + if (perCurrencyId[currency.id]) return perCurrencyId[currency.id]() + const f = throttle(async () => { + const api = apiForCurrency(currency) + try { + await api.connect() + const res = await api.getServerInfo() + return res + } catch (e) { + f.cancel() + throw e + } finally { + api.disconnect() + } + }, 60000) + perCurrencyId[currency.id] = f + return f() +})({}) + const RippleJSBridge: WalletBridge = { scanAccountsOnDevice(currency, deviceId, { next, complete, error }) { let finished = false @@ -151,7 +179,7 @@ const RippleJSBridge: WalletBridge = { const api = apiForCurrency(currency) try { await api.connect() - const serverInfo = await api.getServerInfo() + const serverInfo = await getServerInfo(currency) const ledgers = serverInfo.completeLedgers.split('-') const minLedgerVersion = Number(ledgers[0]) const maxLedgerVersion = Number(ledgers[1]) @@ -159,9 +187,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 +206,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 +216,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 +244,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 +278,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 @@ -258,7 +289,7 @@ const RippleJSBridge: WalletBridge = { try { await api.connect() if (finished) return - const serverInfo = await api.getServerInfo() + const serverInfo = await getServerInfo(currency) if (finished) return const ledgers = serverInfo.completeLedgers.split('-') const minLedgerVersion = Number(ledgers[0]) @@ -266,7 +297,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 +313,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, }) @@ -297,9 +328,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(), } @@ -351,6 +391,11 @@ const RippleJSBridge: WalletBridge = { isValidTransaction: (a, t) => (t.amount > 0 && t.recipient && true) || false, + canBeSpent: async (a, t) => { + const r = await getServerInfo(a.currency) + return t.amount + t.fee + parseAPIValue(r.validatedLedger.reserveBaseXRP) <= a.balance + }, + getTotalSpent: (a, t) => Promise.resolve(t.amount + t.fee), getMaxAmount: (a, t) => Promise.resolve(a.balance - t.fee), @@ -362,7 +407,7 @@ const RippleJSBridge: WalletBridge = { const amount = formatAPICurrencyXRP(t.amount) const payment = { source: { - address: a.address, + address: a.freshAddress, amount, }, destination: { @@ -375,7 +420,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({ @@ -392,11 +437,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/UnsupportedBridge.js b/src/bridge/UnsupportedBridge.js index 55261eb4..0a6b26b9 100644 --- a/src/bridge/UnsupportedBridge.js +++ b/src/bridge/UnsupportedBridge.js @@ -30,6 +30,8 @@ const UnsupportedBridge: WalletBridge<*> = { getTransactionRecipient: () => '', + canBeSpent: () => Promise.resolve(false), + getTotalSpent: () => Promise.resolve(0), getMaxAmount: () => Promise.resolve(0), diff --git a/src/bridge/makeMockBridge.js b/src/bridge/makeMockBridge.js index 1bc2eef9..80a3de90 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' @@ -29,6 +30,7 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> { extraInitialTransactionProps, getTotalSpent, getMaxAmount, + canBeSpent, } = { ...defaultOpts, ...opts, @@ -53,7 +55,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 }) @@ -142,6 +144,8 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> { isValidTransaction: (a, t) => (t.amount > 0 && t.recipient && true) || false, + canBeSpent, + getTotalSpent, getMaxAmount, @@ -149,12 +153,16 @@ 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.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..97a6f211 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 / ... @@ -84,6 +84,8 @@ export interface WalletBridge { // render the whole advanced part of the form EditAdvancedOptions?: React$ComponentType>; + canBeSpent(account: Account, transaction: Transaction): Promise; + getTotalSpent(account: Account, transaction: Transaction): Promise; // NB this is not used yet but we'll use it when we have MAX @@ -93,10 +95,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/commands/getAddress.js b/src/commands/getAddress.js index 03d9328d..6029b1e7 100644 --- a/src/commands/getAddress.js +++ b/src/commands/getAddress.js @@ -2,7 +2,7 @@ import { createCommand, Command } from 'helpers/ipc' import { fromPromise } from 'rxjs/observable/fromPromise' -import CommNodeHid from '@ledgerhq/hw-transport-node-hid' +import { withDevice } from 'helpers/deviceAccess' import getAddressForCurrency from 'helpers/getAddressForCurrency' type Input = { @@ -24,7 +24,7 @@ const cmd: Command = createCommand( 'getAddress', ({ currencyId, devicePath, path, ...options }) => fromPromise( - CommNodeHid.open(devicePath).then(transport => + withDevice(devicePath)(transport => getAddressForCurrency(currencyId)(transport, currencyId, path, options), ), ), diff --git a/src/commands/signTransaction.js b/src/commands/signTransaction.js index b89ad0c5..dfc1cfcb 100644 --- a/src/commands/signTransaction.js +++ b/src/commands/signTransaction.js @@ -2,7 +2,7 @@ import { createCommand, Command } from 'helpers/ipc' import { fromPromise } from 'rxjs/observable/fromPromise' -import CommNodeHid from '@ledgerhq/hw-transport-node-hid' +import { withDevice } from 'helpers/deviceAccess' import signTransactionForCurrency from 'helpers/signTransactionForCurrency' type Input = { @@ -19,7 +19,7 @@ const cmd: Command = createCommand( 'signTransaction', ({ currencyId, devicePath, path, transaction }) => fromPromise( - CommNodeHid.open(devicePath).then(transport => + withDevice(devicePath)(transport => signTransactionForCurrency(currencyId)(transport, currencyId, path, transaction), ), ), diff --git a/src/components/BalanceSummary/index.js b/src/components/BalanceSummary/index.js index 17bd93ef..0a032cf3 100644 --- a/src/components/BalanceSummary/index.js +++ b/src/components/BalanceSummary/index.js @@ -35,7 +35,8 @@ const BalanceSummary = ({ renderHeader, selectedTime, }: Props) => { - const unit = getFiatCurrencyByTicker(counterValue).units[0] + const currency = getFiatCurrencyByTicker(counterValue) + const account = accounts.length === 1 ? accounts[0] : undefined return ( - isAvailable ? ( - - ) : null + renderTooltip={ + isAvailable && !account + ? d => ( + + + + {d.date.toISOString().substr(0, 10)} + + + ) + : undefined } /> diff --git a/src/components/CalculateBalance.js b/src/components/CalculateBalance.js index 7c4337ce..fb544402 100644 --- a/src/components/CalculateBalance.js +++ b/src/components/CalculateBalance.js @@ -4,7 +4,7 @@ import { PureComponent } from 'react' import { connect } from 'react-redux' -import type { Account, BalanceHistory } from '@ledgerhq/live-common/lib/types' +import type { Account } from '@ledgerhq/live-common/lib/types' import { getBalanceHistorySum } from '@ledgerhq/live-common/lib/helpers/account' import CounterValues from 'helpers/countervalues' import { exchangeSettingsForAccountSelector, counterValueCurrencySelector } from 'reducers/settings' @@ -16,8 +16,14 @@ type OwnProps = { children: Props => *, } +type Item = { + date: Date, + value: number, + originalValue: number, +} + type Props = OwnProps & { - balanceHistory: BalanceHistory, + balanceHistory: Item[], balanceStart: number, balanceEnd: number, isAvailable: boolean, @@ -26,10 +32,18 @@ type Props = OwnProps & { const mapStateToProps = (state: State, props: OwnProps) => { const counterValueCurrency = counterValueCurrencySelector(state) let isAvailable = true + + // create array of original values, used to reconciliate + // with counter values after calculation + const originalValues = [] + const balanceHistory = getBalanceHistorySum( props.accounts, props.daysCount, (account, value, date) => { + // keep track of original value + originalValues.push(value) + const cv = CounterValues.calculateSelector(state, { value, date, @@ -43,7 +57,11 @@ const mapStateToProps = (state: State, props: OwnProps) => { } return cv }, + ).map((item, i) => + // reconciliate balance history with original values + ({ ...item, originalValue: originalValues[i] || 0 }), ) + return { isAvailable, balanceHistory, diff --git a/src/components/CounterValue/index.js b/src/components/CounterValue/index.js index 4b46b8a9..1874ee01 100644 --- a/src/components/CounterValue/index.js +++ b/src/components/CounterValue/index.js @@ -49,7 +49,9 @@ const mapStateToProps = (state: State, props: OwnProps) => { class CounterValue extends PureComponent { render() { const { value, counterValueCurrency, date, ...props } = this.props - if (!value && value !== 0) return null + if (!value && value !== 0) { + return null + } return ( ( + return } diff --git a/src/components/DashboardPage/AccountCard.js b/src/components/DashboardPage/AccountCard.js index a5fca80e..62f6d608 100644 --- a/src/components/DashboardPage/AccountCard.js +++ b/src/components/DashboardPage/AccountCard.js @@ -86,9 +86,9 @@ const AccountCard = ({ color={account.currency.color} height={52} hideAxis - interactive={false} + isInteractive={false} id={`account-chart-${account.id}`} - unit={account.unit} + account={account} /> )} diff --git a/src/components/DevTools.js b/src/components/DevTools.js index 1e6e1729..9baa004c 100644 --- a/src/components/DevTools.js +++ b/src/components/DevTools.js @@ -254,26 +254,8 @@ class DevTools extends PureComponent { color="#8884d8" height={50} hideAxis - interactive={false} + isInteractive={false} /> - {/* ( - - )} - /> */} ))} 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/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/EnsureDeviceApp/index.js b/src/components/EnsureDeviceApp/index.js index 9c8e7920..bce04b25 100644 --- a/src/components/EnsureDeviceApp/index.js +++ b/src/components/EnsureDeviceApp/index.js @@ -110,9 +110,9 @@ class EnsureDeviceApp extends PureComponent { appOptions = { 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) { appOptions = { @@ -125,7 +125,7 @@ class EnsureDeviceApp extends PureComponent { try { if (appOptions) { const { address } = await getAddress.send(appOptions).toPromise() - if (account && account.address !== address) { + if (account && account.freshAddress !== address) { throw new Error('Account address is different than device address') } } else { diff --git a/src/components/FeesField/EthereumKind.js b/src/components/FeesField/EthereumKind.js index c496e035..0ac63bbc 100644 --- a/src/components/FeesField/EthereumKind.js +++ b/src/components/FeesField/EthereumKind.js @@ -15,14 +15,21 @@ type Props = { } class FeesField extends Component { + state = { + isFocused: false, + } componentDidUpdate() { const { gasPrice, fees, onChange } = this.props - if (!gasPrice && fees && fees.gas_price) { + const { isFocused } = this.state + if (!gasPrice && fees && fees.gas_price && !isFocused) { onChange(fees.gas_price) // we want to set the default to gas_price } } + onChangeFocus = isFocused => { + this.setState({ isFocused }) + } render() { - const { account, gasPrice, onChange, error } = this.props + const { account, gasPrice, error, onChange } = this.props const { units } = account.currency return ( @@ -32,6 +39,7 @@ class FeesField extends Component { containerProps={{ grow: true }} value={gasPrice} onChange={onChange} + onChangeFocus={this.onChangeFocus} /> ) diff --git a/src/components/OperationsList/ConfirmationCheck.js b/src/components/OperationsList/ConfirmationCheck.js index 2b99d38e..599f8ae1 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; @@ -44,25 +46,21 @@ 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: '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..c46606fc 100644 --- a/src/components/OperationsList/Operation.js +++ b/src/components/OperationsList/Operation.js @@ -7,10 +7,11 @@ 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' +import type { T, CurrencySettings } from 'types/common' import { currencySettingsForAccountSelector, marketIndicatorSelector } from 'reducers/settings' import { rgba, getMarketColor } from 'styles/helpers' @@ -32,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', })` @@ -102,16 +103,16 @@ const Cell = styled(Box).attrs({ type Props = { account: Account, - currencySettings: *, - onAccountClick: Function, - onOperationClick: Function, + currencySettings: CurrencySettings, + 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,21 +133,28 @@ 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 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, type, marketColor })}> + onOperationClick({ operation: op, account, marketColor })} + > @@ -154,7 +162,7 @@ class Operation extends PureComponent { - {t(`operationsList:${type}`)} + {t(`operationsList:${op.type}`)} {time.format('HH:mm')} @@ -185,31 +193,31 @@ class Operation extends PureComponent { )} -
+
- + ) } } -export default connect(mapStateToProps)(Operation) +export default connect(mapStateToProps)(OperationComponent) diff --git a/src/components/SettingsPage/sections/Currencies.js b/src/components/SettingsPage/sections/Currencies.js index 2ee3c0c8..c26124b5 100644 --- a/src/components/SettingsPage/sections/Currencies.js +++ b/src/components/SettingsPage/sections/Currencies.js @@ -15,7 +15,6 @@ import type { Settings, CurrencySettings, T } from 'types/common' import { counterValueCurrencySelector } from 'reducers/settings' import { currenciesSelector } from 'reducers/accounts' -import CounterValues from 'helpers/countervalues' import SelectCurrency from 'components/SelectCurrency' import StepperNumber from 'components/base/StepperNumber' @@ -138,21 +137,13 @@ class TabCurrencies extends PureComponent { /> - - {polling => ( - // TODO move to a dedicated "row" component - { - this.handleChangeExchange(exchange) - polling.poll() - }} - style={{ minWidth: 200 }} - /> - )} - + { title={t('settings:display.counterValue')} desc={t('settings:display.counterValueDesc')} > - - {polling => ( - (item ? item.name : '')} + renderSelected={item => item && item.name} + items={fiats} + value={cachedCounterValue} + /> - + + + + ) @@ -63,9 +77,11 @@ function generateRandomData(n) { const data = [] const chance = new Chance() while (!day.isSame(today)) { + const value = chance.integer({ min: 0.5e8, max: 1e8 }) data.push({ date: day.toDate(), - value: chance.integer({ min: 0.5e8, max: 1e8 }), + value, + originalValue: value, }) day.add(1, 'day') } diff --git a/src/components/base/Chart/types.js b/src/components/base/Chart/types.js index 0c22e948..7446ab14 100644 --- a/src/components/base/Chart/types.js +++ b/src/components/base/Chart/types.js @@ -3,6 +3,7 @@ export type Item = { date: Date, value: number, + originalValue: number, } type EnrichedItem = { diff --git a/src/components/base/InputCurrency/index.js b/src/components/base/InputCurrency/index.js index 9abe3a45..65c65886 100644 --- a/src/components/base/InputCurrency/index.js +++ b/src/components/base/InputCurrency/index.js @@ -45,6 +45,7 @@ function stopPropagation(e) { } type Props = { + onChangeFocus: boolean => void, onChange: (number, Unit) => void, // FIXME Unit shouldn't be provided (this is not "standard" onChange) onChangeUnit: Unit => void, renderRight: any, @@ -61,6 +62,7 @@ type State = { class InputCurrency extends PureComponent { static defaultProps = { + onChangeFocus: noop, onChange: noop, renderRight: null, units: [], @@ -122,8 +124,15 @@ class InputCurrency extends PureComponent { this.setState({ displayValue: v || '' }) } - handleBlur = () => this.syncInput({ isFocused: false }) - handleFocus = () => this.syncInput({ isFocused: true }) + handleBlur = () => { + this.syncInput({ isFocused: false }) + this.props.onChangeFocus(false) + } + + handleFocus = () => { + this.syncInput({ isFocused: true }) + this.props.onChangeFocus(true) + } syncInput = ({ isFocused }: { isFocused: boolean }) => { const { value, showAllDigits, unit } = this.props diff --git a/src/components/modals/AddAccount/index.js b/src/components/modals/AddAccount/index.js index e460b437..2799a0e9 100644 --- a/src/components/modals/AddAccount/index.js +++ b/src/components/modals/AddAccount/index.js @@ -28,7 +28,6 @@ import Button from 'components/base/Button' import Modal, { ModalContent, ModalTitle, ModalFooter, ModalBody } from 'components/base/Modal' import StepConnectDevice from 'components/modals/StepConnectDevice' import { getBridgeForCurrency } from 'bridge' -import CounterValues from 'helpers/countervalues' import StepCurrency from './01-step-currency' import StepImport from './03-step-import' @@ -62,7 +61,6 @@ type Props = { closeModal: Function, t: T, updateAccount: Function, - counterValuesPolling: *, } type State = { @@ -173,8 +171,6 @@ class AddAccountModal extends PureComponent { selectedAccounts.forEach(a => addAccount({ ...a, archived: false })) this.setState({ selectedAccounts: [] }) closeModal(MODAL_ADD_ACCOUNT) - this.props.counterValuesPolling.poll() - this.props.counterValuesPolling.flush() } handleNextStep = () => { @@ -298,17 +294,4 @@ class AddAccountModal extends PureComponent { ) } } - -// FIXME This is kinda ugly architecture right now. -// I think we should delegate more work to individual steps -// e.g. each step is responsible to connect to redux, not at top level. - -const AddAccountModalAndCounterValues = props => ( - - {cvPolling => } - -) - -export default compose(connect(mapStateToProps, mapDispatchToProps), translate())( - AddAccountModalAndCounterValues, -) +export default compose(connect(mapStateToProps, mapDispatchToProps), translate())(AddAccountModal) diff --git a/src/components/modals/OperationDetails.js b/src/components/modals/OperationDetails.js index 4d6a972f..5c3d8917 100644 --- a/src/components/modals/OperationDetails.js +++ b/src/components/modals/OperationDetails.js @@ -6,9 +6,10 @@ 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' +import type { T, CurrencySettings } from 'types/common' import { MODAL_OPERATION_DETAILS } from 'config/constants' @@ -61,19 +62,19 @@ type Props = { t: T, operation: Operation, account: Account, - type: 'from' | 'to', - onClose: Function, - currencySettings: *, + onClose: () => void, + currencySettings: 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 isConfirmed = confirmations >= currencySettings.minConfirmations + const confirmations = operation.blockHeight ? account.blockHeight - operation.blockHeight : 0 + const isConfirmed = confirmations >= currencySettings.confirmationsNb return ( Operation details @@ -81,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 b571da79..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