From 05f6850e73364f1b205504d6c562e363fdf9d71c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Tue, 22 May 2018 08:53:39 +0200 Subject: [PATCH] add "Command" for more robust type checked com over ipc --- package.json | 3 + src/bridge/EthereumJSBridge.js | 189 ++++++++++------------- src/components/DeviceCheckAddress.js | 55 ++++--- src/components/EnsureDeviceApp/index.js | 46 ++---- src/components/ReceiveBox.js | 184 ---------------------- src/helpers/ipc.js | 118 ++++++++++++++ src/internals/accounts/helpers.js | 39 ----- src/internals/accounts/index.js | 34 ---- src/internals/devices/ensureDeviceApp.js | 36 ----- src/internals/devices/getAddress.js | 53 +++---- src/internals/devices/index.js | 15 +- src/internals/devices/signTransaction.js | 48 +++--- src/internals/index.js | 25 ++- yarn.lock | 42 ++--- 14 files changed, 338 insertions(+), 549 deletions(-) delete mode 100644 src/components/ReceiveBox.js create mode 100644 src/helpers/ipc.js delete mode 100644 src/internals/accounts/helpers.js delete mode 100644 src/internals/devices/ensureDeviceApp.js diff --git a/package.json b/package.json index 97ec51cd..b6ec1bed 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,8 @@ "redux-actions": "^2.3.0", "redux-thunk": "^2.2.0", "reselect": "^3.0.1", + "rxjs": "^6.2.0", + "rxjs-compat": "^6.2.0", "smooth-scrollbar": "^8.2.7", "source-map": "0.7.2", "source-map-support": "^0.5.4", @@ -89,6 +91,7 @@ "styled-system": "^2.2.1", "tippy.js": "^2.5.2", "uncontrollable": "^6.0.0", + "uuid": "^3.2.1", "ws": "^5.1.1", "zxcvbn": "^4.4.2" }, diff --git a/src/bridge/EthereumJSBridge.js b/src/bridge/EthereumJSBridge.js index 67b1a97d..ae971ecd 100644 --- a/src/bridge/EthereumJSBridge.js +++ b/src/bridge/EthereumJSBridge.js @@ -1,12 +1,12 @@ // @flow import React from 'react' -import { ipcRenderer } from 'electron' -import { sendEvent } from 'renderer/events' import EthereumKind from 'components/FeesField/EthereumKind' import type { Account, Operation } from '@ledgerhq/live-common/lib/types' import { apiForCurrency } from 'api/Ethereum' import type { Tx } from 'api/Ethereum' import { makeBip44Path } from 'helpers/bip32path' +import getAddressCommand from 'internals/devices/getAddress' +import signTransactionCommand from 'internals/devices/signTransaction' import type { EditProps, WalletBridge } from './types' // TODO in future it would be neat to support eip55 @@ -62,67 +62,19 @@ const paginateMoreTransactions = async ( return mergeOps(acc, txs.map(toAccountOperation(account))) } -function signTransactionOnDevice( - a: Account, - t: Transaction, - deviceId: string, - nonce: string, -): Promise { - const transaction = { ...t, nonce } - return new Promise((resolve, reject) => { - const unbind = () => { - ipcRenderer.removeListener('msg', handleMsgEvent) - } - - function handleMsgEvent(e, { data, type }) { - if (type === 'devices.signTransaction.success') { - unbind() - resolve(data) - } else if (type === 'devices.signTransaction.fail') { - unbind() - reject(new Error('failed to get address')) - } - } - - ipcRenderer.on('msg', handleMsgEvent) - - sendEvent('devices', 'signTransaction', { - currencyId: a.currency.id, - devicePath: deviceId, - path: a.path, - transaction, - }) - }) -} - const EthereumBridge: WalletBridge = { scanAccountsOnDevice(currency, deviceId, { next, complete, error }) { let finished = false - const unbind = () => { + const unsubscribe = () => { finished = true - ipcRenderer.removeListener('msg', handleMsgEvent) } const api = apiForCurrency(currency) - // FIXME: THIS IS SPAghetti, we need to move to a more robust approach to get an observable with a sendEvent // in future ideally what we want is: // return mergeMap(addressesObservable, address => fetchAccount(address)) - let index = 0 let balanceZerosCount = 0 - function pollNextAddress() { - sendEvent('devices', 'getAddress', { - currencyId: currency.id, - devicePath: deviceId, - path: makeBip44Path({ - currency, - x: index, - }), - }) - index++ - } - let currentBlockPromise function lazyCurrentBlock() { if (!currentBlockPromise) { @@ -131,52 +83,23 @@ const EthereumBridge: WalletBridge = { return currentBlockPromise } - async function stepAddress({ address, path }) { - try { - const balance = await api.getAccountBalance(address) - if (finished) return - if (balance === 0) { - if (balanceZerosCount === 0) { - // first zero account will emit one account as opportunity to create a new account.. - const currentBlock = await lazyCurrentBlock() - const accountId = `${currency.id}_${address}` - const account: Account = { - id: accountId, - xpub: '', - path, - walletPath: String(index), - name: 'New Account', - isSegwit: false, - address, - addresses: [address], - balance, - blockHeight: currentBlock.height, - archived: true, - index, - currency, - operations: [], - unit: currency.units[0], - lastSyncDate: new Date(), - } - next(account) - } - balanceZerosCount++ - // NB we currently stop earlier. in future we shouldn't stop here, just continue & user will stop at the end! - // NB (what's the max tho?) - unbind() - complete() - } else { + async function stepAddress( + index, + { address, path }, + ): { account?: Account, complete?: boolean } { + const balance = await api.getAccountBalance(address) + if (finished) return {} + if (balance === 0) { + if (balanceZerosCount === 0) { + // first zero account will emit one account as opportunity to create a new account.. const currentBlock = await lazyCurrentBlock() - if (finished) return - const { txs } = await api.getTransactions(address) - if (finished) return const accountId = `${currency.id}_${address}` const account: Account = { id: accountId, xpub: '', path, walletPath: String(index), - name: address.slice(32), + name: 'New Account', isSegwit: false, address, addresses: [address], @@ -189,32 +112,69 @@ const EthereumBridge: WalletBridge = { unit: currency.units[0], lastSyncDate: new Date(), } - account.operations = txs.map(toAccountOperation(account)) - next(account) - pollNextAddress() + // NB we currently stop earlier. in future we shouldn't stop here, just continue & user will stop at the end! + // NB (what's the max tho?) + return { account, complete: true } } - } catch (e) { - error(e) + balanceZerosCount++ + return { complete: true } } - } - function handleMsgEvent(e, { data, type }) { - if (type === 'devices.getAddress.success') { - stepAddress(data) - } else if (type === 'devices.getAddress.fail') { - error(new Error(data.message)) + const currentBlock = await lazyCurrentBlock() + if (finished) return {} + const { txs } = await api.getTransactions(address) + if (finished) return {} + const accountId = `${currency.id}_${address}` + const account: Account = { + id: accountId, + xpub: '', + path, + walletPath: String(index), + name: address.slice(32), + isSegwit: false, + address, + addresses: [address], + balance, + blockHeight: currentBlock.height, + archived: true, + index, + currency, + operations: [], + unit: currency.units[0], + lastSyncDate: new Date(), } + account.operations = txs.map(toAccountOperation(account)) + return { account } } - ipcRenderer.on('msg', handleMsgEvent) + async function main() { + try { + for (let index = 0; index < 255; index++) { + const res = await getAddressCommand + .send({ + currencyId: currency.id, + devicePath: deviceId, + path: makeBip44Path({ + currency, + x: index, + }), + }) + .toPromise() + const r = await stepAddress(index, res) + if (r.account) next(r.account) + if (r.complete) { + complete() + break + } + } + } catch (e) { + error(e) + } + } - pollNextAddress() + main() - return { - unsubscribe() { - unbind() - }, - } + return { unsubscribe } }, synchronize({ address, blockHeight, currency }, { next, complete, error }) { @@ -297,9 +257,20 @@ const EthereumBridge: WalletBridge = { signAndBroadcast: async (a, t, deviceId) => { const api = apiForCurrency(a.currency) + const nonce = await api.getAccountNonce(a.address) - const transaction = await signTransactionOnDevice(a, t, deviceId, nonce) + + const transaction = await signTransactionCommand + .send({ + currencyId: a.currency.id, + devicePath: deviceId, + path: a.path, + transaction: { ...t, nonce }, + }) + .toPromise() + const result = await api.broadcastTransaction(transaction) + return result }, } diff --git a/src/components/DeviceCheckAddress.js b/src/components/DeviceCheckAddress.js index 485972d4..ce2353f6 100644 --- a/src/components/DeviceCheckAddress.js +++ b/src/components/DeviceCheckAddress.js @@ -1,16 +1,14 @@ // @flow import { PureComponent } from 'react' -import { ipcRenderer } from 'electron' - import type { Account } from '@ledgerhq/live-common/lib/types' import type { Device } from 'types/common' -import { sendEvent } from 'renderer/events' +import getAddress from 'internals/devices/getAddress' type Props = { - onCheck: Function, - render: Function, + onCheck: boolean => void, + render: ({ isVerified?: ?boolean }) => *, account: Account, device: Device, } @@ -26,39 +24,40 @@ class CheckAddress extends PureComponent { componentDidMount() { const { device, account } = this.props - ipcRenderer.on('msg', this.handleMsgEvent) this.verifyAddress({ device, account }) } - componentWillUnmount() { - ipcRenderer.removeListener('msg', this.handleMsgEvent) + componentDidUnmount() { + if (this.sub) this.sub.unsubscribe() } - handleMsgEvent = (e: any, { type }: { type: string }) => { - const { onCheck } = this.props + sub: * - if (type === 'accounts.verifyAddress.success') { - this.setState({ - isVerified: true, + verifyAddress = ({ device, account }: { device: Device, account: Account }) => { + this.sub = getAddress + .send({ + currencyId: account.currency.id, + devicePath: device.path, + path: account.path, + segwit: account.isSegwit, + verify: true, }) - onCheck(true) - } - - if (type === 'accounts.verifyAddress.fail') { - this.setState({ - isVerified: false, + .subscribe({ + next: () => { + this.setState({ + isVerified: true, + }) + this.props.onCheck(true) + }, + error: () => { + this.setState({ + isVerified: false, + }) + this.props.onCheck(false) + }, }) - onCheck(false) - } } - verifyAddress = ({ device, account }: { device: Device, account: Account }) => - sendEvent('accounts', 'verifyAddress', { - pathDevice: device.path, - path: account.path, - segwit: account.path.startsWith("49'"), // TODO: store segwit info in account - }) - render() { const { render } = this.props const { isVerified } = this.state diff --git a/src/components/EnsureDeviceApp/index.js b/src/components/EnsureDeviceApp/index.js index 47d4fb46..944456a1 100644 --- a/src/components/EnsureDeviceApp/index.js +++ b/src/components/EnsureDeviceApp/index.js @@ -2,15 +2,14 @@ import invariant from 'invariant' import { PureComponent } from 'react' import { connect } from 'react-redux' -import { ipcRenderer } from 'electron' import { makeBip44Path } from 'helpers/bip32path' import type { Account, CryptoCurrency } from '@ledgerhq/live-common/lib/types' import type { Device } from 'types/common' -import { sendEvent } from 'renderer/events' import { getDevices } from 'reducers/devices' import type { State as StoreState } from 'reducers/index' +import getAddress from 'internals/devices/getAddress' type OwnProps = { currency: ?CryptoCurrency, @@ -54,7 +53,6 @@ class EnsureDeviceApp extends PureComponent { } componentDidMount() { - ipcRenderer.on('msg', this.handleMsgEvent) if (this.props.deviceSelected !== null) { this.checkAppOpened() } @@ -88,21 +86,21 @@ class EnsureDeviceApp extends PureComponent { } componentWillUnmount() { - ipcRenderer.removeListener('msg', this.handleMsgEvent) clearTimeout(this._timeout) } - checkAppOpened = () => { + checkAppOpened = async () => { const { deviceSelected, account, currency } = this.props if (!deviceSelected) { return } - let options = null + let options if (account) { options = { + devicePath: deviceSelected.path, currencyId: account.currency.id, path: account.path, accountAddress: account.address, @@ -110,6 +108,7 @@ class EnsureDeviceApp extends PureComponent { } } else if (currency) { options = { + devicePath: deviceSelected.path, currencyId: currency.id, path: makeBip44Path({ currency }), } @@ -117,11 +116,17 @@ class EnsureDeviceApp extends PureComponent { throw new Error('either currency or account is required') } - // TODO just use getAddress! - sendEvent('devices', 'ensureDeviceApp', { - devicePath: deviceSelected.path, - ...options, - }) + try { + const { address } = await getAddress.send(options).toPromise() + if (account && account.address !== address) { + throw new Error('Account address is different than device address') + } + this.handleStatusChange(this.state.deviceStatus, 'success') + } catch (e) { + this.handleStatusChange(this.state.deviceStatus, 'fail', e.message) + } + + this._timeout = setTimeout(this.checkAppOpened, 1e3) } _timeout: * @@ -133,25 +138,6 @@ class EnsureDeviceApp extends PureComponent { onStatusChange && onStatusChange(deviceStatus, appStatus, errorMessage) } - handleMsgEvent = (e, { type, data }) => { - const { deviceStatus } = this.state - const { deviceSelected } = this.props - - if (!deviceSelected) { - return - } - - if (type === 'devices.ensureDeviceApp.success' && deviceSelected.path === data.devicePath) { - this.handleStatusChange(deviceStatus, 'success') - this._timeout = setTimeout(this.checkAppOpened, 1e3) - } - - if (type === 'devices.ensureDeviceApp.fail' && deviceSelected.path === data.devicePath) { - this.handleStatusChange(deviceStatus, 'fail', data.message) - this._timeout = setTimeout(this.checkAppOpened, 1e3) - } - } - render() { const { currency, account, devices, deviceSelected, render } = this.props const { appStatus, deviceStatus, errorMessage } = this.state diff --git a/src/components/ReceiveBox.js b/src/components/ReceiveBox.js deleted file mode 100644 index b9f9656a..00000000 --- a/src/components/ReceiveBox.js +++ /dev/null @@ -1,184 +0,0 @@ -// @flow - -import React, { PureComponent } from 'react' -import { connect } from 'react-redux' -import styled from 'styled-components' -import { ipcRenderer } from 'electron' -import type { Account } from '@ledgerhq/live-common/lib/types' - -import type { Device } from 'types/common' - -import { getCurrentDevice } from 'reducers/devices' -import { sendEvent } from 'renderer/events' - -import Box from 'components/base/Box' -import Button from 'components/base/Button' -import CopyToClipboard from 'components/base/CopyToClipboard' -import Print from 'components/base/Print' -import QRCode from 'components/base/QRCode' -import Text from 'components/base/Text' - -export const AddressBox = styled(Box).attrs({ - bg: 'lightGrey', - p: 2, -})` - border-radius: ${p => p.theme.radii[1]}px; - border: 1px solid ${p => p.theme.colors.fog}; - cursor: text; - text-align: center; - user-select: text; - word-break: break-all; -` - -const Action = styled(Box).attrs({ - alignItems: 'center', - color: 'fog', - flex: 1, - flow: 1, - fontSize: 0, -})` - font-weight: bold; - text-align: center; - cursor: pointer; - text-transform: uppercase; - - &:hover { - color: ${p => p.theme.colors.grey}; - } -` - -const mapStateToProps = state => ({ - currentDevice: getCurrentDevice(state), -}) - -type Props = { - currentDevice: Device | null, - account: Account, - amount?: string, -} - -type State = { - isVerified: null | boolean, - isDisplay: boolean, -} - -const defaultState = { - isVerified: null, - isDisplay: false, -} - -class ReceiveBox extends PureComponent { - static defaultProps = { - amount: undefined, - } - - state = { - ...defaultState, - } - - componentDidMount() { - ipcRenderer.on('msg', this.handleMsgEvent) - } - - componentWillReceiveProps(nextProps: Props) { - if (this.props.account !== nextProps.account) { - this.setState({ - ...defaultState, - }) - } - } - - componentWillUnmount() { - ipcRenderer.removeListener('msg', this.handleMsgEvent) - this.setState({ - ...defaultState, - }) - } - - handleMsgEvent = (e, { type }) => { - if (type === 'wallet.verifyAddress.success') { - this.setState({ - isVerified: true, - }) - } - - if (type === 'wallet.verifyAddress.fail') { - this.setState({ - isVerified: false, - }) - } - } - - handleVerifyAddress = () => { - const { currentDevice, account } = this.props - - if (currentDevice !== null) { - sendEvent('usb', 'wallet.verifyAddress', { - pathDevice: currentDevice.path, - path: `${account.walletPath}${account.path}`, - }) - - this.setState({ - isDisplay: true, - }) - } - } - - render() { - const { amount, account } = this.props - const { isVerified, isDisplay } = this.state - - if (!isDisplay) { - return ( - - - - ) - } - - const { address } = account - - return ( - - - isVerified:{' '} - {isVerified === null - ? 'not yet...' - : isVerified === true - ? 'ok!' - : '/!\\ contact support'} - - - - - - {'Current address'} - {address} - - - ( - - {'Copy'} - - )} - /> - ( - - {isLoading ? '...' : 'Print'} - - )} - /> - - {'Share'} - - - - ) - } -} - -export default connect(mapStateToProps, null)(ReceiveBox) diff --git a/src/helpers/ipc.js b/src/helpers/ipc.js new file mode 100644 index 00000000..c9a6a344 --- /dev/null +++ b/src/helpers/ipc.js @@ -0,0 +1,118 @@ +// @flow +import { ipcRenderer } from 'electron' +import { Observable } from 'rxjs' +import uuidv4 from 'uuid/v4' + +type Msg = { + type: string, + data?: A, + options?: *, +} + +function send(msg: Msg) { + process.send(msg) +} + +export class Command { + channel: string + type: string + id: string + impl: In => Observable + constructor(channel: string, type: string, impl: In => Observable) { + this.channel = channel + this.type = type + this.id = `${channel}.${type}` + this.impl = impl + } + + // ~~~ On exec side we can: + + exec(data: In, requestId: string) { + return this.impl(data).subscribe({ + next: (data: A) => { + send({ + type: `NEXT_${requestId}`, + data, + }) + }, + complete: () => { + send({ + type: `COMPLETE_${requestId}`, + options: { kill: true }, + }) + }, + error: error => { + send({ + type: `ERROR_${requestId}`, + data: { + name: error && error.name, + message: error && error.message, + }, + options: { kill: true }, + }) + }, + }) + } + + // ~~~ On renderer side we can: + + /** + * Usage example: + * sub = send(data).subscribe({ next: ... }) + * // or + * const res = await send(data).toPromise() + */ + send(data: In): Observable { + return Observable.create(o => { + const { channel, type, id } = this + const requestId: string = uuidv4() + + const unsubscribe = () => { + ipcRenderer.removeListener('msg', handleMsgEvent) + } + + function handleMsgEvent(e, msg: Msg) { + switch (msg.type) { + case `NEXT_${requestId}`: + if (msg.data) { + o.next(msg.data) + } + break + + case `COMPLETE_${requestId}`: + o.complete() + unsubscribe() + break + + case `ERROR_${requestId}`: + o.error(msg.data) + unsubscribe() + break + + default: + } + } + + ipcRenderer.on('msg', handleMsgEvent) + + ipcRenderer.send(channel, { + type, + data: { + id, + data, + requestId, + }, + }) + + return unsubscribe + }) + } +} + +export function createCommand( + channel: string, + type: string, + impl: In => Observable, +): Command { + return new Command(channel, type, impl) +} diff --git a/src/internals/accounts/helpers.js b/src/internals/accounts/helpers.js deleted file mode 100644 index 9a156769..00000000 --- a/src/internals/accounts/helpers.js +++ /dev/null @@ -1,39 +0,0 @@ -// @flow - -/* eslint-disable no-bitwise */ - -import Btc from '@ledgerhq/hw-app-btc' - -export async function getFreshReceiveAddress({ - currencyId, - accountIndex, -}: { - currencyId: string, - accountIndex: number, -}) { - // TODO: investigate why importing it on file scope causes trouble - const core = require('init-ledger-core')() - - const wallet = await core.getWallet(currencyId) - const account = await wallet.getAccount(accountIndex) - const addresses = await account.getFreshPublicAddresses() - if (!addresses.length) { - throw new Error('No fresh addresses') - } - return addresses[0] -} - -export function verifyAddress({ - transport, - path, - segwit = true, -}: { - transport: Object, - path: string, - segwit?: boolean, -}) { - console.warn('DEPRECATED use devices.getAddress with verify option') - const btc = new Btc(transport) - - return btc.getWalletPublicKey(path, true, segwit) -} diff --git a/src/internals/accounts/index.js b/src/internals/accounts/index.js index bf3d205c..18c02cfe 100644 --- a/src/internals/accounts/index.js +++ b/src/internals/accounts/index.js @@ -1,11 +1,8 @@ // @flow -import CommNodeHid from '@ledgerhq/hw-transport-node-hid' - import type { IPCSend } from 'types/electron' import scanAccountsOnDevice from './scanAccountsOnDevice' -import { verifyAddress, getFreshReceiveAddress } from './helpers' import sync from './sync' @@ -36,37 +33,6 @@ export default { send('accounts.scanAccountsOnDevice.fail', formatErr(err)) } }, - - getFreshReceiveAddress: async ( - send: IPCSend, - { - currencyId, - accountIndex, - }: { - currencyId: string, - accountIndex: number, - }, - ) => { - try { - const freshAddress = await getFreshReceiveAddress({ currencyId, accountIndex }) - send('accounts.getFreshReceiveAddress.success', freshAddress) - } catch (err) { - send('accounts.getFreshReceiveAddress.fail', err) - } - }, - - verifyAddress: async ( - send: IPCSend, - { pathDevice, path }: { pathDevice: string, path: string }, - ) => { - const transport = await CommNodeHid.open(pathDevice) - try { - await verifyAddress({ transport, path }) - send('accounts.verifyAddress.success') - } catch (err) { - send('accounts.verifyAddress.fail') - } - }, } // TODO: move this to a helper diff --git a/src/internals/devices/ensureDeviceApp.js b/src/internals/devices/ensureDeviceApp.js deleted file mode 100644 index 22a3c9e3..00000000 --- a/src/internals/devices/ensureDeviceApp.js +++ /dev/null @@ -1,36 +0,0 @@ -// @flow - -import invariant from 'invariant' -import CommNodeHid from '@ledgerhq/hw-transport-node-hid' -import type Transport from '@ledgerhq/hw-transport' -import type { IPCSend } from 'types/electron' -import getAddressForCurrency from './getAddressForCurrency' - -export default async ( - send: IPCSend, - { - currencyId, - devicePath, - path, - accountAddress, - ...options - }: { - currencyId: string, - devicePath: string, - path: string, - accountAddress: ?string, - }, -) => { - try { - invariant(currencyId, 'currencyId "%s" not defined', currencyId) - const transport: Transport<*> = await CommNodeHid.open(devicePath) - const resolver = getAddressForCurrency(currencyId) - const { address } = await resolver(transport, currencyId, path, options) - if (accountAddress && address !== accountAddress) { - throw new Error('Account address is different than device address') - } - send('devices.ensureDeviceApp.success', { devicePath }) - } catch (err) { - send('devices.ensureDeviceApp.fail', { devicePath, message: err.message }) - } -} diff --git a/src/internals/devices/getAddress.js b/src/internals/devices/getAddress.js index c8e54fde..8ef7a798 100644 --- a/src/internals/devices/getAddress.js +++ b/src/internals/devices/getAddress.js @@ -1,32 +1,33 @@ // @flow -import invariant from 'invariant' +import { createCommand, Command } from 'helpers/ipc' +import { fromPromise } from 'rxjs/observable/fromPromise' import CommNodeHid from '@ledgerhq/hw-transport-node-hid' -import type Transport from '@ledgerhq/hw-transport' -import type { IPCSend } from 'types/electron' import getAddressForCurrency from './getAddressForCurrency' -export default async ( - send: IPCSend, - { - currencyId, - devicePath, - path, - ...options - }: { - currencyId: string, - devicePath: string, - path: string, - verify?: boolean, - }, -) => { - try { - invariant(currencyId, 'currencyId "%s" not defined', currencyId) - const transport: Transport<*> = await CommNodeHid.open(devicePath) - const resolver = getAddressForCurrency(currencyId) - const res = await resolver(transport, currencyId, path, options) - send('devices.getAddress.success', res) - } catch (err) { - send('devices.getAddress.fail', { message: err.message }) - } +type Input = { + currencyId: string, + devicePath: string, + path: string, + verify?: boolean, + segwit?: boolean, } + +type Result = { + address: string, + path: string, + publicKey: string, +} + +const cmd: Command = createCommand( + 'devices', + 'getAddress', + ({ currencyId, devicePath, path, ...options }) => + fromPromise( + CommNodeHid.open(devicePath).then(transport => + getAddressForCurrency(currencyId)(transport, currencyId, path, options), + ), + ), +) + +export default cmd diff --git a/src/internals/devices/index.js b/src/internals/devices/index.js index 2516aafa..f1779b0d 100644 --- a/src/internals/devices/index.js +++ b/src/internals/devices/index.js @@ -1,4 +1,11 @@ -export listen from './listen' -export ensureDeviceApp from './ensureDeviceApp' -export getAddress from './getAddress' -export signTransaction from './signTransaction' +// @flow +import type { Command } from 'helpers/ipc' + +import getAddress from './getAddress' +import listen from './listen' +import signTransaction from './signTransaction' + +// TODO port these to commands +export { listen } + +export const commands: Array> = [getAddress, signTransaction] diff --git a/src/internals/devices/signTransaction.js b/src/internals/devices/signTransaction.js index f40c4334..17139067 100644 --- a/src/internals/devices/signTransaction.js +++ b/src/internals/devices/signTransaction.js @@ -1,32 +1,28 @@ // @flow -import invariant from 'invariant' +import { createCommand, Command } from 'helpers/ipc' +import { fromPromise } from 'rxjs/observable/fromPromise' import CommNodeHid from '@ledgerhq/hw-transport-node-hid' -import type Transport from '@ledgerhq/hw-transport' -import type { IPCSend } from 'types/electron' import signTransactionForCurrency from './signTransactionForCurrency' -export default async ( - send: IPCSend, - { - currencyId, - devicePath, - path, - transaction, - }: { - currencyId: string, - devicePath: string, - path: string, - transaction: *, - }, -) => { - try { - invariant(currencyId, 'currencyId "%s" not defined', currencyId) - const transport: Transport<*> = await CommNodeHid.open(devicePath) - const signer = signTransactionForCurrency(currencyId) - const res = await signer(transport, currencyId, path, transaction) - send('devices.signTransaction.success', res) - } catch (err) { - send('devices.signTransaction.fail') - } +type Input = { + currencyId: string, + devicePath: string, + path: string, + transaction: *, } + +type Result = string + +const cmd: Command = createCommand( + 'devices', + 'signTransaction', + ({ currencyId, devicePath, path, transaction }) => + fromPromise( + CommNodeHid.open(devicePath).then(transport => + signTransactionForCurrency(currencyId)(transport, currencyId, path, transaction), + ), + ), +) + +export default cmd diff --git a/src/internals/index.js b/src/internals/index.js index d7ea0a4a..88a8ca08 100644 --- a/src/internals/index.js +++ b/src/internals/index.js @@ -24,13 +24,26 @@ if (handlers.default) { } process.on('message', payload => { - const { type, data } = payload - const handler = objectPath.get(handlers, type) - if (!handler) { - console.warn(`No handler found for ${type}`) - return + console.log(payload) + if (payload.data && payload.data.requestId) { + const { data, requestId, id } = payload.data + // this is the new type of "command" payload! + const cmd = (handlers.commands || []).find(cmd => cmd.id === id) + if (!cmd) { + console.warn(`command ${id} not found`) + } else { + cmd.exec(data, requestId) + } + } else { + // this will be deprecated! + const { type, data } = payload + const handler = objectPath.get(handlers, type) + if (!handler) { + console.warn(`No handler found for ${type}`) + return + } + handler(sendEvent, data) } - handler(sendEvent, data) }) if (__DEV__ || DEV_TOOLS) { diff --git a/yarn.lock b/yarn.lock index eb6c7058..cc07b02a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5072,7 +5072,7 @@ debug@3.1.0, debug@^3.0.0, debug@^3.1.0: dependencies: ms "2.0.0" -debuglog@*, debuglog@^1.0.1: +debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -7494,7 +7494,7 @@ import-local@^1.0.0: pkg-dir "^2.0.0" resolve-cwd "^2.0.0" -imurmurhash@*, imurmurhash@^0.1.4: +imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -8808,10 +8808,6 @@ lodash-es@^4.17.4, lodash-es@^4.17.5, lodash-es@^4.2.1: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05" -lodash._baseindexof@*: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" - lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -8819,25 +8815,11 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@*: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - -lodash._cacheindexof@*: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" - -lodash._createcache@*: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" - dependencies: - lodash._getnative "^3.0.0" - lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" -lodash._getnative@*, lodash._getnative@^3.0.0: +lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" @@ -8897,10 +8879,6 @@ lodash.pick@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" -lodash.restparam@*: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - lodash.some@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" @@ -11594,7 +11572,7 @@ readable-stream@~2.1.5: string_decoder "~0.10.x" util-deprecate "~1.0.1" -readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0: +readdir-scoped-modules@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" dependencies: @@ -12073,12 +12051,22 @@ rx@2.3.24: version "2.3.24" resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7" +rxjs-compat@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/rxjs-compat/-/rxjs-compat-6.2.0.tgz#2eb49cc6ac20d0d7057c6887d1895beaab0966f9" + rxjs@^5.1.1, rxjs@^5.4.2, rxjs@^5.5.2: version "5.5.10" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.10.tgz#fde02d7a614f6c8683d0d1957827f492e09db045" dependencies: symbol-observable "1.0.1" +rxjs@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.2.0.tgz#e024d0e180b72756a83c2aaea8f25423751ba978" + dependencies: + tslib "^1.9.0" + safe-buffer@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -13201,7 +13189,7 @@ tryer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.0.tgz#027b69fa823225e551cace3ef03b11f6ab37c1d7" -tslib@^1.7.1: +tslib@^1.7.1, tslib@^1.9.0: version "1.9.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.1.tgz#a5d1f0532a49221c87755cfcc89ca37197242ba7"