diff --git a/package.json b/package.json index 49757fc7..45147cc7 100644 --- a/package.json +++ b/package.json @@ -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/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/helpers/deviceAccess.js b/src/helpers/deviceAccess.js new file mode 100644 index 00000000..68a74958 --- /dev/null +++ b/src/helpers/deviceAccess.js @@ -0,0 +1,51 @@ +// @flow +import createSemaphore from 'semaphore' +import type Transport from '@ledgerhq/hw-transport' +import CommNodeHid from '@ledgerhq/hw-transport-node-hid' + +// all open to device must use openDevice so we can prevent race conditions +// and guarantee we do one device access at a time. It also will handle the .close() +// NOTE optim: in the future we can debounce the close & reuse the same transport instance. + +type WithDevice = (devicePath: string) => (job: (Transport<*>) => Promise) => Promise + +const semaphorePerDevice = {} + +export const withDevice: WithDevice = devicePath => { + const { FORK_TYPE } = process.env + if (FORK_TYPE !== 'devices') { + console.warn( + `deviceAccess is only expected to be used in process 'devices'. Any other usage may lead to race conditions. (Got: '${FORK_TYPE}')`, + ) + } + const sem = + semaphorePerDevice[devicePath] || (semaphorePerDevice[devicePath] = createSemaphore(1)) + return job => + takeSemaphorePromise(sem, async () => { + const t = await CommNodeHid.open(devicePath) + try { + const res = await job(t) + // $FlowFixMe + return res + } finally { + t.close() + } + }) +} + +function takeSemaphorePromise(sem, f: () => Promise): Promise { + return new Promise((resolve, reject) => { + sem.take(() => { + f().then( + r => { + sem.leave() + resolve(r) + }, + e => { + sem.leave() + reject(e) + }, + ) + }) + }) +} diff --git a/src/internals/accounts/scanAccountsOnDevice.js b/src/internals/accounts/scanAccountsOnDevice.js index 029f2f2a..10f8f2bb 100644 --- a/src/internals/accounts/scanAccountsOnDevice.js +++ b/src/internals/accounts/scanAccountsOnDevice.js @@ -9,11 +9,9 @@ // import Btc from '@ledgerhq/hw-app-btc' -import CommNodeHid from '@ledgerhq/hw-transport-node-hid' +import { withDevice } from 'helpers/deviceAccess' import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' -import type Transport from '@ledgerhq/hw-transport' - import type { AccountRaw, OperationRaw, OperationType } from '@ledgerhq/live-common/lib/types' import type { NJSAccount, NJSOperation } from '@ledgerhq/ledger-core/src/ledgercore_doc' @@ -23,27 +21,30 @@ type Props = { onAccountScanned: Function, } -export default async function scanAccountsOnDevice(props: Props): Promise { +export default function scanAccountsOnDevice(props: Props): Promise { const { devicePath, currencyId, onAccountScanned } = props - // instanciate app on device - const transport: Transport<*> = await CommNodeHid.open(devicePath) - const hwApp = new Btc(transport) + return withDevice(devicePath)(async transport => { + const hwApp = new Btc(transport) - const commonParams = { - hwApp, - currencyId, - onAccountScanned, - devicePath, - } + const commonParams = { + hwApp, + currencyId, + onAccountScanned, + devicePath, + } - // scan segwit AND non-segwit accounts - const segwitAccounts = await scanAccountsOnDeviceBySegwit({ ...commonParams, isSegwit: true }) - const nonSegwitAccounts = await scanAccountsOnDeviceBySegwit({ ...commonParams, isSegwit: false }) + // scan segwit AND non-segwit accounts + const segwitAccounts = await scanAccountsOnDeviceBySegwit({ ...commonParams, isSegwit: true }) + const nonSegwitAccounts = await scanAccountsOnDeviceBySegwit({ + ...commonParams, + isSegwit: false, + }) - const accounts = [...segwitAccounts, ...nonSegwitAccounts] + const accounts = [...segwitAccounts, ...nonSegwitAccounts] - return accounts + return accounts + }) } export async function getWalletIdentifier({ diff --git a/src/internals/accounts/signAndBroadcastTransaction/btc.js b/src/internals/accounts/signAndBroadcastTransaction/btc.js index ae7e3b4f..f0e88a49 100644 --- a/src/internals/accounts/signAndBroadcastTransaction/btc.js +++ b/src/internals/accounts/signAndBroadcastTransaction/btc.js @@ -1,11 +1,9 @@ // @flow import Btc from '@ledgerhq/hw-app-btc' -import CommNodeHid from '@ledgerhq/hw-transport-node-hid' +import { withDevice } from 'helpers/deviceAccess' import type { AccountRaw } from '@ledgerhq/live-common/lib/types' -import type Transport from '@ledgerhq/hw-transport' - import type { IPCSend } from 'types/electron' import { getWalletIdentifier } from '../scanAccountsOnDevice' @@ -31,38 +29,40 @@ export default async function signAndBroadcastTransactionBTCLike( // TODO: investigate why importing it on file scope causes trouble const core = require('init-ledger-core')() - // instanciate app on device - const transport: Transport<*> = await CommNodeHid.open(deviceId) - const hwApp = new Btc(transport) + const txHash = await withDevice(deviceId)(async transport => { + 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 signedTransaction = await core.signTransaction(hwApp, builded) + const builded = await transactionBuilder.build() + const signedTransaction = await core.signTransaction(hwApp, builded) - const txHash = await njsAccount - .asBitcoinLikeAccount() - .broadcastRawTransaction(signedTransaction) + const txHash = await njsAccount + .asBitcoinLikeAccount() + .broadcastRawTransaction(signedTransaction) + + return txHash + }) send('accounts.signAndBroadcastTransactionBTCLike.success', txHash) } catch (err) { diff --git a/src/internals/manager/helpers.js b/src/internals/manager/helpers.js index 0b80262f..fbc42dd5 100644 --- a/src/internals/manager/helpers.js +++ b/src/internals/manager/helpers.js @@ -1,6 +1,6 @@ // @flow -import CommNodeHid from '@ledgerhq/hw-transport-node-hid' +import { withDevice } from 'helpers/deviceAccess' import chalk from 'chalk' import Websocket from 'ws' import qs from 'qs' @@ -44,6 +44,7 @@ export function createTransportHandler( errorResponse: string, }, ) { + console.log('DEPRECATED: createTransportHandler use withDevice and commands/*') return async function transportHandler({ devicePath, ...params @@ -51,9 +52,7 @@ export function createTransportHandler( devicePath: string, }): Promise { try { - const transport: Transport<*> = await CommNodeHid.open(devicePath) - // $FlowFixMe - const data = await action(transport, params) + const data = await withDevice(devicePath)(transport => action(transport, params)) send(successResponse, data) } catch (err) { if (!err) { diff --git a/yarn.lock b/yarn.lock index cd03a539..00d355a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12203,6 +12203,10 @@ selfsigned@^1.9.1: dependencies: node-forge "0.7.5" +semaphore@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/semaphore/-/semaphore-1.1.0.tgz#aaad8b86b20fe8e9b32b16dc2ee682a8cd26a8aa" + semver-diff@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"