Browse Source

Prevent libcore race conditions

master
meriadec 7 years ago
parent
commit
acdd982179
No known key found for this signature in database GPG Key ID: 1D2FC2305E2CB399
  1. 30
      src/commands/libcoreScanAccounts.js
  2. 116
      src/commands/libcoreSignAndBroadcast.js
  3. 16
      src/helpers/libcore.js
  4. 28
      src/helpers/withLibcore.js
  5. 20
      src/init-ledger-core.js

30
src/commands/libcoreScanAccounts.js

@ -4,6 +4,7 @@ import type { AccountRaw } from '@ledgerhq/live-common/lib/types'
import { createCommand, Command } from 'helpers/ipc' import { createCommand, Command } from 'helpers/ipc'
import { Observable } from 'rxjs' import { Observable } from 'rxjs'
import { scanAccountsOnDevice } from 'helpers/libcore' import { scanAccountsOnDevice } from 'helpers/libcore'
import withLibcore from 'helpers/withLibcore'
type Input = { type Input = {
devicePath: string, devicePath: string,
@ -17,19 +18,22 @@ const cmd: Command<Input, Result> = createCommand(
({ devicePath, currencyId }) => ({ devicePath, currencyId }) =>
Observable.create(o => { Observable.create(o => {
// TODO scanAccountsOnDevice should directly return a Observable so we just have to pass-in // TODO scanAccountsOnDevice should directly return a Observable so we just have to pass-in
scanAccountsOnDevice({ withLibcore(core =>
devicePath, scanAccountsOnDevice({
currencyId, core,
onAccountScanned: account => { devicePath,
o.next(account) currencyId,
}, onAccountScanned: account => {
}).then( o.next(account)
() => { },
o.complete() }).then(
}, () => {
e => { o.complete()
o.error(e) },
}, e => {
o.error(e)
},
),
) )
function unsubscribe() { function unsubscribe() {

116
src/commands/libcoreSignAndBroadcast.js

@ -2,11 +2,13 @@
import type { AccountRaw, OperationRaw } from '@ledgerhq/live-common/lib/types' import type { AccountRaw, OperationRaw } from '@ledgerhq/live-common/lib/types'
import Btc from '@ledgerhq/hw-app-btc' 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 { createCommand, Command } from 'helpers/ipc'
import { withDevice } from 'helpers/deviceAccess' import { withDevice } from 'helpers/deviceAccess'
import { getWalletIdentifier } from 'helpers/libcore' import { getWalletIdentifier } from 'helpers/libcore'
import { fromPromise } from 'rxjs/observable/fromPromise'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies'
type BitcoinLikeTransaction = { type BitcoinLikeTransaction = {
amount: number, amount: number,
@ -24,70 +26,68 @@ type Result = $Exact<OperationRaw>
const cmd: Command<Input, Result> = createCommand( const cmd: Command<Input, Result> = createCommand(
'libcoreSignAndBroadcast', 'libcoreSignAndBroadcast',
({ account, transaction, deviceId }) => { ({ account, transaction, deviceId }) =>
// TODO: investigate why importing it on file scope causes trouble fromPromise(
const core = require('init-ledger-core')() withDevice(deviceId)(transport =>
withLibcore(async core => {
return fromPromise( const hwApp = new Btc(transport)
withDevice(deviceId)(async transport => {
const hwApp = new Btc(transport)
const WALLET_IDENTIFIER = await getWalletIdentifier({ const WALLET_IDENTIFIER = await getWalletIdentifier({
hwApp, hwApp,
isSegwit: !!account.isSegwit, isSegwit: !!account.isSegwit,
currencyId: account.currencyId, currencyId: account.currencyId,
devicePath: deviceId, devicePath: deviceId,
}) })
const njsWallet = await core.getWallet(WALLET_IDENTIFIER) const njsWallet = await core.getWallet(WALLET_IDENTIFIER)
const njsAccount = await njsWallet.getAccount(account.index) const njsAccount = await njsWallet.getAccount(account.index)
const bitcoinLikeAccount = njsAccount.asBitcoinLikeAccount() const bitcoinLikeAccount = njsAccount.asBitcoinLikeAccount()
const njsWalletCurrency = njsWallet.getCurrency() const njsWalletCurrency = njsWallet.getCurrency()
const amount = core.createAmount(njsWalletCurrency, transaction.amount) const amount = core.createAmount(njsWalletCurrency, transaction.amount)
const fees = core.createAmount(njsWalletCurrency, transaction.feePerByte) const fees = core.createAmount(njsWalletCurrency, transaction.feePerByte)
const transactionBuilder = bitcoinLikeAccount.buildTransaction() 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) transactionBuilder.sendToAddress(amount, transaction.recipient)
// TODO: don't use hardcoded value for sequence (and first also maybe) // TODO: don't use hardcoded value for sequence (and first also maybe)
transactionBuilder.pickInputs(0, 0xffffff) transactionBuilder.pickInputs(0, 0xffffff)
transactionBuilder.setFeesPerByte(fees) transactionBuilder.setFeesPerByte(fees)
const builded = await transactionBuilder.build() const builded = await transactionBuilder.build()
const sigHashType = core.helpers.bytesToHex( const sigHashType = core.helpers.bytesToHex(
njsWalletCurrency.bitcoinLikeNetworkParameters.SigHash, njsWalletCurrency.bitcoinLikeNetworkParameters.SigHash,
) )
const currency = getCryptoCurrencyById(account.currencyId) const currency = getCryptoCurrencyById(account.currencyId)
const signedTransaction = await core.signTransaction({ const signedTransaction = await core.signTransaction({
hwApp, hwApp,
transaction: builded, transaction: builded,
sigHashType, sigHashType,
supportsSegwit: currency.supportsSegwit, supportsSegwit: currency.supportsSegwit,
isSegwit: account.isSegwit, isSegwit: account.isSegwit,
}) })
const txHash = await njsAccount const txHash = await njsAccount
.asBitcoinLikeAccount() .asBitcoinLikeAccount()
.broadcastRawTransaction(signedTransaction) .broadcastRawTransaction(signedTransaction)
// optimistic operation // optimistic operation
return { return {
id: txHash, id: txHash,
hash: txHash, hash: txHash,
type: 'OUT', type: 'OUT',
value: amount, value: amount,
blockHash: null, blockHash: null,
blockHeight: null, blockHeight: null,
senders: [account.freshAddress], senders: [account.freshAddress],
recipients: [transaction.recipient], recipients: [transaction.recipient],
accountId: account.id, accountId: account.id,
date: new Date().toISOString(), date: new Date().toISOString(),
} }
}), }),
) ),
}, ),
) )
export default cmd export default cmd

16
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' import type { NJSAccount, NJSOperation } from '@ledgerhq/ledger-core/src/ledgercore_doc'
type Props = { type Props = {
core: Object,
devicePath: string, devicePath: string,
currencyId: string, currencyId: string,
onAccountScanned: AccountRaw => void, onAccountScanned: AccountRaw => void,
@ -24,13 +25,14 @@ type Props = {
const { SHOW_LEGACY_NEW_ACCOUNT } = process.env const { SHOW_LEGACY_NEW_ACCOUNT } = process.env
export function scanAccountsOnDevice(props: Props): Promise<AccountRaw[]> { export function scanAccountsOnDevice(props: Props): Promise<AccountRaw[]> {
const { devicePath, currencyId, onAccountScanned } = props const { devicePath, currencyId, onAccountScanned, core } = props
const currency = getCryptoCurrencyById(currencyId) const currency = getCryptoCurrencyById(currencyId)
return withDevice(devicePath)(async transport => { return withDevice(devicePath)(async transport => {
const hwApp = new Btc(transport) const hwApp = new Btc(transport)
const commonParams = { const commonParams = {
core,
hwApp, hwApp,
currencyId, currencyId,
onAccountScanned, onAccountScanned,
@ -78,6 +80,7 @@ export async function getWalletIdentifier({
} }
async function scanAccountsOnDeviceBySegwit({ async function scanAccountsOnDeviceBySegwit({
core,
hwApp, hwApp,
currencyId, currencyId,
onAccountScanned, onAccountScanned,
@ -96,12 +99,13 @@ async function scanAccountsOnDeviceBySegwit({
const WALLET_IDENTIFIER = await getWalletIdentifier({ hwApp, isSegwit, currencyId, devicePath }) const WALLET_IDENTIFIER = await getWalletIdentifier({ hwApp, isSegwit, currencyId, devicePath })
// retrieve or create the wallet // 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() const accountsCount = await wallet.getAccountCount()
// recursively scan all accounts on device on the given app // recursively scan all accounts on device on the given app
// new accounts will be created in sqlite, existing ones will be updated // new accounts will be created in sqlite, existing ones will be updated
const accounts = await scanNextAccount({ const accounts = await scanNextAccount({
core,
wallet, wallet,
hwApp, hwApp,
currencyId, currencyId,
@ -119,6 +123,7 @@ async function scanAccountsOnDeviceBySegwit({
async function scanNextAccount(props: { async function scanNextAccount(props: {
// $FlowFixMe // $FlowFixMe
wallet: NJSWallet, wallet: NJSWallet,
core: Object,
hwApp: Object, hwApp: Object,
currencyId: string, currencyId: string,
accountsCount: number, accountsCount: number,
@ -129,6 +134,7 @@ async function scanNextAccount(props: {
showNewAccount: boolean, showNewAccount: boolean,
}): Promise<AccountRaw[]> { }): Promise<AccountRaw[]> {
const { const {
core,
wallet, wallet,
hwApp, hwApp,
currencyId, currencyId,
@ -140,9 +146,6 @@ async function scanNextAccount(props: {
showNewAccount, showNewAccount,
} = props } = 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 console.log(`>> Scanning account ${accountIndex} - isSegwit: ${isSegwit.toString()}`) // eslint-disable-line no-console
// create account only if account has not been scanned yet // create account only if account has not been scanned yet
@ -187,12 +190,11 @@ async function scanNextAccount(props: {
} }
async function getOrCreateWallet( async function getOrCreateWallet(
core: Object,
WALLET_IDENTIFIER: string, WALLET_IDENTIFIER: string,
currencyId: string, currencyId: string,
isSegwit: boolean, isSegwit: boolean,
): NJSWallet { ): NJSWallet {
// TODO: investigate why importing it on file scope causes trouble
const core = require('init-ledger-core')()
try { try {
const wallet = await core.getWallet(WALLET_IDENTIFIER) const wallet = await core.getWallet(WALLET_IDENTIFIER)
return wallet return wallet

28
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<any>
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
}

20
src/init-ledger-core.js

@ -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
}
Loading…
Cancel
Save