From 307aa85ddc41a38b0c71391e585b27fd7df98a79 Mon Sep 17 00:00:00 2001 From: ledger-bot <37080477+ledger-bot@users.noreply.github.com> Date: Sun, 3 Jun 2018 13:18:51 +0200 Subject: [PATCH 01/19] New Crowdin translations (#433) * New translations emptyState.yml (French) * New translations common.yml (French) * New translations account.yml (French) * New translations onboarding.yml (French) * New translations send.yml (French) * New translations send.yml (French) --- static/i18n/fr/send.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/static/i18n/fr/send.yml b/static/i18n/fr/send.yml index e93f1016..60948fa3 100644 --- a/static/i18n/fr/send.yml +++ b/static/i18n/fr/send.yml @@ -13,6 +13,7 @@ steps: useRBF: Utiliser la transaction RBF message: Laisser un message (140) rippleTag: Tag + ethereumGasLimit: Gas limit connectDevice: title: Connecter l'appareil verification: From d6e79f913262495395a94c57032215801954108a Mon Sep 17 00:00:00 2001 From: meriadec Date: Tue, 29 May 2018 18:34:05 +0200 Subject: [PATCH 02/19] Store real xpub in account --- src/helpers/libcore.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/helpers/libcore.js b/src/helpers/libcore.js index ef1476f4..2418d376 100644 --- a/src/helpers/libcore.js +++ b/src/helpers/libcore.js @@ -168,7 +168,6 @@ async function scanNextAccount(props: { wallet, currencyId, core, - hwApp, ops, }) @@ -217,7 +216,6 @@ async function buildAccountRaw({ wallet, currencyId, core, - hwApp, accountIndex, ops, }: { @@ -236,15 +234,11 @@ async function buildAccountRaw({ const balance = njsBalance.toLong() const jsCurrency = getCryptoCurrencyById(currencyId) - - // retrieve xpub const { derivations } = await wallet.getAccountCreationInfo(accountIndex) const [walletPath, accountPath] = derivations - const isVerify = false - const { bitcoinAddress } = await hwApp.getWalletPublicKey(accountPath, isVerify, isSegwit) - - const xpub = bitcoinAddress + // retrieve xpub + const xpub = njsAccount.getRestoreKey() // blockHeight const { height: blockHeight } = await njsAccount.getLastBlock() From b463e4bf53c7478dc7619bd561fd81bf00178169 Mon Sep 17 00:00:00 2001 From: meriadec Date: Tue, 29 May 2018 18:56:53 +0200 Subject: [PATCH 03/19] Create libcoreSyncAccount command --- src/commands/libcoreSyncAccount.js | 37 ++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/commands/libcoreSyncAccount.js diff --git a/src/commands/libcoreSyncAccount.js b/src/commands/libcoreSyncAccount.js new file mode 100644 index 00000000..b091e575 --- /dev/null +++ b/src/commands/libcoreSyncAccount.js @@ -0,0 +1,37 @@ +// @flow + +import type { AccountRaw } from '@ledgerhq/live-common/lib/types' +import { createCommand, Command } from 'helpers/ipc' +import { Observable } from 'rxjs' +import { syncAccount } from 'helpers/libcore' + +type Input = { + rawAccount: AccountRaw, +} + +type Result = AccountRaw + +const cmd: Command = createCommand( + 'accounts', + 'libcoreSyncAccount', + ({ rawAccount }) => + Observable.create(o => { + // TODO scanAccountsOnDevice should directly return a Observable so we just have to pass-in + syncAccount({ rawAccount }).then( + () => { + o.complete() + }, + e => { + o.error(e) + }, + ) + + function unsubscribe() { + // FIXME not implemented + } + + return unsubscribe + }), +) + +export default cmd From acdd98217945f4f50e782e145eec41efced9c9ec Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 30 May 2018 18:00:37 +0200 Subject: [PATCH 04/19] Prevent libcore race conditions --- src/commands/libcoreScanAccounts.js | 30 +++--- src/commands/libcoreSignAndBroadcast.js | 116 ++++++++++++------------ src/helpers/libcore.js | 16 ++-- src/helpers/withLibcore.js | 28 ++++++ src/init-ledger-core.js | 20 ---- 5 files changed, 112 insertions(+), 98 deletions(-) create mode 100644 src/helpers/withLibcore.js delete mode 100644 src/init-ledger-core.js diff --git a/src/commands/libcoreScanAccounts.js b/src/commands/libcoreScanAccounts.js index c20ab6d1..7044220d 100644 --- a/src/commands/libcoreScanAccounts.js +++ b/src/commands/libcoreScanAccounts.js @@ -4,6 +4,7 @@ import type { AccountRaw } from '@ledgerhq/live-common/lib/types' import { createCommand, Command } from 'helpers/ipc' import { Observable } from 'rxjs' import { scanAccountsOnDevice } from 'helpers/libcore' +import withLibcore from 'helpers/withLibcore' type Input = { devicePath: string, @@ -17,19 +18,22 @@ const cmd: Command = createCommand( ({ devicePath, currencyId }) => Observable.create(o => { // TODO scanAccountsOnDevice should directly return a Observable so we just have to pass-in - scanAccountsOnDevice({ - devicePath, - currencyId, - onAccountScanned: account => { - o.next(account) - }, - }).then( - () => { - o.complete() - }, - e => { - o.error(e) - }, + withLibcore(core => + scanAccountsOnDevice({ + core, + devicePath, + currencyId, + onAccountScanned: account => { + o.next(account) + }, + }).then( + () => { + o.complete() + }, + e => { + o.error(e) + }, + ), ) function unsubscribe() { diff --git a/src/commands/libcoreSignAndBroadcast.js b/src/commands/libcoreSignAndBroadcast.js index 5727b9f7..c08c63bd 100644 --- a/src/commands/libcoreSignAndBroadcast.js +++ b/src/commands/libcoreSignAndBroadcast.js @@ -2,11 +2,13 @@ import type { AccountRaw, OperationRaw } from '@ledgerhq/live-common/lib/types' import Btc from '@ledgerhq/hw-app-btc' +import { fromPromise } from 'rxjs/observable/fromPromise' +import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' + +import withLibcore from 'helpers/withLibcore' import { createCommand, Command } from 'helpers/ipc' import { withDevice } from 'helpers/deviceAccess' import { getWalletIdentifier } from 'helpers/libcore' -import { fromPromise } from 'rxjs/observable/fromPromise' -import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' type BitcoinLikeTransaction = { amount: number, @@ -24,70 +26,68 @@ type Result = $Exact const cmd: Command = createCommand( 'libcoreSignAndBroadcast', - ({ account, transaction, deviceId }) => { - // TODO: investigate why importing it on file scope causes trouble - const core = require('init-ledger-core')() - - return fromPromise( - withDevice(deviceId)(async transport => { - const hwApp = new Btc(transport) + ({ account, transaction, deviceId }) => + fromPromise( + withDevice(deviceId)(transport => + withLibcore(async core => { + const hwApp = new Btc(transport) - const WALLET_IDENTIFIER = await getWalletIdentifier({ - hwApp, - isSegwit: !!account.isSegwit, - currencyId: account.currencyId, - devicePath: deviceId, - }) + const WALLET_IDENTIFIER = await getWalletIdentifier({ + hwApp, + isSegwit: !!account.isSegwit, + currencyId: account.currencyId, + devicePath: deviceId, + }) - const njsWallet = await core.getWallet(WALLET_IDENTIFIER) - const njsAccount = await njsWallet.getAccount(account.index) - const bitcoinLikeAccount = njsAccount.asBitcoinLikeAccount() - const njsWalletCurrency = njsWallet.getCurrency() - const amount = core.createAmount(njsWalletCurrency, transaction.amount) - const fees = core.createAmount(njsWalletCurrency, transaction.feePerByte) - const transactionBuilder = bitcoinLikeAccount.buildTransaction() + const njsWallet = await core.getWallet(WALLET_IDENTIFIER) + const njsAccount = await njsWallet.getAccount(account.index) + const bitcoinLikeAccount = njsAccount.asBitcoinLikeAccount() + const njsWalletCurrency = njsWallet.getCurrency() + const amount = core.createAmount(njsWalletCurrency, transaction.amount) + const fees = core.createAmount(njsWalletCurrency, transaction.feePerByte) + const transactionBuilder = bitcoinLikeAccount.buildTransaction() - // TODO: check if is valid address. if not, it will fail silently on invalid + // TODO: check if is valid address. if not, it will fail silently on invalid - transactionBuilder.sendToAddress(amount, transaction.recipient) - // TODO: don't use hardcoded value for sequence (and first also maybe) - transactionBuilder.pickInputs(0, 0xffffff) - transactionBuilder.setFeesPerByte(fees) + transactionBuilder.sendToAddress(amount, transaction.recipient) + // TODO: don't use hardcoded value for sequence (and first also maybe) + transactionBuilder.pickInputs(0, 0xffffff) + transactionBuilder.setFeesPerByte(fees) - const builded = await transactionBuilder.build() - const sigHashType = core.helpers.bytesToHex( - njsWalletCurrency.bitcoinLikeNetworkParameters.SigHash, - ) + const builded = await transactionBuilder.build() + const sigHashType = core.helpers.bytesToHex( + njsWalletCurrency.bitcoinLikeNetworkParameters.SigHash, + ) - const currency = getCryptoCurrencyById(account.currencyId) - const signedTransaction = await core.signTransaction({ - hwApp, - transaction: builded, - sigHashType, - supportsSegwit: currency.supportsSegwit, - isSegwit: account.isSegwit, - }) + const currency = getCryptoCurrencyById(account.currencyId) + const signedTransaction = await core.signTransaction({ + hwApp, + transaction: builded, + sigHashType, + supportsSegwit: currency.supportsSegwit, + isSegwit: account.isSegwit, + }) - const txHash = await njsAccount - .asBitcoinLikeAccount() - .broadcastRawTransaction(signedTransaction) + const txHash = await njsAccount + .asBitcoinLikeAccount() + .broadcastRawTransaction(signedTransaction) - // optimistic operation - return { - id: txHash, - hash: txHash, - type: 'OUT', - value: amount, - blockHash: null, - blockHeight: null, - senders: [account.freshAddress], - recipients: [transaction.recipient], - accountId: account.id, - date: new Date().toISOString(), - } - }), - ) - }, + // optimistic operation + return { + id: txHash, + hash: txHash, + type: 'OUT', + value: amount, + blockHash: null, + blockHeight: null, + senders: [account.freshAddress], + recipients: [transaction.recipient], + accountId: account.id, + date: new Date().toISOString(), + } + }), + ), + ), ) export default cmd diff --git a/src/helpers/libcore.js b/src/helpers/libcore.js index 2418d376..070ae1f4 100644 --- a/src/helpers/libcore.js +++ b/src/helpers/libcore.js @@ -16,6 +16,7 @@ import type { AccountRaw, OperationRaw, OperationType } from '@ledgerhq/live-com import type { NJSAccount, NJSOperation } from '@ledgerhq/ledger-core/src/ledgercore_doc' type Props = { + core: Object, devicePath: string, currencyId: string, onAccountScanned: AccountRaw => void, @@ -24,13 +25,14 @@ type Props = { const { SHOW_LEGACY_NEW_ACCOUNT } = process.env export function scanAccountsOnDevice(props: Props): Promise { - const { devicePath, currencyId, onAccountScanned } = props + const { devicePath, currencyId, onAccountScanned, core } = props const currency = getCryptoCurrencyById(currencyId) return withDevice(devicePath)(async transport => { const hwApp = new Btc(transport) const commonParams = { + core, hwApp, currencyId, onAccountScanned, @@ -78,6 +80,7 @@ export async function getWalletIdentifier({ } async function scanAccountsOnDeviceBySegwit({ + core, hwApp, currencyId, onAccountScanned, @@ -96,12 +99,13 @@ async function scanAccountsOnDeviceBySegwit({ const WALLET_IDENTIFIER = await getWalletIdentifier({ hwApp, isSegwit, currencyId, devicePath }) // retrieve or create the wallet - const wallet = await getOrCreateWallet(WALLET_IDENTIFIER, currencyId, isSegwit) + const wallet = await getOrCreateWallet(core, WALLET_IDENTIFIER, currencyId, isSegwit) const accountsCount = await wallet.getAccountCount() // recursively scan all accounts on device on the given app // new accounts will be created in sqlite, existing ones will be updated const accounts = await scanNextAccount({ + core, wallet, hwApp, currencyId, @@ -119,6 +123,7 @@ async function scanAccountsOnDeviceBySegwit({ async function scanNextAccount(props: { // $FlowFixMe wallet: NJSWallet, + core: Object, hwApp: Object, currencyId: string, accountsCount: number, @@ -129,6 +134,7 @@ async function scanNextAccount(props: { showNewAccount: boolean, }): Promise { const { + core, wallet, hwApp, currencyId, @@ -140,9 +146,6 @@ async function scanNextAccount(props: { showNewAccount, } = props - // TODO: investigate why importing it on file scope causes trouble - const core = require('init-ledger-core')() - console.log(`>> Scanning account ${accountIndex} - isSegwit: ${isSegwit.toString()}`) // eslint-disable-line no-console // create account only if account has not been scanned yet @@ -187,12 +190,11 @@ async function scanNextAccount(props: { } async function getOrCreateWallet( + core: Object, WALLET_IDENTIFIER: string, currencyId: string, isSegwit: boolean, ): NJSWallet { - // TODO: investigate why importing it on file scope causes trouble - const core = require('init-ledger-core')() try { const wallet = await core.getWallet(WALLET_IDENTIFIER) return wallet diff --git a/src/helpers/withLibcore.js b/src/helpers/withLibcore.js new file mode 100644 index 00000000..0fc08212 --- /dev/null +++ b/src/helpers/withLibcore.js @@ -0,0 +1,28 @@ +// @flow + +const core = require('@ledgerhq/ledger-core') + +let instanciated = false +let queue = Promise.resolve() + +// TODO: `core` should be typed +type Job = Object => Promise + +export default function withLibcore(job: Job) { + if (!instanciated) { + core.instanciateWalletPool({ + // sqlite files will be located in the app local data folder + dbPath: process.env.LEDGER_LIVE_SQLITE_PATH, + }) + instanciated = true + } + queue = queue.then(() => { + try { + return job(core) + } catch (e) { + console.log(`withLibCore: Error in job`, e) // eslint-disable-line no-console + return Promise.resolve() + } + }) + return queue +} diff --git a/src/init-ledger-core.js b/src/init-ledger-core.js deleted file mode 100644 index 61e94e88..00000000 --- a/src/init-ledger-core.js +++ /dev/null @@ -1,20 +0,0 @@ -// Yep. That's a singleton. -// -// Electron needs to tell lib ledger core where to store the sqlite files, when -// instanciating wallet pool, but we don't need to do each everytime we -// require ledger-core, only the first time, so, eh. - -const core = require('@ledgerhq/ledger-core') - -let instanciated = false - -module.exports = () => { - if (!instanciated) { - core.instanciateWalletPool({ - // sqlite files will be located in the app local data folder - dbPath: process.env.LEDGER_LIVE_SQLITE_PATH, - }) - instanciated = true - } - return core -} From d8e2f57a8f0a4e649b11bf93c7a01e1df0e2e44b Mon Sep 17 00:00:00 2001 From: meriadec Date: Wed, 30 May 2018 18:28:30 +0200 Subject: [PATCH 05/19] Create barebone for libcore sync account --- src/bridge/LibcoreBridge.js | 22 ++++++++++++++++++++-- src/commands/index.js | 2 ++ src/commands/libcoreSyncAccount.js | 28 ++++++---------------------- src/helpers/libcore.js | 24 ++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/bridge/LibcoreBridge.js b/src/bridge/LibcoreBridge.js index 3d15dbec..81ce2cb8 100644 --- a/src/bridge/LibcoreBridge.js +++ b/src/bridge/LibcoreBridge.js @@ -4,6 +4,7 @@ import { map } from 'rxjs/operators' import { decodeAccount, encodeAccount } from 'reducers/accounts' import FeesBitcoinKind from 'components/FeesField/BitcoinKind' import libcoreScanAccounts from 'commands/libcoreScanAccounts' +import libcoreSyncAccount from 'commands/libcoreSyncAccount' import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast' // import AdvancedOptionsBitcoinKind from 'components/AdvancedOptions/BitcoinKind' import type { WalletBridge, EditProps } from './types' @@ -49,7 +50,24 @@ const LibcoreBridge: WalletBridge = { .subscribe(observer) }, - synchronize(_initialAccount, _observer) { + synchronize(account, { next, complete, error }) { + ;(async () => { + try { + const rawAccount = encodeAccount(account) + const rawSyncedAccount = await libcoreSyncAccount.send({ rawAccount }).toPromise() + const syncedAccount = decodeAccount(rawSyncedAccount) + next(account => ({ + ...account, + balance: syncedAccount.balance, + blockHeight: syncedAccount.blockHeight, + operations: syncedAccount.operations, // TODO: is a simple replace enough? + lastSyncDate: new Date(), + })) + complete() + } catch (e) { + error(e) + } + })() // FIXME TODO: use next(), to actually emit account updates..... // - need to sync the balance // - need to sync block height & block hash @@ -61,7 +79,7 @@ const LibcoreBridge: WalletBridge = { // then we probably should trash them out? it's a complex question for UI return { unsubscribe() { - console.warn('LibcoreBridge: sync not implemented') + console.warn('LibcoreBridge: unsub sync not implemented') }, } }, diff --git a/src/commands/index.js b/src/commands/index.js index 5698f374..86ae1ad5 100644 --- a/src/commands/index.js +++ b/src/commands/index.js @@ -16,6 +16,7 @@ import isCurrencyAppOpened from 'commands/isCurrencyAppOpened' import libcoreGetVersion from 'commands/libcoreGetVersion' import libcoreScanAccounts from 'commands/libcoreScanAccounts' import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast' +import libcoreSyncAccount from 'commands/libcoreSyncAccount' import listApps from 'commands/listApps' import listenDevices from 'commands/listenDevices' import signTransaction from 'commands/signTransaction' @@ -39,6 +40,7 @@ const all: Array> = [ libcoreGetVersion, libcoreScanAccounts, libcoreSignAndBroadcast, + libcoreSyncAccount, listApps, listenDevices, signTransaction, diff --git a/src/commands/libcoreSyncAccount.js b/src/commands/libcoreSyncAccount.js index b091e575..fd030939 100644 --- a/src/commands/libcoreSyncAccount.js +++ b/src/commands/libcoreSyncAccount.js @@ -1,37 +1,21 @@ // @flow import type { AccountRaw } from '@ledgerhq/live-common/lib/types' +import { fromPromise } from 'rxjs/observable/fromPromise' + import { createCommand, Command } from 'helpers/ipc' -import { Observable } from 'rxjs' import { syncAccount } from 'helpers/libcore' +import withLibcore from 'helpers/withLibcore' type Input = { + core: Object, rawAccount: AccountRaw, } type Result = AccountRaw -const cmd: Command = createCommand( - 'accounts', - 'libcoreSyncAccount', - ({ rawAccount }) => - Observable.create(o => { - // TODO scanAccountsOnDevice should directly return a Observable so we just have to pass-in - syncAccount({ rawAccount }).then( - () => { - o.complete() - }, - e => { - o.error(e) - }, - ) - - function unsubscribe() { - // FIXME not implemented - } - - return unsubscribe - }), +const cmd: Command = createCommand('libcoreSyncAccount', ({ rawAccount }) => + fromPromise(withLibcore(core => syncAccount({ rawAccount, core }))), ) export default cmd diff --git a/src/helpers/libcore.js b/src/helpers/libcore.js index 070ae1f4..fbbbce57 100644 --- a/src/helpers/libcore.js +++ b/src/helpers/libcore.js @@ -326,3 +326,27 @@ function buildOperationRaw({ date: op.getDate().toISOString(), } } + +export async function syncAccount({ rawAccount }: { rawAccount: AccountRaw }) { + // AWWWWW.. little problem here. + // + // we need to get account from libcore db. in order to do that we have to: + // 1) get wallet using a wallet identifier + // 2) get account from wallet using `rawAccount.index` + // + // Here is the problem: the wallet identifier is currently built like that: + // `${publicKey}__${currencyId}${isSegwit ? '_segwit' : ''}` + // + // and to get the `publicKey` we need the device. + // + // BUT we don't want the device to be required to access ledger-live + // SO.. it's a problem. + // + // Solution 1: store wallet identifier inside the Account (uurgh...) + // Solution 2: stop this project + + return { + ...rawAccount, + balance: 424242424242, + } +} From 8f122ffb9fab63c5887c3ed72a223e4a38758946 Mon Sep 17 00:00:00 2001 From: meriadec Date: Thu, 31 May 2018 12:01:26 +0200 Subject: [PATCH 06/19] Use `withLibcore` in libcoreGetVersion --- src/commands/libcoreGetVersion.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/commands/libcoreGetVersion.js b/src/commands/libcoreGetVersion.js index 052079b3..43a9924f 100644 --- a/src/commands/libcoreGetVersion.js +++ b/src/commands/libcoreGetVersion.js @@ -1,16 +1,17 @@ // @flow -import { createCommand, Command } from 'helpers/ipc' import { fromPromise } from 'rxjs/observable/fromPromise' +import { createCommand, Command } from 'helpers/ipc' +import withLibcore from 'helpers/withLibcore' + type Input = void type Result = { stringVersion: string, intVersion: number } const cmd: Command = createCommand('libcoreGetVersion', () => fromPromise( - Promise.resolve().then(() => { - const ledgerCore = require('init-ledger-core')() + withLibcore(ledgerCore => { const core = new ledgerCore.NJSLedgerCore() const stringVersion = core.getStringVersion() const intVersion = core.getIntVersion() From 0ddbafd4bdbee37522e15f7d9b9cefea14b62074 Mon Sep 17 00:00:00 2001 From: meriadec Date: Fri, 1 Jun 2018 09:27:33 +0200 Subject: [PATCH 07/19] Correct sigHashType && isSegwitSupported when signing tx --- src/commands/libcoreSignAndBroadcast.js | 4 ++-- src/helpers/libcore.js | 8 -------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/commands/libcoreSignAndBroadcast.js b/src/commands/libcoreSignAndBroadcast.js index c08c63bd..a804f95a 100644 --- a/src/commands/libcoreSignAndBroadcast.js +++ b/src/commands/libcoreSignAndBroadcast.js @@ -63,8 +63,8 @@ const cmd: Command = createCommand( const signedTransaction = await core.signTransaction({ hwApp, transaction: builded, - sigHashType, - supportsSegwit: currency.supportsSegwit, + sigHashType: `0x${sigHashType}`, + supportsSegwit: !!currency.supportsSegwit, isSegwit: account.isSegwit, }) diff --git a/src/helpers/libcore.js b/src/helpers/libcore.js index fbbbce57..05e7b168 100644 --- a/src/helpers/libcore.js +++ b/src/helpers/libcore.js @@ -1,13 +1,5 @@ // @flow -// Scan accounts on device -// ----------------------- -// -// _ ,--() -// ( )-'-.------|> -// " `--[] -// - import Btc from '@ledgerhq/hw-app-btc' import { withDevice } from 'helpers/deviceAccess' import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' From c1801befe1f7ab043f4f8742ff246718911110ab Mon Sep 17 00:00:00 2001 From: meriadec Date: Fri, 1 Jun 2018 12:46:35 +0200 Subject: [PATCH 08/19] Remove useless devices icon --- src/components/TopBar/index.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/TopBar/index.js b/src/components/TopBar/index.js index 76452029..a3871d2f 100644 --- a/src/components/TopBar/index.js +++ b/src/components/TopBar/index.js @@ -15,7 +15,6 @@ import { lock } from 'reducers/application' import { hasPassword } from 'reducers/settings' import { openModal } from 'reducers/modals' -import IconDevices from 'icons/Devices' import IconLock from 'icons/Lock' import IconSettings from 'icons/Settings' @@ -98,9 +97,6 @@ class TopBar extends PureComponent { - - - From 44531e6ba91afe6db1d1638a4f8d9c76692f83a1 Mon Sep 17 00:00:00 2001 From: meriadec Date: Fri, 1 Jun 2018 16:22:00 +0200 Subject: [PATCH 09/19] Send correct info through action -> command -> libcore -> redux --- src/bridge/LibcoreBridge.js | 14 ++--- src/commands/libcoreSignAndBroadcast.js | 2 +- src/commands/libcoreSyncAccount.js | 5 +- src/components/SideBar/Item.js | 10 +--- src/components/TopBar/ActivityIndicator.js | 11 ++-- src/helpers/accountId.js | 16 ++++++ src/helpers/libcore.js | 62 +++++++++++++--------- src/helpers/withLibcore.js | 19 ++++--- 8 files changed, 80 insertions(+), 59 deletions(-) create mode 100644 src/helpers/accountId.js diff --git a/src/bridge/LibcoreBridge.js b/src/bridge/LibcoreBridge.js index 81ce2cb8..789fbd1a 100644 --- a/src/bridge/LibcoreBridge.js +++ b/src/bridge/LibcoreBridge.js @@ -51,6 +51,11 @@ const LibcoreBridge: WalletBridge = { }, synchronize(account, { next, complete, error }) { + // FIXME TODO: + // - 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 ;(async () => { try { const rawAccount = encodeAccount(account) @@ -68,15 +73,6 @@ const LibcoreBridge: WalletBridge = { error(e) } })() - // 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 return { unsubscribe() { console.warn('LibcoreBridge: unsub sync not implemented') diff --git a/src/commands/libcoreSignAndBroadcast.js b/src/commands/libcoreSignAndBroadcast.js index a804f95a..ed852f05 100644 --- a/src/commands/libcoreSignAndBroadcast.js +++ b/src/commands/libcoreSignAndBroadcast.js @@ -63,7 +63,7 @@ const cmd: Command = createCommand( const signedTransaction = await core.signTransaction({ hwApp, transaction: builded, - sigHashType: `0x${sigHashType}`, + sigHashType: parseInt(sigHashType, 16).toString(), supportsSegwit: !!currency.supportsSegwit, isSegwit: account.isSegwit, }) diff --git a/src/commands/libcoreSyncAccount.js b/src/commands/libcoreSyncAccount.js index fd030939..a6a38ec6 100644 --- a/src/commands/libcoreSyncAccount.js +++ b/src/commands/libcoreSyncAccount.js @@ -8,14 +8,15 @@ import { syncAccount } from 'helpers/libcore' import withLibcore from 'helpers/withLibcore' type Input = { - core: Object, rawAccount: AccountRaw, } type Result = AccountRaw const cmd: Command = createCommand('libcoreSyncAccount', ({ rawAccount }) => - fromPromise(withLibcore(core => syncAccount({ rawAccount, core }))), + fromPromise( + withLibcore((core, njsWalletPool) => syncAccount({ rawAccount, core, njsWalletPool })), + ), ) export default cmd diff --git a/src/components/SideBar/Item.js b/src/components/SideBar/Item.js index 71e48124..932de6da 100644 --- a/src/components/SideBar/Item.js +++ b/src/components/SideBar/Item.js @@ -3,7 +3,7 @@ import React from 'react' import styled from 'styled-components' import { compose } from 'redux' -import { matchPath, withRouter } from 'react-router' +import { withRouter } from 'react-router' import { push } from 'react-router-redux' import { connect } from 'react-redux' @@ -91,13 +91,7 @@ function Item({ highlight, }: Props) { const { pathname } = location - const isActive = linkTo - ? linkTo === '/' - ? linkTo === pathname - : matchPath(pathname, { - path: linkTo, - }) - : false + const isActive = linkTo === pathname return ( { render() { const { pending, error, onClick } = this.props return ( - + diff --git a/src/helpers/accountId.js b/src/helpers/accountId.js new file mode 100644 index 00000000..b70b5cef --- /dev/null +++ b/src/helpers/accountId.js @@ -0,0 +1,16 @@ +// @flow + +type Params = { + type: string, + xpub: string, + walletName: string, +} + +export function encode({ type, xpub, walletName }: Params) { + return `${type}:${xpub}:${walletName}` +} + +export function decode(accountId: string) { + const [type, xpub, walletName] = accountId.split(':') + return { type, xpub, walletName } +} diff --git a/src/helpers/libcore.js b/src/helpers/libcore.js index 05e7b168..b50f5320 100644 --- a/src/helpers/libcore.js +++ b/src/helpers/libcore.js @@ -7,6 +7,8 @@ import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currenc import type { AccountRaw, OperationRaw, OperationType } from '@ledgerhq/live-common/lib/types' import type { NJSAccount, NJSOperation } from '@ledgerhq/ledger-core/src/ledgercore_doc' +import * as accountId from 'helpers/accountId' + type Props = { core: Object, devicePath: string, @@ -80,6 +82,7 @@ async function scanAccountsOnDeviceBySegwit({ isSegwit, showNewAccount, }: { + core: Object, hwApp: Object, currencyId: string, onAccountScanned: AccountRaw => void, @@ -138,8 +141,6 @@ async function scanNextAccount(props: { showNewAccount, } = props - console.log(`>> Scanning account ${accountIndex} - isSegwit: ${isSegwit.toString()}`) // eslint-disable-line no-console - // create account only if account has not been scanned yet // if it has already been created, we just need to get it, and sync it const hasBeenScanned = accountIndex < accountsCount @@ -220,7 +221,6 @@ async function buildAccountRaw({ currencyId: string, accountIndex: number, core: Object, - hwApp: Object, // $FlowFixMe ops: NJSOperation[], }): Promise { @@ -260,7 +260,7 @@ async function buildAccountRaw({ } const rawAccount: AccountRaw = { - id: xpub, // FIXME for account id you might want to prepend the crypto currency id to this because it's not gonna be unique. + id: accountId.encode({ type: 'libcore', xpub, walletName: wallet.getName() }), xpub, path: walletPath, name, @@ -319,26 +319,38 @@ function buildOperationRaw({ } } -export async function syncAccount({ rawAccount }: { rawAccount: AccountRaw }) { - // AWWWWW.. little problem here. - // - // we need to get account from libcore db. in order to do that we have to: - // 1) get wallet using a wallet identifier - // 2) get account from wallet using `rawAccount.index` - // - // Here is the problem: the wallet identifier is currently built like that: - // `${publicKey}__${currencyId}${isSegwit ? '_segwit' : ''}` - // - // and to get the `publicKey` we need the device. - // - // BUT we don't want the device to be required to access ledger-live - // SO.. it's a problem. - // - // Solution 1: store wallet identifier inside the Account (uurgh...) - // Solution 2: stop this project +export async function syncAccount({ + rawAccount, + core, + njsWalletPool, +}: { + core: Object, + rawAccount: AccountRaw, + njsWalletPool: Object, +}) { + const decodedAccountId = accountId.decode(rawAccount.id) + const njsWallet = await njsWalletPool.getWallet(decodedAccountId.walletName) + const njsAccount = await njsWallet.getAccount(rawAccount.index) - return { - ...rawAccount, - balance: 424242424242, - } + await core.syncAccount(njsAccount) + + const query = njsAccount.queryOperations() + const ops = await query.complete().execute() + const njsBalance = await njsAccount.getBalance() + + const syncedRawAccount = await buildAccountRaw({ + njsAccount, + isSegwit: rawAccount.isSegwit === true, + accountIndex: rawAccount.index, + wallet: njsWallet, + currencyId: rawAccount.currencyId, + core, + ops, + }) + + syncedRawAccount.balance = njsBalance.toLong() + + console.log(`Synced account [${syncedRawAccount.name}]: ${syncedRawAccount.balance}`) + + return syncedRawAccount } diff --git a/src/helpers/withLibcore.js b/src/helpers/withLibcore.js index 0fc08212..7b053ea4 100644 --- a/src/helpers/withLibcore.js +++ b/src/helpers/withLibcore.js @@ -2,23 +2,26 @@ const core = require('@ledgerhq/ledger-core') -let instanciated = false +let walletPool = null let queue = Promise.resolve() -// TODO: `core` should be typed -type Job = Object => Promise +// TODO: `core` and `NJSWalletPool` should be typed +type Job = (Object, Object) => any export default function withLibcore(job: Job) { - if (!instanciated) { - core.instanciateWalletPool({ + if (!walletPool) { + walletPool = core.instanciateWalletPool({ // sqlite files will be located in the app local data folder dbPath: process.env.LEDGER_LIVE_SQLITE_PATH, }) - instanciated = true } - queue = queue.then(() => { + // $FlowFixMe WTF is happening here, dudes. + queue = queue.then(async () => { try { - return job(core) + if (!walletPool) { + throw new Error('wallet pool not instanciated. this should not happen') + } + return job(core, walletPool) } catch (e) { console.log(`withLibCore: Error in job`, e) // eslint-disable-line no-console return Promise.resolve() From 7627785d8b6f46e9de680477560e2f96596f1970 Mon Sep 17 00:00:00 2001 From: meriadec Date: Fri, 1 Jun 2018 16:13:39 +0200 Subject: [PATCH 10/19] Make UPDATE_ACCOUNT a standard redux action --- src/actions/accounts.js | 13 +++++++------ src/reducers/accounts.js | 4 +++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/actions/accounts.js b/src/actions/accounts.js index 03f92870..3bbc2d1b 100644 --- a/src/actions/accounts.js +++ b/src/actions/accounts.js @@ -67,9 +67,8 @@ export const fetchAccounts: FetchAccounts = () => (dispatch, getState) => { export type UpdateAccountWithUpdater = (accountId: string, (Account) => Account) => * export const updateAccountWithUpdater: UpdateAccountWithUpdater = (accountId, updater) => ({ - type: 'UPDATE_ACCOUNT', - accountId, - updater, + type: 'DB:UPDATE_ACCOUNT', + payload: { accountId, updater }, }) export type UpdateAccount = ($Shape) => (Function, Function) => void @@ -78,9 +77,11 @@ export const updateAccount: UpdateAccount = payload => (dispatch, getState) => { settings: { orderAccounts }, } = getState() dispatch({ - type: 'UPDATE_ACCOUNT', - updater: account => ({ ...account, ...payload }), - accountId: payload.id, + type: 'DB:UPDATE_ACCOUNT', + payload: { + updater: account => ({ ...account, ...payload }), + accountId: payload.id, + }, }) dispatch(updateOrderAccounts(orderAccounts)) // TODO should not be here IMO.. feels wrong for perf, probably better to move in reducer too diff --git a/src/reducers/accounts.js b/src/reducers/accounts.js index 5e12bd16..e413889f 100644 --- a/src/reducers/accounts.js +++ b/src/reducers/accounts.js @@ -30,7 +30,9 @@ const handlers: Object = { UPDATE_ACCOUNT: ( state: AccountsState, - { accountId, updater }: { accountId: string, updater: Account => Account }, + { + payload: { accountId, updater }, + }: { payload: { accountId: string, updater: Account => Account } }, ): AccountsState => state.map(existingAccount => { if (existingAccount.id !== accountId) { From 76956a8ab36179a2517fc82cfd3b897435dfdac7 Mon Sep 17 00:00:00 2001 From: meriadec Date: Sat, 2 Jun 2018 18:05:53 +0200 Subject: [PATCH 11/19] process.env.__DEV__ to enable dev mode --- src/reducers/settings.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/reducers/settings.js b/src/reducers/settings.js index bcf5eb6e..8a44a97d 100644 --- a/src/reducers/settings.js +++ b/src/reducers/settings.js @@ -63,7 +63,7 @@ const INITIAL_STATE: SettingsState = { marketIndicator: 'western', currenciesSettings: {}, region, - developerMode: false, + developerMode: !!process.env.__DEV__, loaded: false, shareAnalytics: false, } @@ -106,6 +106,7 @@ const handlers: Object = { ) => ({ ...state, ...settings, + developerMode: settings.developerMode || !!process.env.__DEV__, }), FETCH_SETTINGS: ( state: SettingsState, @@ -113,6 +114,7 @@ const handlers: Object = { ) => ({ ...state, ...settings, + developerMode: settings.developerMode || !!process.env.__DEV__, loaded: true, }), } From 115b84e6126a551cd1fe922c8d96d5c492ac937a Mon Sep 17 00:00:00 2001 From: meriadec Date: Sat, 2 Jun 2018 22:14:35 +0200 Subject: [PATCH 12/19] Bump ledger-core@1.4.3 --- package.json | 2 +- src/helpers/libcore.js | 3 ++- yarn.lock | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d445f66c..b2f1490b 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@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.4.1", + "@ledgerhq/ledger-core": "1.4.3", "@ledgerhq/live-common": "2.24.0", "axios": "^0.18.0", "babel-runtime": "^6.26.0", diff --git a/src/helpers/libcore.js b/src/helpers/libcore.js index b50f5320..f93818d7 100644 --- a/src/helpers/libcore.js +++ b/src/helpers/libcore.js @@ -332,7 +332,8 @@ export async function syncAccount({ const njsWallet = await njsWalletPool.getWallet(decodedAccountId.walletName) const njsAccount = await njsWallet.getAccount(rawAccount.index) - await core.syncAccount(njsAccount) + const unsub = await core.syncAccount(njsAccount) + unsub() const query = njsAccount.queryOperations() const ops = await query.complete().execute() diff --git a/yarn.lock b/yarn.lock index d0e3b30c..0a5686ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1482,9 +1482,9 @@ dependencies: events "^2.0.0" -"@ledgerhq/ledger-core@1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@ledgerhq/ledger-core/-/ledger-core-1.4.1.tgz#c12d4a9140765731458ff1c68112818948c7f91d" +"@ledgerhq/ledger-core@1.4.3": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@ledgerhq/ledger-core/-/ledger-core-1.4.3.tgz#6cc44560e5a8fb35f85c8ad9fbae56436eacfc94" dependencies: "@ledgerhq/hw-app-btc" "^4.7.3" "@ledgerhq/hw-transport-node-hid" "^4.7.6" From 00feedb2949667bf7839af85ade058c81f08dd85 Mon Sep 17 00:00:00 2001 From: meriadec Date: Sun, 3 Jun 2018 00:13:16 +0200 Subject: [PATCH 13/19] Order operations in libcore sync --- src/helpers/libcore.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/helpers/libcore.js b/src/helpers/libcore.js index f93818d7..1d524c65 100644 --- a/src/helpers/libcore.js +++ b/src/helpers/libcore.js @@ -251,6 +251,8 @@ async function buildAccountRaw({ const { str: freshAddress, path: freshAddressPath } = addresses[0] + ops.sort((a, b) => b.getDate() - a.getDate()) + const operations = ops.map(op => buildOperationRaw({ core, op, xpub })) const currency = getCryptoCurrencyById(currencyId) From 34e54f814c4a5f0fb62a07ee5e899334993e405a Mon Sep 17 00:00:00 2001 From: meriadec Date: Sun, 3 Jun 2018 02:39:32 +0200 Subject: [PATCH 14/19] Add utility script --- package.json | 1 + scripts/hey.js | 131 ++++++++++++++++++++++++ src/commands/libcoreSignAndBroadcast.js | 122 +++++++++++----------- src/helpers/libcore.js | 19 +++- yarn.lock | 32 +++++- 5 files changed, 244 insertions(+), 61 deletions(-) create mode 100644 scripts/hey.js diff --git a/package.json b/package.json index b2f1490b..300d5c34 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,7 @@ "flow-typed": "^2.4.0", "hard-source-webpack-plugin": "^0.6.0", "husky": "^0.14.3", + "inquirer": "^6.0.0", "jest": "^22.4.3", "js-yaml": "^3.10.0", "node-loader": "^0.6.0", diff --git a/scripts/hey.js b/scripts/hey.js new file mode 100644 index 00000000..4c361809 --- /dev/null +++ b/scripts/hey.js @@ -0,0 +1,131 @@ +require('babel-polyfill') +require('babel-register') + +const chalk = require('chalk') +const inquirer = require('inquirer') +const path = require('path') +const TransportNodeHid = require('@ledgerhq/hw-transport-node-hid').default + +const { serializeAccounts, encodeAccount, decodeAccount } = require('../src/reducers/accounts') +const { doSignAndBroadcast } = require('../src/commands/libcoreSignAndBroadcast') + +const coreHelper = require('../src/helpers/libcore') +const withLibcore = require('../src/helpers/withLibcore').default + +if (!process.env.LEDGER_LIVE_SQLITE_PATH) { + throw new Error('you must define process.env.LEDGER_LIVE_SQLITE_PATH first') +} + +const LOCAL_DIRECTORY_PATH = path.resolve(process.env.LEDGER_LIVE_SQLITE_PATH, '../') + +gimmeDeviceAndLibCore(async ({ device, core, njsWalletPool }) => { + const raw = require(path.join(LOCAL_DIRECTORY_PATH, 'accounts.json')) // eslint-disable-line import/no-dynamic-require + const accounts = serializeAccounts(raw.data) + const accountToUse = await chooseAccount('Which account to use?', accounts) + await actionLoop({ account: accountToUse, accounts, core, njsWalletPool, device }) + process.exit(0) +}) + +async function actionLoop(props) { + try { + const { account, accounts, core, njsWalletPool, device } = props + const actionToDo = await chooseAction(`What do you want to do with [${account.name}] ?`) + if (actionToDo === 'send funds') { + const transport = await TransportNodeHid.open(device.path) + const accountToReceive = await chooseAccount('To which account?', accounts) + const receiveAddress = await getFreshAddress({ + account: accountToReceive, + core, + njsWalletPool, + }) + console.log(`the receive address is ${receiveAddress}`) + const rawAccount = encodeAccount(account) + console.log(`trying to sign and broadcast...`) + const rawOp = await doSignAndBroadcast({ + account: rawAccount, + transaction: { + amount: 4200000, + recipient: receiveAddress, + feePerByte: 16, + isRBF: false, + }, + deviceId: device.path, + core, + transport, + }) + console.log(rawOp) + } else if (actionToDo === 'sync') { + console.log(`\nLaunch sync...\n`) + const rawAccount = encodeAccount(account) + const syncedAccount = await coreHelper.syncAccount({ rawAccount, core, njsWalletPool }) + console.log(`\nEnd sync...\n`) + console.log(`updated account: `, displayAccount(syncedAccount, 'red')) + } else if (actionToDo === 'quit') { + return true + } + } catch (err) { + console.log(`x Something went wrong`) + console.log(err) + process.exit(1) + } + return actionLoop(props) +} + +async function chooseInList(msg, list, formatItem = i => i) { + const choices = list.map(formatItem) + const { choice } = await inquirer.prompt([ + { + type: 'list', + name: 'choice', + message: msg, + choices, + }, + ]) + const index = choices.indexOf(choice) + return list[index] +} + +async function chooseAction(msg) { + return chooseInList(msg, ['sync', 'send funds', 'quit']) +} + +function chooseAccount(msg, accounts) { + return chooseInList(msg, accounts, acc => displayAccount(acc)) +} + +async function gimmeDeviceAndLibCore(cb) { + withLibcore((core, njsWalletPool) => { + TransportNodeHid.listen({ + error: () => {}, + complete: () => {}, + next: async e => { + if (!e.device) { + return + } + if (e.type === 'add') { + const { device } = e + cb({ device, core, njsWalletPool }) + } + }, + }) + }) +} + +function displayAccount(acc, color = null) { + const isRawAccount = typeof acc.lastSyncDate === 'string' + if (isRawAccount) { + acc = decodeAccount(acc) + } + const str = `[${acc.name}] ${acc.isSegwit ? '' : '(legacy) '}${acc.unit.code} ${acc.balance} - ${ + acc.operations.length + } txs` + return color ? chalk[color](str) : str +} + +async function getFreshAddress({ account, core, njsWalletPool }) { + const njsAccount = await coreHelper.getNJSAccount({ account, njsWalletPool }) + const unsub = await core.syncAccount(njsAccount) + unsub() + const rawAddresses = await njsAccount.getFreshPublicAddresses() + return rawAddresses[0] +} diff --git a/src/commands/libcoreSignAndBroadcast.js b/src/commands/libcoreSignAndBroadcast.js index ed852f05..8ae4d954 100644 --- a/src/commands/libcoreSignAndBroadcast.js +++ b/src/commands/libcoreSignAndBroadcast.js @@ -29,65 +29,73 @@ const cmd: Command = createCommand( ({ account, transaction, deviceId }) => fromPromise( withDevice(deviceId)(transport => - withLibcore(async core => { - const hwApp = new Btc(transport) - - const WALLET_IDENTIFIER = await getWalletIdentifier({ - hwApp, - isSegwit: !!account.isSegwit, - currencyId: account.currencyId, - devicePath: deviceId, - }) - - const njsWallet = await core.getWallet(WALLET_IDENTIFIER) - const njsAccount = await njsWallet.getAccount(account.index) - const bitcoinLikeAccount = njsAccount.asBitcoinLikeAccount() - const njsWalletCurrency = njsWallet.getCurrency() - const amount = core.createAmount(njsWalletCurrency, transaction.amount) - const fees = core.createAmount(njsWalletCurrency, transaction.feePerByte) - const transactionBuilder = bitcoinLikeAccount.buildTransaction() - - // TODO: check if is valid address. if not, it will fail silently on invalid - - transactionBuilder.sendToAddress(amount, transaction.recipient) - // TODO: don't use hardcoded value for sequence (and first also maybe) - transactionBuilder.pickInputs(0, 0xffffff) - transactionBuilder.setFeesPerByte(fees) - - const builded = await transactionBuilder.build() - const sigHashType = core.helpers.bytesToHex( - njsWalletCurrency.bitcoinLikeNetworkParameters.SigHash, - ) - - const currency = getCryptoCurrencyById(account.currencyId) - const signedTransaction = await core.signTransaction({ - hwApp, - transaction: builded, - sigHashType: parseInt(sigHashType, 16).toString(), - supportsSegwit: !!currency.supportsSegwit, - isSegwit: account.isSegwit, - }) - - const txHash = await njsAccount - .asBitcoinLikeAccount() - .broadcastRawTransaction(signedTransaction) - - // optimistic operation - return { - id: txHash, - hash: txHash, - type: 'OUT', - value: amount, - blockHash: null, - blockHeight: null, - senders: [account.freshAddress], - recipients: [transaction.recipient], - accountId: account.id, - date: new Date().toISOString(), - } - }), + withLibcore(core => + doSignAndBroadcast({ + account, + transaction, + deviceId, + core, + transport, + }), + ), ), ), ) +export async function doSignAndBroadcast({ account, transaction, deviceId, core, transport }) { + const hwApp = new Btc(transport) + + const WALLET_IDENTIFIER = await getWalletIdentifier({ + hwApp, + isSegwit: !!account.isSegwit, + currencyId: account.currencyId, + devicePath: deviceId, + }) + + const njsWallet = await core.getWallet(WALLET_IDENTIFIER) + const njsAccount = await njsWallet.getAccount(account.index) + const bitcoinLikeAccount = njsAccount.asBitcoinLikeAccount() + const njsWalletCurrency = njsWallet.getCurrency() + const amount = core.createAmount(njsWalletCurrency, transaction.amount) + const fees = core.createAmount(njsWalletCurrency, transaction.feePerByte) + const transactionBuilder = bitcoinLikeAccount.buildTransaction() + + // TODO: check if is valid address. if not, it will fail silently on invalid + + transactionBuilder.sendToAddress(amount, transaction.recipient) + // TODO: don't use hardcoded value for sequence (and first also maybe) + transactionBuilder.pickInputs(0, 0xffffff) + transactionBuilder.setFeesPerByte(fees) + + const builded = await transactionBuilder.build() + const sigHashType = core.helpers.bytesToHex( + njsWalletCurrency.bitcoinLikeNetworkParameters.SigHash, + ) + + const currency = getCryptoCurrencyById(account.currencyId) + const signedTransaction = await core.signTransaction({ + hwApp, + transaction: builded, + sigHashType: parseInt(sigHashType, 16).toString(), + supportsSegwit: !!currency.supportsSegwit, + isSegwit: account.isSegwit, + }) + + const txHash = await njsAccount.asBitcoinLikeAccount().broadcastRawTransaction(signedTransaction) + + // optimistic operation + return { + id: txHash, + hash: txHash, + type: 'OUT', + value: amount, + blockHash: null, + blockHeight: null, + senders: [account.freshAddress], + recipients: [transaction.recipient], + accountId: account.id, + date: new Date().toISOString(), + } +} + export default cmd diff --git a/src/helpers/libcore.js b/src/helpers/libcore.js index 1d524c65..2a09a3a4 100644 --- a/src/helpers/libcore.js +++ b/src/helpers/libcore.js @@ -7,7 +7,7 @@ import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currenc import type { AccountRaw, OperationRaw, OperationType } from '@ledgerhq/live-common/lib/types' import type { NJSAccount, NJSOperation } from '@ledgerhq/ledger-core/src/ledgercore_doc' -import * as accountId from 'helpers/accountId' +import * as accountIdHelper from 'helpers/accountId' type Props = { core: Object, @@ -262,7 +262,7 @@ async function buildAccountRaw({ } const rawAccount: AccountRaw = { - id: accountId.encode({ type: 'libcore', xpub, walletName: wallet.getName() }), + id: accountIdHelper.encode({ type: 'libcore', xpub, walletName: wallet.getName() }), xpub, path: walletPath, name, @@ -321,6 +321,19 @@ function buildOperationRaw({ } } +export async function getNJSAccount({ + account, + njsWalletPool, +}: { + accountId: string, + njsWalletPool: any, +}) { + const decodedAccountId = accountIdHelper.decode(account.id) + const njsWallet = await njsWalletPool.getWallet(decodedAccountId.walletName) + const njsAccount = await njsWallet.getAccount(account.index) + return njsAccount +} + export async function syncAccount({ rawAccount, core, @@ -330,7 +343,7 @@ export async function syncAccount({ rawAccount: AccountRaw, njsWalletPool: Object, }) { - const decodedAccountId = accountId.decode(rawAccount.id) + const decodedAccountId = accountIdHelper.decode(rawAccount.id) const njsWallet = await njsWalletPool.getWallet(decodedAccountId.walletName) const njsAccount = await njsWallet.getAccount(rawAccount.index) diff --git a/yarn.lock b/yarn.lock index 0a5686ae..81bae4a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4099,6 +4099,10 @@ chardet@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" +chardet@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.5.0.tgz#fe3ac73c00c3d865ffcc02a0682e2c20b6a06029" + charenc@~0.0.1: version "0.0.2" resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" @@ -6374,6 +6378,14 @@ external-editor@^2.0.4, external-editor@^2.1.0: iconv-lite "^0.4.17" tmp "^0.0.33" +external-editor@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.0.tgz#dc35c48c6f98a30ca27a20e9687d7f3c77704bb6" + dependencies: + chardet "^0.5.0" + iconv-lite "^0.4.22" + tmp "^0.0.33" + extglob@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" @@ -7539,7 +7551,7 @@ i18next@^11.2.2: version "11.3.2" resolved "https://registry.yarnpkg.com/i18next/-/i18next-11.3.2.tgz#4a1a7bb14383ba6aed4abca139b03681fc96e023" -iconv-lite@0.4, iconv-lite@^0.4.17, iconv-lite@^0.4.23, iconv-lite@^0.4.4, iconv-lite@~0.4.13: +iconv-lite@0.4, iconv-lite@^0.4.17, iconv-lite@^0.4.22, iconv-lite@^0.4.23, iconv-lite@^0.4.4, iconv-lite@~0.4.13: version "0.4.23" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" dependencies: @@ -7697,6 +7709,24 @@ inquirer@^5.2.0: strip-ansi "^4.0.0" through "^2.3.6" +inquirer@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.0.0.tgz#e8c20303ddc15bbfc2c12a6213710ccd9e1413d8" + dependencies: + ansi-escapes "^3.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^3.0.0" + figures "^2.0.0" + lodash "^4.3.0" + mute-stream "0.0.7" + run-async "^2.2.0" + rxjs "^6.1.0" + string-width "^2.1.0" + strip-ansi "^4.0.0" + through "^2.3.6" + insert-css@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/insert-css/-/insert-css-2.0.0.tgz#eb5d1097b7542f4c79ea3060d3aee07d053880f4" From cfc47ef41808ae36abaa6a2971493fc1f0a395c1 Mon Sep 17 00:00:00 2001 From: meriadec Date: Sun, 3 Jun 2018 03:23:05 +0200 Subject: [PATCH 15/19] Uniq senders in OperationDetails --- src/components/modals/OperationDetails.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/modals/OperationDetails.js b/src/components/modals/OperationDetails.js index 476d8b7a..e90ba0ed 100644 --- a/src/components/modals/OperationDetails.js +++ b/src/components/modals/OperationDetails.js @@ -1,6 +1,7 @@ // @flow import React from 'react' +import uniq from 'lodash/uniq' import { connect } from 'react-redux' import { shell } from 'electron' import { translate } from 'react-i18next' @@ -78,6 +79,7 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => { const isConfirmed = confirmations >= currencySettings.confirmationsNb const url = getTxURL(account, operation) + const uniqSenders = uniq(senders) return ( @@ -132,7 +134,7 @@ const OperationDetails = connect(mapStateToProps)((props: Props) => { From - {senders.map(v => {v} )} + {uniqSenders.map(v => {v})} From b43f6733007e9b18b54e00c5d7a3721ac3c2e5b8 Mon Sep 17 00:00:00 2001 From: meriadec Date: Sun, 3 Jun 2018 12:31:07 +0200 Subject: [PATCH 16/19] Flow fixes and typecheck account id encode/decode --- src/commands/libcoreGetVersion.js | 2 +- src/commands/libcoreSignAndBroadcast.js | 15 ++++++++++- src/helpers/accountId.js | 16 ++++++++---- src/helpers/libcore.js | 33 ++++++++++++++----------- 4 files changed, 45 insertions(+), 21 deletions(-) diff --git a/src/commands/libcoreGetVersion.js b/src/commands/libcoreGetVersion.js index 43a9924f..e8fbd065 100644 --- a/src/commands/libcoreGetVersion.js +++ b/src/commands/libcoreGetVersion.js @@ -11,7 +11,7 @@ type Result = { stringVersion: string, intVersion: number } const cmd: Command = createCommand('libcoreGetVersion', () => fromPromise( - withLibcore(ledgerCore => { + withLibcore(async ledgerCore => { const core = new ledgerCore.NJSLedgerCore() const stringVersion = core.getStringVersion() const intVersion = core.getIntVersion() diff --git a/src/commands/libcoreSignAndBroadcast.js b/src/commands/libcoreSignAndBroadcast.js index 8ae4d954..22324197 100644 --- a/src/commands/libcoreSignAndBroadcast.js +++ b/src/commands/libcoreSignAndBroadcast.js @@ -4,6 +4,7 @@ import type { AccountRaw, OperationRaw } from '@ledgerhq/live-common/lib/types' import Btc from '@ledgerhq/hw-app-btc' import { fromPromise } from 'rxjs/observable/fromPromise' import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' +import type Transport from '@ledgerhq/hw-transport' import withLibcore from 'helpers/withLibcore' import { createCommand, Command } from 'helpers/ipc' @@ -42,7 +43,19 @@ const cmd: Command = createCommand( ), ) -export async function doSignAndBroadcast({ account, transaction, deviceId, core, transport }) { +export async function doSignAndBroadcast({ + account, + transaction, + deviceId, + core, + transport, +}: { + account: AccountRaw, + transaction: BitcoinLikeTransaction, + deviceId: string, + core: *, + transport: Transport<*>, +}) { const hwApp = new Btc(transport) const WALLET_IDENTIFIER = await getWalletIdentifier({ diff --git a/src/helpers/accountId.js b/src/helpers/accountId.js index b70b5cef..543a8223 100644 --- a/src/helpers/accountId.js +++ b/src/helpers/accountId.js @@ -1,16 +1,22 @@ // @flow +import invariant from 'invariant' + type Params = { type: string, + version: string, xpub: string, walletName: string, } -export function encode({ type, xpub, walletName }: Params) { - return `${type}:${xpub}:${walletName}` +export function encode({ type, version, xpub, walletName }: Params) { + return `${type}:${version}:${xpub}:${walletName}` } -export function decode(accountId: string) { - const [type, xpub, walletName] = accountId.split(':') - return { type, xpub, walletName } +export function decode(accountId: string): Params { + invariant(typeof accountId === 'string', 'accountId is not a string') + const splitted = accountId.split(':') + invariant(splitted.length === 4, 'invalid size for accountId') + const [type, version, xpub, walletName] = splitted + return { type, version, xpub, walletName } } diff --git a/src/helpers/libcore.js b/src/helpers/libcore.js index 2a09a3a4..6fd035d1 100644 --- a/src/helpers/libcore.js +++ b/src/helpers/libcore.js @@ -10,7 +10,7 @@ import type { NJSAccount, NJSOperation } from '@ledgerhq/ledger-core/src/ledgerc import * as accountIdHelper from 'helpers/accountId' type Props = { - core: Object, + core: *, devicePath: string, currencyId: string, onAccountScanned: AccountRaw => void, @@ -82,7 +82,7 @@ async function scanAccountsOnDeviceBySegwit({ isSegwit, showNewAccount, }: { - core: Object, + core: *, hwApp: Object, currencyId: string, onAccountScanned: AccountRaw => void, @@ -118,7 +118,7 @@ async function scanAccountsOnDeviceBySegwit({ async function scanNextAccount(props: { // $FlowFixMe wallet: NJSWallet, - core: Object, + core: *, hwApp: Object, currencyId: string, accountsCount: number, @@ -183,7 +183,7 @@ async function scanNextAccount(props: { } async function getOrCreateWallet( - core: Object, + core: *, WALLET_IDENTIFIER: string, currencyId: string, isSegwit: boolean, @@ -220,7 +220,7 @@ async function buildAccountRaw({ wallet: NJSWallet, currencyId: string, accountIndex: number, - core: Object, + core: *, // $FlowFixMe ops: NJSOperation[], }): Promise { @@ -262,7 +262,12 @@ async function buildAccountRaw({ } const rawAccount: AccountRaw = { - id: accountIdHelper.encode({ type: 'libcore', xpub, walletName: wallet.getName() }), + id: accountIdHelper.encode({ + type: 'libcore', + version: '1', + xpub, + walletName: wallet.getName(), + }), xpub, path: walletPath, name, @@ -288,7 +293,7 @@ function buildOperationRaw({ op, xpub, }: { - core: Object, + core: *, op: NJSOperation, xpub: string, }): OperationRaw { @@ -322,15 +327,15 @@ function buildOperationRaw({ } export async function getNJSAccount({ - account, + accountRaw, njsWalletPool, }: { - accountId: string, - njsWalletPool: any, + accountRaw: AccountRaw, + njsWalletPool: *, }) { - const decodedAccountId = accountIdHelper.decode(account.id) + const decodedAccountId = accountIdHelper.decode(accountRaw.id) const njsWallet = await njsWalletPool.getWallet(decodedAccountId.walletName) - const njsAccount = await njsWallet.getAccount(account.index) + const njsAccount = await njsWallet.getAccount(accountRaw.index) return njsAccount } @@ -339,9 +344,9 @@ export async function syncAccount({ core, njsWalletPool, }: { - core: Object, + core: *, rawAccount: AccountRaw, - njsWalletPool: Object, + njsWalletPool: *, }) { const decodedAccountId = accountIdHelper.decode(rawAccount.id) const njsWallet = await njsWalletPool.getWallet(decodedAccountId.walletName) From d4f2ebe96bc61b903d29b72fc6a5f07126dd4a2e Mon Sep 17 00:00:00 2001 From: meriadec Date: Sun, 3 Jun 2018 13:15:31 +0200 Subject: [PATCH 17/19] This react-router thing is becoming ridiculous --- src/components/SideBar/index.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/SideBar/index.js b/src/components/SideBar/index.js index cbc64845..227df258 100644 --- a/src/components/SideBar/index.js +++ b/src/components/SideBar/index.js @@ -4,6 +4,7 @@ import React, { PureComponent, Fragment } from 'react' import { compose } from 'redux' import { translate } from 'react-i18next' import styled from 'styled-components' +import { withRouter } from 'react-router' import { connect } from 'react-redux' import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react' import type { Account } from '@ledgerhq/live-common/lib/types' @@ -124,9 +125,17 @@ class SideBar extends PureComponent { } } -const AccountsList = connect(state => ({ - accounts: accountsSelector(state), -}))(({ accounts }: { accounts: Account[] }) => ( +const AccountsList = compose( + withRouter, + connect( + state => ({ + accounts: accountsSelector(state), + }), + null, + null, + { pure: false }, + ), +)(({ accounts }: { accounts: Account[] }) => ( {accounts.map(account => { const Icon = getCryptoCurrencyIcon(account.currency) @@ -155,6 +164,7 @@ const AccountsList = connect(state => ({ )) export default compose( + withRouter, connect(mapStateToProps, mapDispatchToProps, null, { pure: false }), translate(), )(SideBar) From c928cf626a4f61b097855e6d7462ef71fe230fcb Mon Sep 17 00:00:00 2001 From: meriadec Date: Sun, 3 Jun 2018 14:13:57 +0200 Subject: [PATCH 18/19] Sync fresh address --- src/bridge/LibcoreBridge.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bridge/LibcoreBridge.js b/src/bridge/LibcoreBridge.js index 789fbd1a..9e0ad063 100644 --- a/src/bridge/LibcoreBridge.js +++ b/src/bridge/LibcoreBridge.js @@ -63,6 +63,8 @@ const LibcoreBridge: WalletBridge = { const syncedAccount = decodeAccount(rawSyncedAccount) next(account => ({ ...account, + freshAddress: syncedAccount.freshAddress, + freshAddressPath: syncedAccount.freshAddressPath, balance: syncedAccount.balance, blockHeight: syncedAccount.blockHeight, operations: syncedAccount.operations, // TODO: is a simple replace enough? From e69fa025338e6865fd16cf95fd55157aa46bc4ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Sun, 3 Jun 2018 17:44:25 +0200 Subject: [PATCH 19/19] better flowtype on withLibcore --- src/helpers/withLibcore.js | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/helpers/withLibcore.js b/src/helpers/withLibcore.js index 7b053ea4..babacb3e 100644 --- a/src/helpers/withLibcore.js +++ b/src/helpers/withLibcore.js @@ -1,31 +1,30 @@ // @flow +import invariant from 'invariant' + const core = require('@ledgerhq/ledger-core') -let walletPool = null +let walletPoolInstance: ?Object = null let queue = Promise.resolve() // TODO: `core` and `NJSWalletPool` should be typed -type Job = (Object, Object) => any +type Job = (Object, Object) => Promise -export default function withLibcore(job: Job) { - if (!walletPool) { - walletPool = core.instanciateWalletPool({ +export default function withLibcore(job: Job): Promise { + if (!walletPoolInstance) { + walletPoolInstance = core.instanciateWalletPool({ // sqlite files will be located in the app local data folder dbPath: process.env.LEDGER_LIVE_SQLITE_PATH, }) } - // $FlowFixMe WTF is happening here, dudes. - queue = queue.then(async () => { - try { - if (!walletPool) { - throw new Error('wallet pool not instanciated. this should not happen') - } - return job(core, walletPool) - } catch (e) { - console.log(`withLibCore: Error in job`, e) // eslint-disable-line no-console - return Promise.resolve() - } + const walletPool = walletPoolInstance + invariant(walletPool, 'core.instanciateWalletPool returned null !!') + + const p = queue.then(() => job(core, walletPool)) + + queue = p.catch(e => { + console.warn(`withLibCore: Error in job`, e) }) - return queue + + return p }