From b7d2b9a0fe3c6fd830a41504ee9861f737a1f5c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Thu, 14 Jun 2018 17:50:48 +0200 Subject: [PATCH] Implement libcore features for Send funds - address validation - total fees & Total spent calculation - not enough balance detection --- package.json | 3 +- src/bridge/LibcoreBridge.js | 54 ++++++++++++++++++++++--- src/commands/index.js | 4 ++ src/commands/libcoreGetFees.js | 63 +++++++++++++++++++++++++++++ src/commands/libcoreValidAddress.js | 24 +++++++++++ src/helpers/libcore.js | 5 +++ yarn.lock | 6 +-- 7 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 src/commands/libcoreGetFees.js create mode 100644 src/commands/libcoreValidAddress.js diff --git a/package.json b/package.json index f00f7643..3fa406db 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@ledgerhq/hw-app-xrp": "^4.13.0", "@ledgerhq/hw-transport": "^4.13.0", "@ledgerhq/hw-transport-node-hid": "^4.13.0", - "@ledgerhq/ledger-core": "1.7.0", + "@ledgerhq/ledger-core": "1.9.0", "@ledgerhq/live-common": "2.30.0", "async": "^2.6.1", "axios": "^0.18.0", @@ -64,6 +64,7 @@ "i18next-node-fs-backend": "^1.0.0", "invariant": "^2.2.4", "lodash": "^4.17.5", + "lru-cache": "^4.1.3", "moment": "^2.22.2", "qrcode": "^1.2.0", "qrcode-reader": "^1.0.4", diff --git a/src/bridge/LibcoreBridge.js b/src/bridge/LibcoreBridge.js index 9823a762..45321d7e 100644 --- a/src/bridge/LibcoreBridge.js +++ b/src/bridge/LibcoreBridge.js @@ -1,6 +1,7 @@ // @flow import React from 'react' import { Observable } from 'rxjs' +import LRU from 'lru-cache' import { map } from 'rxjs/operators' import type { Account } from '@ledgerhq/live-common/lib/types' import { decodeAccount, encodeAccount } from 'reducers/accounts' @@ -8,6 +9,8 @@ import FeesBitcoinKind from 'components/FeesField/BitcoinKind' import libcoreScanAccounts from 'commands/libcoreScanAccounts' import libcoreSyncAccount from 'commands/libcoreSyncAccount' import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast' +import libcoreGetFees from 'commands/libcoreGetFees' +import libcoreValidAddress from 'commands/libcoreValidAddress' import type { WalletBridge, EditProps } from './types' const notImplemented = new Error('LibcoreBridge: not implemented') @@ -43,6 +46,38 @@ const EditAdvancedOptions = ({ onChange, value }: EditProps) => ( ) */ +const recipientValidLRU = LRU({ max: 100 }) + +const isRecipientValid = (currency, recipient): Promise => { + const key = `${currency.id}_${recipient}` + let promise = recipientValidLRU.get(key) + if (promise) return promise + promise = libcoreValidAddress + .send({ + address: recipient, + currencyId: currency.id, + }) + .toPromise() + recipientValidLRU.set(key, promise) + return promise +} + +const feesLRU = LRU({ max: 100 }) + +const getFees = async (a, transaction) => { + const isValid = await isRecipientValid(a.currency, transaction.recipient) + if (!isValid) return null + const key = `${a.id}_${transaction.amount}_${transaction.recipient}_${transaction.feePerByte}` + let promise = feesLRU.get(key) + if (promise) return promise + promise = libcoreGetFees + .send({ accountId: a.id, accountIndex: a.index, transaction }) + .toPromise() + .then(r => r.totalFees) + feesLRU.set(key, promise) + return promise +} + const LibcoreBridge: WalletBridge = { scanAccountsOnDevice(currency, devicePath, observer) { return libcoreScanAccounts @@ -107,7 +142,7 @@ const LibcoreBridge: WalletBridge = { pullMoreOperations: () => Promise.reject(notImplemented), - isRecipientValid: (currency, recipient) => Promise.resolve(recipient.length > 0), + isRecipientValid, createTransaction: () => ({ amount: 0, @@ -136,14 +171,23 @@ const LibcoreBridge: WalletBridge = { isValidTransaction: (a, t) => (t.amount > 0 && t.recipient && true) || false, - canBeSpent: (a, t) => Promise.resolve(t.amount <= a.balance), // FIXME + canBeSpent: (a, t) => + getFees(a, t) + .then(fees => fees !== null) + .catch(() => false), - getTotalSpent: (a, t) => Promise.resolve(t.amount), // FIXME + getTotalSpent: (a, t) => + getFees(a, t) + .then(totalFees => t.amount + (totalFees || 0)) + .catch(() => 0), - getMaxAmount: (a, _t) => Promise.resolve(a.balance), // FIXME + getMaxAmount: (a, t) => + getFees(a, t) + .catch(() => 0) + .then(totalFees => a.balance - (totalFees || 0)), signAndBroadcast: (account, transaction, deviceId) => { - const encodedAccount = encodeAccount(account) + const encodedAccount = encodeAccount(account) // FIXME no need to send the whole account over the threads return libcoreSignAndBroadcast .send({ account: encodedAccount, diff --git a/src/commands/index.js b/src/commands/index.js index e540e588..e32c6639 100644 --- a/src/commands/index.js +++ b/src/commands/index.js @@ -14,11 +14,13 @@ import installFinalFirmware from 'commands/installFinalFirmware' import installMcu from 'commands/installMcu' import installOsuFirmware from 'commands/installOsuFirmware' import isDashboardOpen from 'commands/isDashboardOpen' +import libcoreGetFees from 'commands/libcoreGetFees' import libcoreGetVersion from 'commands/libcoreGetVersion' import libcoreHardReset from 'commands/libcoreHardReset' import libcoreScanAccounts from 'commands/libcoreScanAccounts' import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast' import libcoreSyncAccount from 'commands/libcoreSyncAccount' +import libcoreValidAddress from 'commands/libcoreValidAddress' import listApps from 'commands/listApps' import listenDevices from 'commands/listenDevices' import signTransaction from 'commands/signTransaction' @@ -39,11 +41,13 @@ const all: Array> = [ installMcu, installOsuFirmware, isDashboardOpen, + libcoreGetFees, libcoreGetVersion, libcoreHardReset, libcoreScanAccounts, libcoreSignAndBroadcast, libcoreSyncAccount, + libcoreValidAddress, listApps, listenDevices, signTransaction, diff --git a/src/commands/libcoreGetFees.js b/src/commands/libcoreGetFees.js new file mode 100644 index 00000000..b08ec777 --- /dev/null +++ b/src/commands/libcoreGetFees.js @@ -0,0 +1,63 @@ +// @flow + +import { Observable } from 'rxjs' +import withLibcore from 'helpers/withLibcore' +import { createCommand, Command } from 'helpers/ipc' +import * as accountIdHelper from 'helpers/accountId' +import { isValidAddress } from 'helpers/libcore' +import createCustomErrorClass from 'helpers/createCustomErrorClass' + +const InvalidAddress = createCustomErrorClass('InvalidAddress') + +type BitcoinLikeTransaction = { + // TODO we rename this Transaction concept into transactionInput + amount: number, + feePerByte: number, + recipient: string, +} + +type Input = { + accountId: string, + accountIndex: number, + transaction: BitcoinLikeTransaction, +} + +type Result = { totalFees: number } + +const cmd: Command = createCommand( + 'libcoreGetFees', + ({ accountId, accountIndex, transaction }) => + Observable.create(o => { + let unsubscribed = false + const isCancelled = () => unsubscribed + + withLibcore(async core => { + const { walletName } = accountIdHelper.decode(accountId) + const njsWallet = await core.getWallet(walletName) + if (isCancelled()) return + const njsAccount = await njsWallet.getAccount(accountIndex) + if (isCancelled()) return + const bitcoinLikeAccount = njsAccount.asBitcoinLikeAccount() + const njsWalletCurrency = njsWallet.getCurrency() + const amount = core.createAmount(njsWalletCurrency, transaction.amount) + const feesPerByte = core.createAmount(njsWalletCurrency, transaction.feePerByte) + const transactionBuilder = bitcoinLikeAccount.buildTransaction() + if (!isValidAddress(core, njsWalletCurrency, transaction.recipient)) { + // FIXME this is a bug in libcore. later it will probably check this and we can remove this check + throw new InvalidAddress() + } + transactionBuilder.sendToAddress(amount, transaction.recipient) + transactionBuilder.pickInputs(0, 0xffffff) + transactionBuilder.setFeesPerByte(feesPerByte) + const builded = await transactionBuilder.build() + const totalFees = builded.getFees().toLong() + o.next({ totalFees }) + }).then(() => o.complete(), e => o.error(e)) + + return () => { + unsubscribed = true + } + }), +) + +export default cmd diff --git a/src/commands/libcoreValidAddress.js b/src/commands/libcoreValidAddress.js new file mode 100644 index 00000000..fc563d35 --- /dev/null +++ b/src/commands/libcoreValidAddress.js @@ -0,0 +1,24 @@ +// @flow + +import { fromPromise } from 'rxjs/observable/fromPromise' +import withLibcore from 'helpers/withLibcore' +import { createCommand, Command } from 'helpers/ipc' +import { isValidAddress } from 'helpers/libcore' + +type Input = { + address: string, + currencyId: string, +} + +const cmd: Command = createCommand( + 'libcoreValidAddress', + ({ currencyId, address }) => + fromPromise( + withLibcore(async core => { + const currency = await core.getCurrency(currencyId) + return isValidAddress(core, currency, address) + }), + ), +) + +export default cmd diff --git a/src/helpers/libcore.js b/src/helpers/libcore.js index 33ff6469..0cc0232d 100644 --- a/src/helpers/libcore.js +++ b/src/helpers/libcore.js @@ -16,6 +16,11 @@ import { getAccountPlaceholderName, getNewAccountPlaceholderName } from './accou const NoAddressesFound = createCustomErrorClass('NoAddressesFound') +export function isValidAddress(core: *, currency: *, address: string): boolean { + const addr = new core.NJSAddress(address, currency) + return addr.isValid(address, currency) +} + type Props = { core: *, devicePath: string, diff --git a/yarn.lock b/yarn.lock index cc52e93b..98e3bbef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1502,9 +1502,9 @@ dependencies: events "^2.0.0" -"@ledgerhq/ledger-core@1.7.0": - version "1.7.0" - resolved "https://registry.yarnpkg.com/@ledgerhq/ledger-core/-/ledger-core-1.7.0.tgz#ac3d738e1b6b2f0a4e18d645300259f8c02a4851" +"@ledgerhq/ledger-core@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/ledger-core/-/ledger-core-1.9.0.tgz#3240857e76f3c2b17ba02d120b96fd1b13b50789" dependencies: "@ledgerhq/hw-app-btc" "^4.7.3" "@ledgerhq/hw-transport-node-hid" "^4.7.6"