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"