From 7e3f581385bcb2e89e94791fa8b476c81aeb9711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Mon, 21 May 2018 20:56:17 +0200 Subject: [PATCH 1/5] Implement Ethereum --- package.json | 3 +- src/api/Ethereum.js | 69 +++ src/api/Fees.js | 13 +- src/api/Ledger.js | 18 +- src/bridge/EthereumJSBridge.js | 297 ++++++++- src/bridge/EthereumMockJSBridge.js | 22 + src/bridge/LibcoreBridge.js | 2 - src/bridge/UnsupportedBridge.js | 2 - src/bridge/index.js | 8 +- src/components/DeviceConnect/index.js | 32 +- src/components/EnsureDeviceApp/index.js | 23 +- src/components/modals/AddAccount/index.js | 22 +- src/components/modals/StepConnectDevice.js | 3 +- src/helpers/bip32path.js | 32 + src/internals/accounts/helpers.js | 22 +- src/internals/devices/ensureDeviceApp.js | 10 +- src/internals/devices/getAddress.js | 32 + .../devices/getAddressForCurrency/btc.js | 8 +- .../devices/getAddressForCurrency/ethereum.js | 8 +- .../devices/getAddressForCurrency/index.js | 11 +- src/internals/devices/index.js | 2 + src/internals/devices/signTransaction.js | 32 + .../signTransactionForCurrency/ethereum.js | 68 ++ .../signTransactionForCurrency/index.js | 27 + src/renderer/events.js | 6 + yarn.lock | 579 ++++++++++-------- 26 files changed, 993 insertions(+), 358 deletions(-) create mode 100644 src/api/Ethereum.js create mode 100644 src/bridge/EthereumMockJSBridge.js create mode 100644 src/helpers/bip32path.js create mode 100644 src/internals/devices/getAddress.js create mode 100644 src/internals/devices/signTransaction.js create mode 100644 src/internals/devices/signTransactionForCurrency/ethereum.js create mode 100644 src/internals/devices/signTransactionForCurrency/index.js diff --git a/package.json b/package.json index 6ba32201..97ec51cd 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@ledgerhq/hw-transport": "^4.12.0", "@ledgerhq/hw-transport-node-hid": "^4.12.0", "@ledgerhq/ledger-core": "^1.0.1", - "@ledgerhq/live-common": "^2.6.0", + "@ledgerhq/live-common": "^2.7.2", "axios": "^0.18.0", "babel-runtime": "^6.26.0", "bcryptjs": "^2.4.3", @@ -53,6 +53,7 @@ "downshift": "^1.31.9", "electron-store": "^1.3.0", "electron-updater": "^2.21.8", + "ethereumjs-tx": "^1.3.4", "fuse.js": "^3.2.0", "history": "^4.7.2", "i18next": "^11.2.2", diff --git a/src/api/Ethereum.js b/src/api/Ethereum.js new file mode 100644 index 00000000..d88ce0cd --- /dev/null +++ b/src/api/Ethereum.js @@ -0,0 +1,69 @@ +// @flow +import axios from 'axios' +import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types' +import { blockchainBaseURL } from './Ledger' + +export type Block = { height: number } // TODO more fields actually +export type Tx = { + hash: string, + received_at: string, + nonce: string, + value: number, + gas: number, + gas_price: number, + cumulative_gas_used: number, + gas_used: number, + from: string, + to: string, + input: string, + index: number, + block: { + hash: string, + height: number, + time: string, + }, + confirmations: number, +} + +export type API = { + getTransactions: ( + address: string, + blockHash: ?string, + ) => Promise<{ + truncated: boolean, + txs: Tx[], + }>, + getCurrentBlock: () => Promise, + getAccountNonce: (address: string) => Promise, + broadcastTransaction: (signedTransaction: string) => Promise, + getAccountBalance: (address: string) => Promise, +} + +export const apiForCurrency = (currency: CryptoCurrency): API => { + const baseURL = blockchainBaseURL(currency) + + return { + async getTransactions(address, blockHash) { + const { data } = await axios.get(`${baseURL}/addresses/${address}/transactions`, { + params: { blockHash, noToken: 1 }, + }) + return data + }, + async getCurrentBlock() { + const { data } = await axios.get(`${baseURL}/blocks/current`) + return data + }, + async getAccountNonce(address) { + const { data } = await axios.get(`${baseURL}/addresses/${address}/nonce`) + return data[0].nonce + }, + async broadcastTransaction(tx) { + const { data } = await axios.post(`${baseURL}/transactions/send`, { tx }) + return data.result + }, + async getAccountBalance(address) { + const { data } = await axios.get(`${baseURL}/addresses/${address}/balance`) + return data[0].balance + }, + } +} diff --git a/src/api/Fees.js b/src/api/Fees.js index 861b63c1..00e4278b 100644 --- a/src/api/Fees.js +++ b/src/api/Fees.js @@ -1,21 +1,14 @@ // @flow +import axios from 'axios' import type { Currency } from '@ledgerhq/live-common/lib/types' -import { get } from './Ledger' - -const mapping = { - bch: 'abc', -} -const currencyToFeeTicker = (currency: Currency) => { - const tickerLowerCase = currency.ticker.toLowerCase() - return mapping[tickerLowerCase] || tickerLowerCase -} +import { blockchainBaseURL } from './Ledger' export type Fees = { [_: string]: number, } export const getEstimatedFees = async (currency: Currency): Promise => { - const { data, status } = await get(`blockchain/v2/${currencyToFeeTicker(currency)}/fees`) + const { data, status } = await axios.get(`${blockchainBaseURL(currency)}/fees`) if (data) { return data } diff --git a/src/api/Ledger.js b/src/api/Ledger.js index c9105d42..93fb690a 100644 --- a/src/api/Ledger.js +++ b/src/api/Ledger.js @@ -1,9 +1,17 @@ // @flow -import axios from 'axios' +import type { Currency } from '@ledgerhq/live-common/lib/types' const BASE_URL = process.env.LEDGER_REST_API_BASE || 'https://api.ledgerwallet.com/' -export const get = (url: string, config: *): Promise<*> => - axios.get(`${BASE_URL}${url}`, { - ...config, - }) +const mapping = { + bch: 'abc', + etc: 'ethc', +} + +export const currencyToFeeTicker = (currency: Currency) => { + const tickerLowerCase = currency.ticker.toLowerCase() + return mapping[tickerLowerCase] || tickerLowerCase +} + +export const blockchainBaseURL = (currency: Currency) => + `${BASE_URL}blockchain/v2/${currencyToFeeTicker(currency)}` diff --git a/src/bridge/EthereumJSBridge.js b/src/bridge/EthereumJSBridge.js index bca51ca9..67b1a97d 100644 --- a/src/bridge/EthereumJSBridge.js +++ b/src/bridge/EthereumJSBridge.js @@ -1,10 +1,19 @@ // @flow import React from 'react' +import { ipcRenderer } from 'electron' +import { sendEvent } from 'renderer/events' import EthereumKind from 'components/FeesField/EthereumKind' -import type { EditProps } from './types' -import makeMockBridge from './makeMockBridge' +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 type { EditProps, WalletBridge } from './types' -const EditFees = ({ account, onChange, value }: EditProps<*>) => ( +// TODO in future it would be neat to support eip55 + +type Transaction = * + +const EditFees = ({ account, onChange, value }: EditProps) => ( { onChange({ ...value, gasPrice }) @@ -14,9 +23,285 @@ const EditFees = ({ account, onChange, value }: EditProps<*>) => ( /> ) -export default makeMockBridge({ - extraInitialTransactionProps: () => ({ gasPrice: 0 }), +const toAccountOperation = (account: Account) => (tx: Tx): Operation => { + const sending = account.address.toLowerCase() === tx.from.toLowerCase() + return { + id: tx.hash, + hash: tx.hash, + address: sending ? tx.to : tx.from, + amount: (sending ? -1 : 1) * tx.value, + blockHeight: tx.block.height, + blockHash: tx.block.hash, + accountId: account.id, + senders: [tx.from], + recipients: [tx.to], + date: new Date(tx.received_at), + } +} + +function isRecipientValid(currency, recipient) { + return !!recipient.match(/^0x[0-9a-fA-F]{40}$/) +} + +function mergeOps(existing: Operation[], newFetched: Operation[]) { + const ids = existing.map(o => o.id) + const all = existing.concat(newFetched.filter(o => !ids.includes(o.id))) + return all.sort((a, b) => a.date - b.date) +} + +const paginateMoreTransactions = async ( + account: Account, + acc: Operation[], +): Promise => { + const api = apiForCurrency(account.currency) + const { txs } = await api.getTransactions( + account.address, + acc.length ? acc[acc.length - 1].blockHash : undefined, + ) + if (txs.length === 0) return acc + 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 = () => { + 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) { + currentBlockPromise = api.getCurrentBlock() + } + 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 { + 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)) + next(account) + pollNextAddress() + } + } catch (e) { + error(e) + } + } + + function handleMsgEvent(e, { data, type }) { + if (type === 'devices.getAddress.success') { + stepAddress(data) + } else if (type === 'devices.getAddress.fail') { + error(new Error(data.message)) + } + } + + ipcRenderer.on('msg', handleMsgEvent) + + pollNextAddress() + + return { + unsubscribe() { + unbind() + }, + } + }, + + synchronize({ address, blockHeight, currency }, { next, complete, error }) { + let unsubscribed = false + const api = apiForCurrency(currency) + async function main() { + try { + const block = await api.getCurrentBlock() + if (unsubscribed) return + if (block.height === blockHeight) { + complete() + } else { + const balance = await api.getAccountBalance(address) + if (unsubscribed) return + const { txs } = await api.getTransactions(address) + if (unsubscribed) return + next(a => { + const currentOps = a.operations + const newOps = txs.map(toAccountOperation(a)) + if (newOps.length === 0 && currentOps.length === 0) return a + if (currentOps[0].id === newOps[0].id) return a + const operations = mergeOps(currentOps, newOps) + return { + ...a, + operations, + balance, + blockHeight: block.height, + lastSyncDate: new Date(), + } + }) + complete() + } + } catch (e) { + error(e) + } + } + main() + return { + unsubscribe() { + unsubscribed = true + }, + } + }, + + pullMoreOperations: async account => { + const operations = await paginateMoreTransactions(account, account.operations) + return a => ({ ...a, operations }) + }, + + isRecipientValid: (currency, recipient) => Promise.resolve(isRecipientValid(currency, recipient)), + + createTransaction: () => ({ + amount: 0, + recipient: '', + gasPrice: 0, + }), + + editTransactionAmount: (account, t, amount) => ({ + ...t, + amount, + }), + + getTransactionAmount: (a, t) => t.amount, + + editTransactionRecipient: (account, t, recipient) => ({ + ...t, + recipient, + }), + + getTransactionRecipient: (a, t) => t.recipient, + + isValidTransaction: (a, t) => (t.amount > 0 && t.recipient && true) || false, + + // $FlowFixMe EditFees, + getTotalSpent: (a, t) => Promise.resolve(t.amount + t.gasPrice), + getMaxAmount: (a, t) => Promise.resolve(a.balance - t.gasPrice), -}) + + 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 result = await api.broadcastTransaction(transaction) + return result + }, +} + +export default EthereumBridge diff --git a/src/bridge/EthereumMockJSBridge.js b/src/bridge/EthereumMockJSBridge.js new file mode 100644 index 00000000..bca51ca9 --- /dev/null +++ b/src/bridge/EthereumMockJSBridge.js @@ -0,0 +1,22 @@ +// @flow +import React from 'react' +import EthereumKind from 'components/FeesField/EthereumKind' +import type { EditProps } from './types' +import makeMockBridge from './makeMockBridge' + +const EditFees = ({ account, onChange, value }: EditProps<*>) => ( + { + onChange({ ...value, gasPrice }) + }} + gasPrice={value.gasPrice} + account={account} + /> +) + +export default makeMockBridge({ + extraInitialTransactionProps: () => ({ gasPrice: 0 }), + EditFees, + getTotalSpent: (a, t) => Promise.resolve(t.amount + t.gasPrice), + getMaxAmount: (a, t) => Promise.resolve(a.balance - t.gasPrice), +}) diff --git a/src/bridge/LibcoreBridge.js b/src/bridge/LibcoreBridge.js index 3104c7d7..eb4864fe 100644 --- a/src/bridge/LibcoreBridge.js +++ b/src/bridge/LibcoreBridge.js @@ -116,8 +116,6 @@ const LibcoreBridge: WalletBridge = { } }, - refreshLastOperations: () => Promise.reject(notImplemented), - pullMoreOperations: () => Promise.reject(notImplemented), isRecipientValid: (currency, recipient) => Promise.resolve(recipient.length > 0), diff --git a/src/bridge/UnsupportedBridge.js b/src/bridge/UnsupportedBridge.js index 1e4b9913..55261eb4 100644 --- a/src/bridge/UnsupportedBridge.js +++ b/src/bridge/UnsupportedBridge.js @@ -14,8 +14,6 @@ const UnsupportedBridge: WalletBridge<*> = { return { unsubscribe() {} } }, - refreshLastOperations: () => Promise.reject(genericError), - pullMoreOperations: () => Promise.reject(genericError), isRecipientValid: () => Promise.reject(genericError), diff --git a/src/bridge/index.js b/src/bridge/index.js index bd0132b8..ce777268 100644 --- a/src/bridge/index.js +++ b/src/bridge/index.js @@ -8,7 +8,11 @@ import EthereumJSBridge from './EthereumJSBridge' const RippleJSBridge = UnsupportedBridge export const getBridgeForCurrency = (currency: Currency): WalletBridge => { - if (currency.id === 'ethereum') return EthereumJSBridge // polyfill js - if (currency.id === 'ripple') return RippleJSBridge // polyfill js + if (currency.id.indexOf('ethereum') === 0) { + return EthereumJSBridge // polyfill js + } + if (currency.id === 'ripple') { + return RippleJSBridge // polyfill js + } return LibcoreBridge // libcore for the rest } diff --git a/src/components/DeviceConnect/index.js b/src/components/DeviceConnect/index.js index 0c45ae47..13b7d5d7 100644 --- a/src/components/DeviceConnect/index.js +++ b/src/components/DeviceConnect/index.js @@ -143,6 +143,7 @@ type Props = { deviceSelected: ?Device, onChangeDevice: Device => void, t: T, + errorMessage: ?string, } const emitChangeDevice = props => { @@ -180,7 +181,15 @@ class DeviceConnect extends PureComponent { } render() { - const { deviceSelected, accountName, currency, t, onChangeDevice, devices } = this.props + const { + deviceSelected, + errorMessage, + accountName, + currency, + t, + onChangeDevice, + devices, + } = this.props const appState = this.getAppState() @@ -250,19 +259,24 @@ class DeviceConnect extends PureComponent { - {accountName !== null && ( - + + {appState.fail ? ( + - - - {'You must use the device associated to the account '} - {accountName} - + + {accountName ? ( + + {'You must use the device associated to the account '} + {accountName} + + ) : ( + String(errorMessage || '') + )} - )} + ) : null} ) } diff --git a/src/components/EnsureDeviceApp/index.js b/src/components/EnsureDeviceApp/index.js index 6ad972a0..47d4fb46 100644 --- a/src/components/EnsureDeviceApp/index.js +++ b/src/components/EnsureDeviceApp/index.js @@ -3,6 +3,7 @@ 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' @@ -15,7 +16,7 @@ type OwnProps = { currency: ?CryptoCurrency, deviceSelected: ?Device, account: ?Account, - onStatusChange?: (DeviceStatus, AppStatus) => void, + onStatusChange?: (DeviceStatus, AppStatus, ?string) => void, // TODO prefer children function render?: ({ appStatus: AppStatus, @@ -23,6 +24,7 @@ type OwnProps = { devices: Device[], deviceSelected: ?Device, deviceStatus: DeviceStatus, + errorMessage: ?string, }) => React$Element<*>, } @@ -37,6 +39,7 @@ type AppStatus = 'success' | 'fail' | 'progress' type State = { deviceStatus: DeviceStatus, appStatus: AppStatus, + errorMessage: ?string, } const mapStateToProps = (state: StoreState) => ({ @@ -47,6 +50,7 @@ class EnsureDeviceApp extends PureComponent { state = { appStatus: 'progress', deviceStatus: this.props.deviceSelected ? 'connected' : 'unconnected', + errorMessage: null, } componentDidMount() { @@ -100,16 +104,20 @@ class EnsureDeviceApp extends PureComponent { if (account) { options = { currencyId: account.currency.id, - accountPath: account.path, + path: account.path, accountAddress: account.address, segwit: account.path.startsWith("49'"), // TODO: store segwit info in account } } else if (currency) { options = { currencyId: currency.id, + path: makeBip44Path({ currency }), } + } else { + throw new Error('either currency or account is required') } + // TODO just use getAddress! sendEvent('devices', 'ensureDeviceApp', { devicePath: deviceSelected.path, ...options, @@ -118,11 +126,11 @@ class EnsureDeviceApp extends PureComponent { _timeout: * - handleStatusChange = (deviceStatus, appStatus) => { + handleStatusChange = (deviceStatus, appStatus, errorMessage = null) => { const { onStatusChange } = this.props clearTimeout(this._timeout) - this.setState({ deviceStatus, appStatus }) - onStatusChange && onStatusChange(deviceStatus, appStatus) + this.setState({ deviceStatus, appStatus, errorMessage }) + onStatusChange && onStatusChange(deviceStatus, appStatus, errorMessage) } handleMsgEvent = (e, { type, data }) => { @@ -139,14 +147,14 @@ class EnsureDeviceApp extends PureComponent { } if (type === 'devices.ensureDeviceApp.fail' && deviceSelected.path === data.devicePath) { - this.handleStatusChange(deviceStatus, 'fail') + this.handleStatusChange(deviceStatus, 'fail', data.message) this._timeout = setTimeout(this.checkAppOpened, 1e3) } } render() { const { currency, account, devices, deviceSelected, render } = this.props - const { appStatus, deviceStatus } = this.state + const { appStatus, deviceStatus, errorMessage } = this.state if (render) { const cur = account ? account.currency : currency @@ -157,6 +165,7 @@ class EnsureDeviceApp extends PureComponent { devices, deviceSelected: deviceStatus === 'connected' ? deviceSelected : null, deviceStatus, + errorMessage, }) } diff --git a/src/components/modals/AddAccount/index.js b/src/components/modals/AddAccount/index.js index b719c189..9a903714 100644 --- a/src/components/modals/AddAccount/index.js +++ b/src/components/modals/AddAccount/index.js @@ -4,6 +4,7 @@ import React, { PureComponent } from 'react' import { connect } from 'react-redux' import { compose } from 'redux' import { translate } from 'react-i18next' +import { createStructuredSelector } from 'reselect' import type { Account, CryptoCurrency } from '@ledgerhq/live-common/lib/types' @@ -12,7 +13,12 @@ import type { Device, T } from 'types/common' import { MODAL_ADD_ACCOUNT } from 'config/constants' import { closeModal } from 'reducers/modals' -import { canCreateAccount, getAccounts, getArchivedAccounts } from 'reducers/accounts' +import { + canCreateAccount, + getAccounts, + getVisibleAccounts, + getArchivedAccounts, +} from 'reducers/accounts' import { addAccount, updateAccount } from 'actions/accounts' @@ -34,10 +40,11 @@ const GET_STEPS = t => [ { label: t('addAccount:steps.importAccounts.title'), Comp: StepImport }, ] -const mapStateToProps = state => ({ - existingAccounts: getAccounts(state), - archivedAccounts: getArchivedAccounts(state), - canCreateAccount: canCreateAccount(state), +const mapStateToProps = createStructuredSelector({ + existingAccounts: getAccounts, + visibleAccounts: getVisibleAccounts, + archivedAccounts: getArchivedAccounts, + canCreateAccount, }) const mapDispatchToProps = { @@ -49,6 +56,7 @@ const mapDispatchToProps = { type Props = { existingAccounts: Account[], addAccount: Function, + visibleAccounts: Account[], archivedAccounts: Account[], canCreateAccount: boolean, closeModal: Function, @@ -93,7 +101,7 @@ class AddAccountModal extends PureComponent { scanSubscription: * startScanAccountsDevice() { - const { existingAccounts, addAccount } = this.props + const { visibleAccounts, addAccount } = this.props const { deviceSelected, currency } = this.state if (!deviceSelected || !currency) { @@ -102,7 +110,7 @@ class AddAccountModal extends PureComponent { const bridge = getBridgeForCurrency(currency) this.scanSubscription = bridge.scanAccountsOnDevice(currency, deviceSelected.path, { next: account => { - if (!existingAccounts.some(a => a.id === account.id)) { + if (!visibleAccounts.some(a => a.id === account.id)) { addAccount(account) this.setState(state => ({ scannedAccounts: [...state.scannedAccounts, account], diff --git a/src/components/modals/StepConnectDevice.js b/src/components/modals/StepConnectDevice.js index 9c6d58eb..b36d6740 100644 --- a/src/components/modals/StepConnectDevice.js +++ b/src/components/modals/StepConnectDevice.js @@ -30,7 +30,7 @@ const StepConnectDevice = ({ currency={currency} deviceSelected={deviceSelected} onStatusChange={onStatusChange} - render={({ currency, appStatus, devices, deviceSelected }) => ( + render={({ currency, appStatus, devices, deviceSelected, errorMessage }) => ( )} /> diff --git a/src/helpers/bip32path.js b/src/helpers/bip32path.js new file mode 100644 index 00000000..0eb4b7bb --- /dev/null +++ b/src/helpers/bip32path.js @@ -0,0 +1,32 @@ +// @flow +import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types' + +function shouldDerivateChangeFieldInsteadOfAccount(c: CryptoCurrency) { + // ethereum have a special way of derivating things + return c.id.indexOf('ethereum') === 0 +} + +// https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki +// x is a derivation index. we don't always derivate the same part of the path +export function makeBip44Path({ + currency, + segwit, + x, +}: { + currency: CryptoCurrency, + segwit?: boolean, + x?: number, +}): string { + const purpose = segwit ? 49 : 44 + const coinType = currency.coinType + let path = `${purpose}'/${coinType}'` + if (shouldDerivateChangeFieldInsteadOfAccount(currency)) { + path += "/0'" + if (x !== undefined) { + path += `/${x}` + } + } else if (x !== undefined) { + path += `/${x}'` + } + return path +} diff --git a/src/internals/accounts/helpers.js b/src/internals/accounts/helpers.js index f32ca20c..9a156769 100644 --- a/src/internals/accounts/helpers.js +++ b/src/internals/accounts/helpers.js @@ -4,27 +4,6 @@ import Btc from '@ledgerhq/hw-app-btc' -import { findCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' - -export function coinTypeForId(id: string) { - const currency = findCryptoCurrencyById(id) - return currency ? currency.coinType : 0 -} - -export function getPath({ - currencyId, - account, - segwit = true, -}: { - currencyId: string, - account?: any, - segwit?: boolean, -}) { - return `${segwit ? 49 : 44}'/${coinTypeForId(currencyId)}'${ - account !== undefined ? `/${account}'` : '' - }` -} - export async function getFreshReceiveAddress({ currencyId, accountIndex, @@ -53,6 +32,7 @@ export function verifyAddress({ 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/devices/ensureDeviceApp.js b/src/internals/devices/ensureDeviceApp.js index 3cbdf5a9..22a3c9e3 100644 --- a/src/internals/devices/ensureDeviceApp.js +++ b/src/internals/devices/ensureDeviceApp.js @@ -11,13 +11,13 @@ export default async ( { currencyId, devicePath, - accountPath, + path, accountAddress, ...options }: { currencyId: string, devicePath: string, - accountPath: ?string, + path: string, accountAddress: ?string, }, ) => { @@ -25,12 +25,12 @@ export default async ( invariant(currencyId, 'currencyId "%s" not defined', currencyId) const transport: Transport<*> = await CommNodeHid.open(devicePath) const resolver = getAddressForCurrency(currencyId) - const address = await resolver(transport, currencyId, accountPath, options) - if (accountPath && accountAddress && address !== accountAddress) { + 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 }) + send('devices.ensureDeviceApp.fail', { devicePath, message: err.message }) } } diff --git a/src/internals/devices/getAddress.js b/src/internals/devices/getAddress.js new file mode 100644 index 00000000..c8e54fde --- /dev/null +++ b/src/internals/devices/getAddress.js @@ -0,0 +1,32 @@ +// @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, + ...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 }) + } +} diff --git a/src/internals/devices/getAddressForCurrency/btc.js b/src/internals/devices/getAddressForCurrency/btc.js index 2dc40d65..401b4768 100644 --- a/src/internals/devices/getAddressForCurrency/btc.js +++ b/src/internals/devices/getAddressForCurrency/btc.js @@ -2,12 +2,11 @@ import Btc from '@ledgerhq/hw-app-btc' import type Transport from '@ledgerhq/hw-transport' -import { getPath } from 'internals/accounts/helpers' export default async ( transport: Transport<*>, currencyId: string, - bip32path: ?string, + path: string, { segwit = true, verify = false, @@ -17,7 +16,6 @@ export default async ( }, ) => { const btc = new Btc(transport) - const path = bip32path || getPath({ currencyId, segwit }) - const { bitcoinAddress } = await btc.getWalletPublicKey(path, verify, segwit) - return bitcoinAddress + const { bitcoinAddress, publicKey } = await btc.getWalletPublicKey(path, verify, segwit) + return { address: bitcoinAddress, path, publicKey } } diff --git a/src/internals/devices/getAddressForCurrency/ethereum.js b/src/internals/devices/getAddressForCurrency/ethereum.js index 152383cb..604de42b 100644 --- a/src/internals/devices/getAddressForCurrency/ethereum.js +++ b/src/internals/devices/getAddressForCurrency/ethereum.js @@ -2,16 +2,14 @@ import Eth from '@ledgerhq/hw-app-eth' import type Transport from '@ledgerhq/hw-transport' -import { getPath } from 'internals/accounts/helpers' export default async ( transport: Transport<*>, currencyId: string, - bip32path: ?string, + path: string, { verify = false }: { verify: boolean }, ) => { const eth = new Eth(transport) - const path = bip32path || getPath({ currencyId }) - const { address } = await eth.getAddress(path, verify) - return address + const { address, publicKey } = await eth.getAddress(path, verify) + return { path, address, publicKey } } diff --git a/src/internals/devices/getAddressForCurrency/index.js b/src/internals/devices/getAddressForCurrency/index.js index 08d8573c..329657b5 100644 --- a/src/internals/devices/getAddressForCurrency/index.js +++ b/src/internals/devices/getAddressForCurrency/index.js @@ -2,13 +2,14 @@ import type Transport from '@ledgerhq/hw-transport' import btc from './btc' +import ethereum from './ethereum' type Resolver = ( transport: Transport<*>, currencyId: string, - bip32path: ?string, // if provided use this path, otherwise resolve it + path: string, options: *, -) => Promise +) => Promise<{ address: string, path: string, publicKey: string }> type Module = (currencyId: string) => Resolver @@ -19,8 +20,10 @@ const all = { bitcoin: btc, bitcoin_testnet: btc, - ethereum: btc, - ethereum_testnet: btc, + ethereum, + ethereum_testnet: ethereum, + ethereum_classic: ethereum, + ethereum_classic_testnet: ethereum, } const getAddressForCurrency: Module = (currencyId: string) => all[currencyId] || fallback(currencyId) diff --git a/src/internals/devices/index.js b/src/internals/devices/index.js index 1f33c58e..2516aafa 100644 --- a/src/internals/devices/index.js +++ b/src/internals/devices/index.js @@ -1,2 +1,4 @@ export listen from './listen' export ensureDeviceApp from './ensureDeviceApp' +export getAddress from './getAddress' +export signTransaction from './signTransaction' diff --git a/src/internals/devices/signTransaction.js b/src/internals/devices/signTransaction.js new file mode 100644 index 00000000..f40c4334 --- /dev/null +++ b/src/internals/devices/signTransaction.js @@ -0,0 +1,32 @@ +// @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 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') + } +} diff --git a/src/internals/devices/signTransactionForCurrency/ethereum.js b/src/internals/devices/signTransactionForCurrency/ethereum.js new file mode 100644 index 00000000..3f0b432f --- /dev/null +++ b/src/internals/devices/signTransactionForCurrency/ethereum.js @@ -0,0 +1,68 @@ +// @flow +import Eth from '@ledgerhq/hw-app-eth' +import type Transport from '@ledgerhq/hw-transport' +import EthereumTx from 'ethereumjs-tx' + +// see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md +function getNetworkId(currencyId: string): ?number { + switch (currencyId) { + case 'ethereum': + return 1 + case 'ethereum_classic': + return 61 + case 'ethereum_classic_testnet': + return 62 + case 'ethereum_testnet': + return 3 // Ropsten by convention + default: + return null + } +} + +export default async ( + transport: Transport<*>, + currencyId: string, + path: string, + t: { + nonce: string, + recipient: string, + gasPrice: number, + amount: number, + }, +) => { + // First, we need to create a partial tx and send to the device + + const chainId = getNetworkId(currencyId) + if (!chainId) throw new Error(`chainId not found for currency=${currencyId}`) + const gasLimit = '0x5208' // cost of a simple send + const tx = new EthereumTx({ + nonce: t.nonce, + gasPrice: `0x${t.gasPrice.toString(16)}`, + gasLimit, + to: t.recipient, + value: `0x${t.amount.toString(16)}`, + chainId, + }) + tx.raw[6] = Buffer.from([chainId]) // v + tx.raw[7] = Buffer.from([]) // r + tx.raw[8] = Buffer.from([]) // s + + const eth = new Eth(transport) + const result = await eth.signTransaction(path, tx.serialize().toString('hex')) + + // Second, we re-set some tx fields from the device signature + + tx.v = Buffer.from(result.v, 'hex') + tx.r = Buffer.from(result.r, 'hex') + tx.s = Buffer.from(result.s, 'hex') + const signedChainId = Math.floor((tx.v[0] - 35) / 2) // EIP155: v should be chain_id * 2 + {35, 36} + const validChainId = chainId & 0xff // eslint-disable-line no-bitwise + if (signedChainId !== validChainId) { + throw new Error( + `Invalid chainId signature returned. Expected: ${chainId}, Got: ${signedChainId}`, + ) + } + + // Finally, we can send the transaction string to broadcast + return `0x${tx.serialize().toString('hex')}` +} diff --git a/src/internals/devices/signTransactionForCurrency/index.js b/src/internals/devices/signTransactionForCurrency/index.js new file mode 100644 index 00000000..c40d534f --- /dev/null +++ b/src/internals/devices/signTransactionForCurrency/index.js @@ -0,0 +1,27 @@ +// @flow + +import type Transport from '@ledgerhq/hw-transport' +import ethereum from './ethereum' + +type Resolver = ( + transport: Transport<*>, + currencyId: string, + path: string, // if provided use this path, otherwise resolve it + transaction: *, // any data +) => Promise + +type Module = (currencyId: string) => Resolver + +const fallback: string => Resolver = currencyId => () => + Promise.reject(new Error(`${currencyId} device support not implemented`)) + +const all = { + ethereum, + ethereum_testnet: ethereum, + ethereum_classic: ethereum, + ethereum_classic_testnet: ethereum, +} + +const m: Module = (currencyId: string) => all[currencyId] || fallback(currencyId) + +export default m diff --git a/src/renderer/events.js b/src/renderer/events.js index cebd47cc..b5341eaf 100644 --- a/src/renderer/events.js +++ b/src/renderer/events.js @@ -2,6 +2,12 @@ // FIXME this file is spaghetti. we need one file per usecase. +// TODO to improve current state: +// a sendEventPromise version that returns a promise +// a sendEventObserver version that takes an observer & return a Subscription +// both of these implementation should have a unique requestId to ensure there is no collision +// events should all appear in the promise result / observer msgs as soon as they have this requestId + import { ipcRenderer } from 'electron' import objectPath from 'object-path' import debug from 'debug' diff --git a/yarn.lock b/yarn.lock index db2718a5..eb6c7058 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22,6 +22,10 @@ "7zip-bin-mac" "~1.0.1" "7zip-bin-win" "~2.2.0" +"7zip-bin@~4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/7zip-bin/-/7zip-bin-4.0.2.tgz#6abbdc22f33cab742053777a26db2e25ca527179" + "7zip@0.0.6": version "0.0.6" resolved "https://registry.yarnpkg.com/7zip/-/7zip-0.0.6.tgz#9cafb171af82329490353b4816f03347aa150a30" @@ -1469,9 +1473,9 @@ npm "^5.7.1" prebuild-install "^2.2.2" -"@ledgerhq/live-common@^2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-2.6.0.tgz#f6a4fd8da6cb8e88125fb6648af4f2fbabe7fc03" +"@ledgerhq/live-common@^2.7.2": + version "2.7.2" + resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-2.7.2.tgz#abfa71428c186220006d35baca44261a1c3ef9ed" dependencies: axios "^0.18.0" invariant "^2.2.2" @@ -1502,11 +1506,11 @@ version "0.7.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" -"@storybook/addon-actions@3.4.4", "@storybook/addon-actions@^3.4.2": - version "3.4.4" - resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-3.4.4.tgz#03cc675977b6c6ce44a4dd7e2d36a910848af85f" +"@storybook/addon-actions@3.4.5", "@storybook/addon-actions@^3.4.2": + version "3.4.5" + resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-3.4.5.tgz#ba0d0c0c74357c0852e0b890b404214975df40a8" dependencies: - "@storybook/components" "3.4.4" + "@storybook/components" "3.4.5" babel-runtime "^6.26.0" deep-equal "^1.0.1" glamor "^2.20.40" @@ -1518,10 +1522,10 @@ uuid "^3.2.1" "@storybook/addon-knobs@^3.4.2": - version "3.4.4" - resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-3.4.4.tgz#96043d68ac382084a9e9d208a084fe58e968377f" + version "3.4.5" + resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-3.4.5.tgz#6ac34c1845b0db6683455e1edbe15993490fc18b" dependencies: - "@storybook/components" "3.4.4" + "@storybook/components" "3.4.5" babel-runtime "^6.26.0" deep-equal "^1.0.1" global "^4.3.2" @@ -1534,58 +1538,58 @@ react-textarea-autosize "^5.2.1" util-deprecate "^1.0.2" -"@storybook/addon-links@3.4.4", "@storybook/addon-links@^3.4.2": - version "3.4.4" - resolved "https://registry.yarnpkg.com/@storybook/addon-links/-/addon-links-3.4.4.tgz#ff2a7c810c97fe465b25e417752eeebb5322ed9c" +"@storybook/addon-links@3.4.5", "@storybook/addon-links@^3.4.2": + version "3.4.5" + resolved "https://registry.yarnpkg.com/@storybook/addon-links/-/addon-links-3.4.5.tgz#c885dae976f5084faa0ae026755f2d6068da034f" dependencies: - "@storybook/components" "3.4.4" + "@storybook/components" "3.4.5" babel-runtime "^6.26.0" global "^4.3.2" prop-types "^15.6.1" "@storybook/addon-options@^3.4.2": - version "3.4.4" - resolved "https://registry.yarnpkg.com/@storybook/addon-options/-/addon-options-3.4.4.tgz#4cdaf7cf1c8c2d5483f16fa8de448b38b9a1fd57" + version "3.4.5" + resolved "https://registry.yarnpkg.com/@storybook/addon-options/-/addon-options-3.4.5.tgz#f1aeb1a8e5f9a804e4cc2b8aa86edcdf18dfe252" dependencies: babel-runtime "^6.26.0" -"@storybook/addons@3.4.4", "@storybook/addons@^3.4.2": - version "3.4.4" - resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-3.4.4.tgz#5f3203791df83499247a8bc4034788943500ff16" +"@storybook/addons@3.4.5", "@storybook/addons@^3.4.2": + version "3.4.5" + resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-3.4.5.tgz#8dd6eb93ae26b4498a9b1e2b890316944efa41da" -"@storybook/channel-postmessage@3.4.4": - version "3.4.4" - resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-3.4.4.tgz#96ead79c7752c747c355f297f4f344a44763600b" +"@storybook/channel-postmessage@3.4.5": + version "3.4.5" + resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-3.4.5.tgz#8c7de674847dec2a154947e0b99e54a2618c79cb" dependencies: - "@storybook/channels" "3.4.4" + "@storybook/channels" "3.4.5" global "^4.3.2" json-stringify-safe "^5.0.1" -"@storybook/channels@3.4.4": - version "3.4.4" - resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-3.4.4.tgz#a0492404d21231dcbe12716707472dd4549fb37a" +"@storybook/channels@3.4.5": + version "3.4.5" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-3.4.5.tgz#cb8e3798cdcbed0b7037e634fb029cdd758c73a8" -"@storybook/client-logger@3.4.4": - version "3.4.4" - resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-3.4.4.tgz#e76a457e7a19ba7a66fae9cce91bf52e6ece3f6e" +"@storybook/client-logger@3.4.5": + version "3.4.5" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-3.4.5.tgz#aca45699b14d6663c694ab3253d46bc257f8783e" -"@storybook/components@3.4.4": - version "3.4.4" - resolved "https://registry.yarnpkg.com/@storybook/components/-/components-3.4.4.tgz#272bcbdb2ac28583de049f75352f9eec76835641" +"@storybook/components@3.4.5": + version "3.4.5" + resolved "https://registry.yarnpkg.com/@storybook/components/-/components-3.4.5.tgz#02653ba562e3678eab3adbc31b012eae4fbe61dc" dependencies: glamor "^2.20.40" glamorous "^4.12.1" prop-types "^15.6.1" -"@storybook/core@3.4.4": - version "3.4.4" - resolved "https://registry.yarnpkg.com/@storybook/core/-/core-3.4.4.tgz#4335974772bcdf858b5ccbc7f89d9cd6090e7889" +"@storybook/core@3.4.5": + version "3.4.5" + resolved "https://registry.yarnpkg.com/@storybook/core/-/core-3.4.5.tgz#a34a498eace85d29a12559a895c420f5ae54b0b7" dependencies: - "@storybook/addons" "3.4.4" - "@storybook/channel-postmessage" "3.4.4" - "@storybook/client-logger" "3.4.4" - "@storybook/node-logger" "3.4.4" - "@storybook/ui" "3.4.4" + "@storybook/addons" "3.4.5" + "@storybook/channel-postmessage" "3.4.5" + "@storybook/client-logger" "3.4.5" + "@storybook/node-logger" "3.4.5" + "@storybook/ui" "3.4.5" autoprefixer "^7.2.6" babel-runtime "^6.26.0" chalk "^2.3.2" @@ -1617,9 +1621,9 @@ "@storybook/react-simple-di" "^1.2.1" babel-runtime "6.x.x" -"@storybook/node-logger@3.4.4": - version "3.4.4" - resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-3.4.4.tgz#f4378e63d3fb6addc76f9a8baaf5be4dae7811f4" +"@storybook/node-logger@3.4.5": + version "3.4.5" + resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-3.4.5.tgz#d08c082b0d4a573736f25c2e0eea261b30183962" dependencies: npmlog "^4.1.2" @@ -1656,17 +1660,17 @@ babel-runtime "^6.5.0" "@storybook/react@^3.4.2": - version "3.4.4" - resolved "https://registry.yarnpkg.com/@storybook/react/-/react-3.4.4.tgz#53c9c8f50283ee9bdc8bfcb882cbf74b0658a4f1" - dependencies: - "@storybook/addon-actions" "3.4.4" - "@storybook/addon-links" "3.4.4" - "@storybook/addons" "3.4.4" - "@storybook/channel-postmessage" "3.4.4" - "@storybook/client-logger" "3.4.4" - "@storybook/core" "3.4.4" - "@storybook/node-logger" "3.4.4" - "@storybook/ui" "3.4.4" + version "3.4.5" + resolved "https://registry.yarnpkg.com/@storybook/react/-/react-3.4.5.tgz#1979423f365252cf03a4508db26b676f239c1e80" + dependencies: + "@storybook/addon-actions" "3.4.5" + "@storybook/addon-links" "3.4.5" + "@storybook/addons" "3.4.5" + "@storybook/channel-postmessage" "3.4.5" + "@storybook/client-logger" "3.4.5" + "@storybook/core" "3.4.5" + "@storybook/node-logger" "3.4.5" + "@storybook/ui" "3.4.5" airbnb-js-shims "^1.4.1" babel-loader "^7.1.4" babel-plugin-macros "^2.2.0" @@ -1699,11 +1703,11 @@ webpack "^3.11.0" webpack-hot-middleware "^2.22.1" -"@storybook/ui@3.4.4": - version "3.4.4" - resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-3.4.4.tgz#2bb8ba4cdb7711695e018c2cc2db0a7e1c48a09a" +"@storybook/ui@3.4.5": + version "3.4.5" + resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-3.4.5.tgz#b11e97590b5e09b2d1452c0a4b84567780ec3390" dependencies: - "@storybook/components" "3.4.4" + "@storybook/components" "3.4.5" "@storybook/mantra-core" "^1.7.2" "@storybook/podda" "^1.2.3" "@storybook/react-komposer" "^2.0.3" @@ -1726,8 +1730,8 @@ react-treebeard "^2.1.0" "@types/node@^8.0.24": - version "8.10.15" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.15.tgz#3ce3cdf6ee1846a9db0c0f52275c14bf0cd67f67" + version "8.10.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.17.tgz#d48cf10f0dc6dcf59f827f5a3fc7a4a6004318d3" "@types/webpack-env@^1.13.6": version "1.13.6" @@ -2053,26 +2057,14 @@ app-builder-bin-linux@1.8.6: version "1.8.6" resolved "https://registry.yarnpkg.com/app-builder-bin-linux/-/app-builder-bin-linux-1.8.6.tgz#81176bbcb2929958a90f2184afb54df90b7210a3" -app-builder-bin-linux@1.8.9: - version "1.8.9" - resolved "https://registry.yarnpkg.com/app-builder-bin-linux/-/app-builder-bin-linux-1.8.9.tgz#478c3269c89a92e35716200bd10497833431fe94" - app-builder-bin-mac@1.8.6: version "1.8.6" resolved "https://registry.yarnpkg.com/app-builder-bin-mac/-/app-builder-bin-mac-1.8.6.tgz#20d7233c5cadf00472e7b0ccaf85627b53f90787" -app-builder-bin-mac@1.8.9: - version "1.8.9" - resolved "https://registry.yarnpkg.com/app-builder-bin-mac/-/app-builder-bin-mac-1.8.9.tgz#e4047505b0b0556258cdb2af200806401b34767a" - app-builder-bin-win@1.8.6: version "1.8.6" resolved "https://registry.yarnpkg.com/app-builder-bin-win/-/app-builder-bin-win-1.8.6.tgz#d09f78fb1dd5a5f8ea231294828fd5c9ad0358a5" -app-builder-bin-win@1.8.9: - version "1.8.9" - resolved "https://registry.yarnpkg.com/app-builder-bin-win/-/app-builder-bin-win-1.8.9.tgz#3ffcf0b24a00a4653dbc6dc9865b166194fa7067" - app-builder-bin@1.8.6: version "1.8.6" resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-1.8.6.tgz#85604ece9c1b63ed0437abe92ddaf41c88c3f2e4" @@ -2081,13 +2073,9 @@ app-builder-bin@1.8.6: app-builder-bin-mac "1.8.6" app-builder-bin-win "1.8.6" -app-builder-bin@1.8.9: - version "1.8.9" - resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-1.8.9.tgz#4b79027d85533926630d5ea749c081d671307616" - optionalDependencies: - app-builder-bin-linux "1.8.9" - app-builder-bin-mac "1.8.9" - app-builder-bin-win "1.8.9" +app-builder-bin@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-1.9.0.tgz#700ff08c2558bb27d271c655e8353fe703fa7647" append-transform@^0.4.0: version "0.4.0" @@ -2289,10 +2277,10 @@ async@^1.4.0, async@^1.5.0, async@^1.5.2: resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" async@^2.1.2, async@^2.1.4, async@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" + version "2.6.1" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" dependencies: - lodash "^4.14.0" + lodash "^4.17.10" asynckit@^0.4.0: version "0.4.0" @@ -2559,12 +2547,12 @@ babel-helpers@^6.24.1: babel-runtime "^6.22.0" babel-template "^6.24.1" -babel-jest@^22.4.3: - version "22.4.3" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-22.4.3.tgz#4b7a0b6041691bbd422ab49b3b73654a49a6627a" +babel-jest@^22.4.3, babel-jest@^22.4.4: + version "22.4.4" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-22.4.4.tgz#977259240420e227444ebe49e226a61e49ea659d" dependencies: babel-plugin-istanbul "^4.1.5" - babel-preset-jest "^22.4.3" + babel-preset-jest "^22.4.4" babel-loader@^7.1.4: version "7.1.4" @@ -2575,12 +2563,13 @@ babel-loader@^7.1.4: mkdirp "^0.5.1" babel-loader@^8.0.0-beta.2: - version "8.0.0-beta.2" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.0-beta.2.tgz#4d5b67c964dc8c9cba866fd13d6b90df3acf8723" + version "8.0.0-beta.3" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.0-beta.3.tgz#49efeea6e8058d5af860a18a6de88b8c1450645b" dependencies: find-cache-dir "^1.0.0" loader-utils "^1.0.2" mkdirp "^0.5.1" + util.promisify "^1.0.0" babel-messages@^6.23.0: version "6.23.0" @@ -2609,9 +2598,9 @@ babel-plugin-istanbul@^4.1.5: istanbul-lib-instrument "^1.10.1" test-exclude "^4.2.1" -babel-plugin-jest-hoist@^22.4.3: - version "22.4.3" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-22.4.3.tgz#7d8bcccadc2667f96a0dcc6afe1891875ee6c14a" +babel-plugin-jest-hoist@^22.4.4: + version "22.4.4" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-22.4.4.tgz#b9851906eab34c7bf6f8c895a2b08bea1a844c0b" babel-plugin-macros@^2.2.0: version "2.2.1" @@ -3197,11 +3186,11 @@ babel-preset-flow@^6.23.0: dependencies: babel-plugin-transform-flow-strip-types "^6.22.0" -babel-preset-jest@^22.4.3: - version "22.4.3" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-22.4.3.tgz#e92eef9813b7026ab4ca675799f37419b5a44156" +babel-preset-jest@^22.4.4: + version "22.4.4" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-22.4.4.tgz#ec9fbd8bcd7dfd24b8b5320e0e688013235b7c39" dependencies: - babel-plugin-jest-hoist "^22.4.3" + babel-plugin-jest-hoist "^22.4.4" babel-plugin-syntax-object-rest-spread "^6.13.0" babel-preset-minify@^0.3.0: @@ -3447,11 +3436,11 @@ binaryextensions@2: version "2.1.1" resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.1.1.tgz#3209a51ca4a4ad541a3b8d3d6a6d5b83a2485935" -bindings@^1.3.0: +bindings@^1.2.1, bindings@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7" -bip66@^1.1.0: +bip66@^1.1.0, bip66@^1.1.3: version "1.1.5" resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22" dependencies: @@ -3508,7 +3497,7 @@ bluebird@~3.4.1: version "3.4.7" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.0, bn.js@^4.11.3, bn.js@^4.4.0: version "4.11.8" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" @@ -3542,18 +3531,6 @@ boolbase@^1.0.0, boolbase@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" -boom@4.x.x: - version "4.3.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" - dependencies: - hoek "4.x.x" - -boom@5.x.x: - version "5.2.0" - resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" - dependencies: - hoek "4.x.x" - bowser@^1.0.0, bowser@^1.7.3: version "1.9.3" resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.9.3.tgz#6643ae4d783f31683f6d23156976b74183862162" @@ -3618,7 +3595,7 @@ browser-resolve@^1.11.2: dependencies: resolve "1.1.7" -browserify-aes@^1.0.0, browserify-aes@^1.0.4: +browserify-aes@^1.0.0, browserify-aes@^1.0.4, browserify-aes@^1.0.6: version "1.2.0" resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" dependencies: @@ -3685,11 +3662,11 @@ browserslist@^2.11.3: electron-to-chromium "^1.3.30" browserslist@^3.0.0, browserslist@^3.2.6: - version "3.2.7" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-3.2.7.tgz#aa488634d320b55e88bab0256184dbbcca1e6de9" + version "3.2.8" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-3.2.8.tgz#b0005361d6471f0f5952797a76fc985f1f978fc6" dependencies: - caniuse-lite "^1.0.30000835" - electron-to-chromium "^1.3.45" + caniuse-lite "^1.0.30000844" + electron-to-chromium "^1.3.47" bs58@^4.0.0, bs58@^4.0.1: version "4.0.1" @@ -3793,11 +3770,11 @@ builder-util@5.8.1: temp-file "^3.1.2" builder-util@^5.8.1: - version "5.9.0" - resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-5.9.0.tgz#1ddf9728f5d674aa85472b6d92f61aa4f7c6b5d5" + version "5.10.0" + resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-5.10.0.tgz#025291dba29a865b6e2b28959d0f757f4883c3bd" dependencies: - "7zip-bin" "~3.1.0" - app-builder-bin "1.8.9" + "7zip-bin" "~4.0.2" + app-builder-bin "1.9.0" bluebird-lst "^1.0.5" builder-util-runtime "^4.2.1" chalk "^2.4.1" @@ -3947,12 +3924,12 @@ caniuse-api@^1.5.2: lodash.uniq "^4.5.0" caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: - version "1.0.30000842" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000842.tgz#8a82c377b8b3d6f2594478e8431ff4fd303e160c" + version "1.0.30000844" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000844.tgz#bca5798cda2b6931d68100c2d69e55fb338cbb41" -caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.30000805, caniuse-lite@^1.0.30000835: - version "1.0.30000842" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000842.tgz#7a198e3181a207f4b5749b8f5a1817685bf3d7df" +caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.30000805, caniuse-lite@^1.0.30000844: + version "1.0.30000844" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000844.tgz#de7c84cde0582143cf4f5abdf1b98e5a0539ad4a" capture-exit@^1.2.0: version "1.2.0" @@ -4315,8 +4292,8 @@ colors@1.0.3: resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" colors@^1.1.2: - version "1.2.5" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.5.tgz#89c7ad9a374bc030df8013241f68136ed8835afc" + version "1.3.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.0.tgz#5f20c9fef6945cb1134260aab33bfbdc8295e04e" colors@~1.1.2: version "1.1.2" @@ -4627,12 +4604,6 @@ crypt@~0.0.1: version "0.0.2" resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" -cryptiles@3.x.x: - version "3.1.2" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" - dependencies: - boom "5.x.x" - crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -5101,7 +5072,7 @@ debug@3.1.0, debug@^3.0.0, debug@^3.1.0: dependencies: ms "2.0.0" -debuglog@^1.0.1: +debuglog@*, debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -5424,6 +5395,14 @@ downshift@^1.31.9: version "1.31.14" resolved "https://registry.yarnpkg.com/downshift/-/downshift-1.31.14.tgz#98b04614cad2abc4297d0d02b50ff2c48b2625e7" +drbg.js@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/drbg.js/-/drbg.js-1.0.1.tgz#3e36b6c42b37043823cdbc332d58f31e2445480b" + dependencies: + browserify-aes "^1.0.6" + create-hash "^1.1.2" + create-hmac "^1.1.4" + duplexer2@~0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" @@ -5638,7 +5617,7 @@ electron-store@^1.3.0: dependencies: conf "^1.3.0" -electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30, electron-to-chromium@^1.3.45: +electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30, electron-to-chromium@^1.3.47: version "1.3.47" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.47.tgz#764e887ca9104d01a0ac8eabee7dfc0e2ce14104" @@ -5710,7 +5689,7 @@ elegant-spinner@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" -elliptic@^6.0.0: +elliptic@^6.0.0, elliptic@^6.2.3: version "6.4.0" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" dependencies: @@ -6104,6 +6083,36 @@ etag@~1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" +ethereum-common@^0.0.18: + version "0.0.18" + resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.0.18.tgz#2fdc3576f232903358976eb39da783213ff9523f" + +ethereumjs-tx@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/ethereumjs-tx/-/ethereumjs-tx-1.3.4.tgz#c2304912f6c07af03237ad8675ac036e290dad48" + dependencies: + ethereum-common "^0.0.18" + ethereumjs-util "^5.0.0" + +ethereumjs-util@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz#3e0c0d1741471acf1036052d048623dee54ad642" + dependencies: + bn.js "^4.11.0" + create-hash "^1.1.2" + ethjs-util "^0.1.3" + keccak "^1.0.2" + rlp "^2.0.0" + safe-buffer "^5.1.1" + secp256k1 "^3.0.1" + +ethjs-util@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/ethjs-util/-/ethjs-util-0.1.4.tgz#1c8b6879257444ef4d3f3fbbac2ded12cd997d93" + dependencies: + is-hex-prefixed "1.0.0" + strip-hex-prefix "1.0.0" + event-emitter@~0.3.5: version "0.3.5" resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" @@ -6200,7 +6209,7 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -expect@^22.4.3: +expect@^22.4.0: version "22.4.3" resolved "https://registry.yarnpkg.com/expect/-/expect-22.4.3.tgz#d5a29d0a0e1fb2153557caef2674d4547e914674" dependencies: @@ -6546,8 +6555,8 @@ flush-write-stream@^1.0.0: readable-stream "^2.0.4" follow-redirects@^1.0.0, follow-redirects@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.1.tgz#d8120f4518190f55aac65bb6fc7b85fcd666d6aa" + version "1.5.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.0.tgz#234f49cf770b7f35b40e790f636ceba0c3a0ab77" dependencies: debug "^3.1.0" @@ -7051,8 +7060,8 @@ har-validator@~5.0.3: har-schema "^2.0.0" hard-source-webpack-plugin@^0.6.0: - version "0.6.7" - resolved "https://registry.yarnpkg.com/hard-source-webpack-plugin/-/hard-source-webpack-plugin-0.6.7.tgz#c77ca3dc3ddd9d090d81b9caf9e84baa10731309" + version "0.6.9" + resolved "https://registry.yarnpkg.com/hard-source-webpack-plugin/-/hard-source-webpack-plugin-0.6.9.tgz#b23b3cd3a3c661b6fce2925d928691055f950e7f" dependencies: lodash "^4.15.0" mkdirp "^0.5.1" @@ -7156,15 +7165,6 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.0" -hawk@~6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" - dependencies: - boom "4.x.x" - cryptiles "3.x.x" - hoek "4.x.x" - sntp "2.x.x" - he@1.1.x: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" @@ -7187,10 +7187,6 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoek@4.x.x: - version "4.2.1" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" - hoist-non-react-statics@1.x.x, hoist-non-react-statics@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb" @@ -7268,8 +7264,8 @@ html-loader@^1.0.0-alpha.0: schema-utils "^0.4.3" html-minifier@^3.2.3, html-minifier@^3.5.8: - version "3.5.15" - resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.15.tgz#f869848d4543cbfd84f26d5514a2a87cbf9a05e0" + version "3.5.16" + resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.16.tgz#39f5aabaf78bdfc057fe67334226efd7f3851175" dependencies: camel-case "3.0.x" clean-css "4.1.x" @@ -7498,7 +7494,7 @@ import-local@^1.0.0: pkg-dir "^2.0.0" resolve-cwd "^2.0.0" -imurmurhash@^0.1.4: +imurmurhash@*, imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -7808,6 +7804,10 @@ is-glob@^4.0.0: dependencies: is-extglob "^2.1.1" +is-hex-prefixed@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz#7d8d37e6ad77e5d127148913c573e082d777f554" + is-installed-globally@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" @@ -8081,15 +8081,15 @@ isurl@^1.0.0-alpha5: has-to-string-tag-x "^1.2.0" is-object "^1.0.1" -jest-changed-files@^22.4.3: +jest-changed-files@^22.2.0: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-22.4.3.tgz#8882181e022c38bd46a2e4d18d44d19d90a90fb2" dependencies: throat "^4.0.0" -jest-cli@^22.4.3: - version "22.4.3" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-22.4.3.tgz#bf16c4a5fb7edc3fa5b9bb7819e34139e88a72c7" +jest-cli@^22.4.4: + version "22.4.4" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-22.4.4.tgz#68cd2a2aae983adb1e6638248ca21082fd6d9e90" dependencies: ansi-escapes "^3.0.0" chalk "^2.0.1" @@ -8102,20 +8102,20 @@ jest-cli@^22.4.3: istanbul-lib-coverage "^1.1.1" istanbul-lib-instrument "^1.8.0" istanbul-lib-source-maps "^1.2.1" - jest-changed-files "^22.4.3" - jest-config "^22.4.3" - jest-environment-jsdom "^22.4.3" - jest-get-type "^22.4.3" - jest-haste-map "^22.4.3" - jest-message-util "^22.4.3" - jest-regex-util "^22.4.3" - jest-resolve-dependencies "^22.4.3" - jest-runner "^22.4.3" - jest-runtime "^22.4.3" - jest-snapshot "^22.4.3" - jest-util "^22.4.3" - jest-validate "^22.4.3" - jest-worker "^22.4.3" + jest-changed-files "^22.2.0" + jest-config "^22.4.4" + jest-environment-jsdom "^22.4.1" + jest-get-type "^22.1.0" + jest-haste-map "^22.4.2" + jest-message-util "^22.4.0" + jest-regex-util "^22.1.0" + jest-resolve-dependencies "^22.1.0" + jest-runner "^22.4.4" + jest-runtime "^22.4.4" + jest-snapshot "^22.4.0" + jest-util "^22.4.1" + jest-validate "^22.4.4" + jest-worker "^22.2.2" micromatch "^2.3.11" node-notifier "^5.2.1" realpath-native "^1.0.0" @@ -8126,23 +8126,23 @@ jest-cli@^22.4.3: which "^1.2.12" yargs "^10.0.3" -jest-config@^22.4.3: - version "22.4.3" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-22.4.3.tgz#0e9d57db267839ea31309119b41dc2fa31b76403" +jest-config@^22.4.4: + version "22.4.4" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-22.4.4.tgz#72a521188720597169cd8b4ff86934ef5752d86a" dependencies: chalk "^2.0.1" glob "^7.1.1" - jest-environment-jsdom "^22.4.3" - jest-environment-node "^22.4.3" - jest-get-type "^22.4.3" - jest-jasmine2 "^22.4.3" - jest-regex-util "^22.4.3" - jest-resolve "^22.4.3" - jest-util "^22.4.3" - jest-validate "^22.4.3" - pretty-format "^22.4.3" - -jest-diff@^22.4.3: + jest-environment-jsdom "^22.4.1" + jest-environment-node "^22.4.1" + jest-get-type "^22.1.0" + jest-jasmine2 "^22.4.4" + jest-regex-util "^22.1.0" + jest-resolve "^22.4.2" + jest-util "^22.4.1" + jest-validate "^22.4.4" + pretty-format "^22.4.0" + +jest-diff@^22.4.0, jest-diff@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-22.4.3.tgz#e18cc3feff0aeef159d02310f2686d4065378030" dependencies: @@ -8151,13 +8151,13 @@ jest-diff@^22.4.3: jest-get-type "^22.4.3" pretty-format "^22.4.3" -jest-docblock@^22.4.3: +jest-docblock@^22.4.0, jest-docblock@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-22.4.3.tgz#50886f132b42b280c903c592373bb6e93bb68b19" dependencies: detect-newline "^2.1.0" -jest-environment-jsdom@^22.4.3: +jest-environment-jsdom@^22.4.1: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-22.4.3.tgz#d67daa4155e33516aecdd35afd82d4abf0fa8a1e" dependencies: @@ -8165,18 +8165,18 @@ jest-environment-jsdom@^22.4.3: jest-util "^22.4.3" jsdom "^11.5.1" -jest-environment-node@^22.4.3: +jest-environment-node@^22.4.1: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-22.4.3.tgz#54c4eaa374c83dd52a9da8759be14ebe1d0b9129" dependencies: jest-mock "^22.4.3" jest-util "^22.4.3" -jest-get-type@^22.4.3: +jest-get-type@^22.1.0, jest-get-type@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.4.3.tgz#e3a8504d8479342dd4420236b322869f18900ce4" -jest-haste-map@^22.4.3: +jest-haste-map@^22.4.2: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-22.4.3.tgz#25842fa2ba350200767ac27f658d58b9d5c2e20b" dependencies: @@ -8188,29 +8188,29 @@ jest-haste-map@^22.4.3: micromatch "^2.3.11" sane "^2.0.0" -jest-jasmine2@^22.4.3: - version "22.4.3" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-22.4.3.tgz#4daf64cd14c793da9db34a7c7b8dcfe52a745965" +jest-jasmine2@^22.4.4: + version "22.4.4" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-22.4.4.tgz#c55f92c961a141f693f869f5f081a79a10d24e23" dependencies: chalk "^2.0.1" co "^4.6.0" - expect "^22.4.3" + expect "^22.4.0" graceful-fs "^4.1.11" is-generator-fn "^1.0.0" - jest-diff "^22.4.3" - jest-matcher-utils "^22.4.3" - jest-message-util "^22.4.3" - jest-snapshot "^22.4.3" - jest-util "^22.4.3" + jest-diff "^22.4.0" + jest-matcher-utils "^22.4.0" + jest-message-util "^22.4.0" + jest-snapshot "^22.4.0" + jest-util "^22.4.1" source-map-support "^0.5.0" -jest-leak-detector@^22.4.3: +jest-leak-detector@^22.4.0: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-22.4.3.tgz#2b7b263103afae8c52b6b91241a2de40117e5b35" dependencies: pretty-format "^22.4.3" -jest-matcher-utils@^22.4.3: +jest-matcher-utils@^22.4.0, jest-matcher-utils@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-22.4.3.tgz#4632fe428ebc73ebc194d3c7b65d37b161f710ff" dependencies: @@ -8218,7 +8218,7 @@ jest-matcher-utils@^22.4.3: jest-get-type "^22.4.3" pretty-format "^22.4.3" -jest-message-util@^22.4.3: +jest-message-util@^22.4.0, jest-message-util@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-22.4.3.tgz#cf3d38aafe4befddbfc455e57d65d5239e399eb7" dependencies: @@ -8232,56 +8232,56 @@ jest-mock@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-22.4.3.tgz#f63ba2f07a1511772cdc7979733397df770aabc7" -jest-regex-util@^22.4.3: +jest-regex-util@^22.1.0, jest-regex-util@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-22.4.3.tgz#a826eb191cdf22502198c5401a1fc04de9cef5af" -jest-resolve-dependencies@^22.4.3: +jest-resolve-dependencies@^22.1.0: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-22.4.3.tgz#e2256a5a846732dc3969cb72f3c9ad7725a8195e" dependencies: jest-regex-util "^22.4.3" -jest-resolve@^22.4.3: +jest-resolve@^22.4.2: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-22.4.3.tgz#0ce9d438c8438229aa9b916968ec6b05c1abb4ea" dependencies: browser-resolve "^1.11.2" chalk "^2.0.1" -jest-runner@^22.4.3: - version "22.4.3" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-22.4.3.tgz#298ddd6a22b992c64401b4667702b325e50610c3" +jest-runner@^22.4.4: + version "22.4.4" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-22.4.4.tgz#dfca7b7553e0fa617e7b1291aeb7ce83e540a907" dependencies: exit "^0.1.2" - jest-config "^22.4.3" - jest-docblock "^22.4.3" - jest-haste-map "^22.4.3" - jest-jasmine2 "^22.4.3" - jest-leak-detector "^22.4.3" - jest-message-util "^22.4.3" - jest-runtime "^22.4.3" - jest-util "^22.4.3" - jest-worker "^22.4.3" + jest-config "^22.4.4" + jest-docblock "^22.4.0" + jest-haste-map "^22.4.2" + jest-jasmine2 "^22.4.4" + jest-leak-detector "^22.4.0" + jest-message-util "^22.4.0" + jest-runtime "^22.4.4" + jest-util "^22.4.1" + jest-worker "^22.2.2" throat "^4.0.0" -jest-runtime@^22.4.3: - version "22.4.3" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-22.4.3.tgz#b69926c34b851b920f666c93e86ba2912087e3d0" +jest-runtime@^22.4.4: + version "22.4.4" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-22.4.4.tgz#9ba7792fc75582a5be0f79af6f8fe8adea314048" dependencies: babel-core "^6.0.0" - babel-jest "^22.4.3" + babel-jest "^22.4.4" babel-plugin-istanbul "^4.1.5" chalk "^2.0.1" convert-source-map "^1.4.0" exit "^0.1.2" graceful-fs "^4.1.11" - jest-config "^22.4.3" - jest-haste-map "^22.4.3" - jest-regex-util "^22.4.3" - jest-resolve "^22.4.3" - jest-util "^22.4.3" - jest-validate "^22.4.3" + jest-config "^22.4.4" + jest-haste-map "^22.4.2" + jest-regex-util "^22.1.0" + jest-resolve "^22.4.2" + jest-util "^22.4.1" + jest-validate "^22.4.4" json-stable-stringify "^1.0.1" micromatch "^2.3.11" realpath-native "^1.0.0" @@ -8294,7 +8294,7 @@ jest-serializer@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-22.4.3.tgz#a679b81a7f111e4766235f4f0c46d230ee0f7436" -jest-snapshot@^22.4.3: +jest-snapshot@^22.4.0: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-22.4.3.tgz#b5c9b42846ffb9faccb76b841315ba67887362d2" dependencies: @@ -8305,7 +8305,7 @@ jest-snapshot@^22.4.3: natural-compare "^1.4.0" pretty-format "^22.4.3" -jest-util@^22.4.3: +jest-util@^22.4.1, jest-util@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-22.4.3.tgz#c70fec8eec487c37b10b0809dc064a7ecf6aafac" dependencies: @@ -8317,28 +8317,28 @@ jest-util@^22.4.3: mkdirp "^0.5.1" source-map "^0.6.0" -jest-validate@^22.4.3: - version "22.4.3" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-22.4.3.tgz#0780954a5a7daaeec8d3c10834b9280865976b30" +jest-validate@^22.4.4: + version "22.4.4" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-22.4.4.tgz#1dd0b616ef46c995de61810d85f57119dbbcec4d" dependencies: chalk "^2.0.1" - jest-config "^22.4.3" - jest-get-type "^22.4.3" + jest-config "^22.4.4" + jest-get-type "^22.1.0" leven "^2.1.0" - pretty-format "^22.4.3" + pretty-format "^22.4.0" -jest-worker@^22.4.3: +jest-worker@^22.2.2, jest-worker@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-22.4.3.tgz#5c421417cba1c0abf64bf56bd5fb7968d79dd40b" dependencies: merge-stream "^1.0.1" jest@^22.4.3: - version "22.4.3" - resolved "https://registry.yarnpkg.com/jest/-/jest-22.4.3.tgz#2261f4b117dc46d9a4a1a673d2150958dee92f16" + version "22.4.4" + resolved "https://registry.yarnpkg.com/jest/-/jest-22.4.4.tgz#ffb36c9654b339a13e10b3d4b338eb3e9d49f6eb" dependencies: import-local "^1.0.0" - jest-cli "^22.4.3" + jest-cli "^22.4.4" js-base64@^2.1.9: version "2.4.5" @@ -8560,6 +8560,15 @@ jsx-ast-utils@^2.0.0, jsx-ast-utils@^2.0.1: dependencies: array-includes "^3.0.3" +keccak@^1.0.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/keccak/-/keccak-1.4.0.tgz#572f8a6dbee8e7b3aa421550f9e6408ca2186f80" + dependencies: + bindings "^1.2.1" + inherits "^2.0.3" + nan "^2.2.1" + safe-buffer "^5.1.0" + keycode@^2.1.9: version "2.2.0" resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04" @@ -8799,6 +8808,10 @@ 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" @@ -8806,11 +8819,25 @@ 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@^3.0.0: +lodash._getnative@*, lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" @@ -8870,6 +8897,10 @@ 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" @@ -8898,7 +8929,7 @@ lodash@^3.10.1: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1: +lodash@^4.0.1, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" @@ -9289,8 +9320,8 @@ minimist@~0.0.1: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" minipass@^2.2.1, minipass@^2.2.4: - version "2.3.0" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.0.tgz#2e11b1c46df7fe7f1afbe9a490280add21ffe384" + version "2.3.1" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.1.tgz#4e872b959131a672837ab3cb554962bc84b1537d" dependencies: safe-buffer "^5.1.1" yallist "^3.0.0" @@ -9412,7 +9443,7 @@ mute-stream@0.0.7, mute-stream@~0.0.4: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" -nan@^2.6.2, nan@^2.9.2: +nan@^2.2.1, nan@^2.6.2, nan@^2.9.2: version "2.10.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" @@ -10828,7 +10859,7 @@ pretty-error@^2.0.2: renderkid "^2.0.1" utila "~0.4" -pretty-format@^22.4.3: +pretty-format@^22.4.0, pretty-format@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-22.4.3.tgz#f873d780839a9c02e9664c8a082e9ee79eaac16f" dependencies: @@ -11563,7 +11594,7 @@ readable-stream@~2.1.5: string_decoder "~0.10.x" util-deprecate "~1.0.1" -readdir-scoped-modules@^1.0.0: +readdir-scoped-modules@*, 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: @@ -11845,8 +11876,8 @@ request-promise-native@^1.0.5: tough-cookie ">=2.3.3" request@2, request@^2.45.0, request@^2.74.0, request@^2.83.0, request@^2.85.0: - version "2.86.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.86.0.tgz#2b9497f449b0a32654c081a5cf426bbfb5bf5b69" + version "2.87.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e" dependencies: aws-sign2 "~0.7.0" aws4 "^1.6.0" @@ -11856,7 +11887,6 @@ request@2, request@^2.45.0, request@^2.74.0, request@^2.83.0, request@^2.85.0: forever-agent "~0.6.1" form-data "~2.3.1" har-validator "~5.0.3" - hawk "~6.0.2" http-signature "~1.2.0" is-typedarray "~1.0.0" isstream "~0.1.2" @@ -11999,6 +12029,10 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +rlp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.0.0.tgz#9db384ff4b89a8f61563d92395d8625b18f3afb0" + rsvp@^3.3.3: version "3.6.2" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" @@ -12106,8 +12140,21 @@ scoped-regex@^1.0.0: resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-1.0.0.tgz#a346bb1acd4207ae70bd7c0c7ca9e566b6baddb8" sdp@^2.6.0, sdp@^2.7.0: - version "2.7.3" - resolved "https://registry.yarnpkg.com/sdp/-/sdp-2.7.3.tgz#ed177eb4074aa3213e150e74a9ab2d06ae6e5dbf" + version "2.7.4" + resolved "https://registry.yarnpkg.com/sdp/-/sdp-2.7.4.tgz#cac76b0e2f16f55243d25bc0432f6bbb5488bfc1" + +secp256k1@^3.0.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-3.5.0.tgz#677d3b8a8e04e1a5fa381a1ae437c54207b738d0" + dependencies: + bindings "^1.2.1" + bip66 "^1.1.3" + bn.js "^4.11.3" + create-hash "^1.1.2" + drbg.js "^1.0.1" + elliptic "^6.2.3" + nan "^2.2.1" + safe-buffer "^5.1.0" select-hose@^2.0.0: version "2.0.0" @@ -12364,12 +12411,6 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" -sntp@2.x.x: - version "2.1.0" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" - dependencies: - hoek "4.x.x" - sockjs-client@1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.4.tgz#5babe386b775e4cf14e7520911452654016c8b12" @@ -12748,6 +12789,12 @@ strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" +strip-hex-prefix@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz#0c5f155fef1151373377de9dbb588da05500e36f" + dependencies: + is-hex-prefixed "1.0.0" + strip-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" @@ -13205,8 +13252,8 @@ uglify-es@3.3.7, uglify-es@^3.3.4, uglify-es@^3.3.9: source-map "~0.6.1" uglify-js@3.3.x: - version "3.3.25" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.3.25.tgz#3266ccb87c5bea229f69041a0296010d6477d539" + version "3.3.26" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.3.26.tgz#858b74e5e7262e876c834b907a5fa57d4fa0d525" dependencies: commander "~2.15.0" source-map "~0.6.1" @@ -13343,8 +13390,8 @@ unset-value@^1.0.0: isobject "^3.0.0" untildify@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.2.tgz#7f1f302055b3fea0f3e81dc78eb36766cb65e3f1" + version "3.0.3" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9" unzip-response@^2.0.1: version "2.0.1" @@ -13660,8 +13707,8 @@ webpack-addons@^1.1.5: jscodeshift "^0.4.0" webpack-bundle-analyzer@^2.11.1: - version "2.12.0" - resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.12.0.tgz#abb8ed9053123dabeb03a0463db52706f54730c7" + version "2.13.0" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.13.0.tgz#8d7db44c3d4844bc911890998e1110514cf12264" dependencies: acorn "^5.3.0" bfj-node4 "^5.2.0" 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 2/5] 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" From 7a2f9ed96d12605efb57c7d1c6ec4b5b1e4b485f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Tue, 22 May 2018 10:10:55 +0200 Subject: [PATCH 3/5] bugfix empty account sync --- src/bridge/EthereumJSBridge.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/bridge/EthereumJSBridge.js b/src/bridge/EthereumJSBridge.js index ae971ecd..653ddb2d 100644 --- a/src/bridge/EthereumJSBridge.js +++ b/src/bridge/EthereumJSBridge.js @@ -194,8 +194,16 @@ const EthereumBridge: WalletBridge = { next(a => { const currentOps = a.operations const newOps = txs.map(toAccountOperation(a)) - if (newOps.length === 0 && currentOps.length === 0) return a - if (currentOps[0].id === newOps[0].id) return a + const { length: newLength } = newOps + const { length } = currentOps + if ( + // still empty + (length === 0 && newLength === 0) || + // latest is still same + (length > 0 && newLength > 0 && currentOps[0].id === newOps[0].id) + ) { + return a + } const operations = mergeOps(currentOps, newOps) return { ...a, From dd4f47c34c1307844899b9831ba6b74f96737cb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Tue, 22 May 2018 11:15:40 +0200 Subject: [PATCH 4/5] fix flow --- src/internals/accounts/scanAccountsOnDevice.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/internals/accounts/scanAccountsOnDevice.js b/src/internals/accounts/scanAccountsOnDevice.js index 0c8a92be..f8ba2daf 100644 --- a/src/internals/accounts/scanAccountsOnDevice.js +++ b/src/internals/accounts/scanAccountsOnDevice.js @@ -253,6 +253,7 @@ function buildOperationRaw({ core, op, xpub }: { core: Object, op: NJSOperation, senders: op.getSenders(), recipients: op.getRecipients(), blockHeight: op.getBlockHeight(), + blockHash: '', accountId: xpub, date: op.getDate().toISOString(), amount, From 3b8661f6f79708c68d9f13fbab8db0d0416a4e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Tue, 22 May 2018 12:14:56 +0200 Subject: [PATCH 5/5] polish --- src/api/Ledger.js | 7 ++++--- src/bridge/EthereumJSBridge.js | 4 ++-- src/{internals/devices => commands}/getAddress.js | 2 +- src/{internals/devices => commands}/signTransaction.js | 2 +- src/components/DeviceCheckAddress.js | 2 +- src/components/EnsureDeviceApp/index.js | 2 +- src/components/modals/AddAccount/index.js | 7 +++---- .../devices => helpers}/getAddressForCurrency/btc.js | 0 .../devices => helpers}/getAddressForCurrency/ethereum.js | 0 .../devices => helpers}/getAddressForCurrency/index.js | 0 .../signTransactionForCurrency/ethereum.js | 0 .../signTransactionForCurrency/index.js | 0 src/internals/devices/index.js | 4 ++-- 13 files changed, 15 insertions(+), 15 deletions(-) rename src/{internals/devices => commands}/getAddress.js (91%) rename src/{internals/devices => commands}/signTransaction.js (89%) rename src/{internals/devices => helpers}/getAddressForCurrency/btc.js (100%) rename src/{internals/devices => helpers}/getAddressForCurrency/ethereum.js (100%) rename src/{internals/devices => helpers}/getAddressForCurrency/index.js (100%) rename src/{internals/devices => helpers}/signTransactionForCurrency/ethereum.js (100%) rename src/{internals/devices => helpers}/signTransactionForCurrency/index.js (100%) diff --git a/src/api/Ledger.js b/src/api/Ledger.js index 93fb690a..d8ee1ad0 100644 --- a/src/api/Ledger.js +++ b/src/api/Ledger.js @@ -4,13 +4,14 @@ import type { Currency } from '@ledgerhq/live-common/lib/types' const BASE_URL = process.env.LEDGER_REST_API_BASE || 'https://api.ledgerwallet.com/' const mapping = { - bch: 'abc', - etc: 'ethc', + bitcoin_cash: 'abc', + ethereum_classic: 'ethc', + ethereum_testnet: 'eth_testnet', } export const currencyToFeeTicker = (currency: Currency) => { const tickerLowerCase = currency.ticker.toLowerCase() - return mapping[tickerLowerCase] || tickerLowerCase + return mapping[currency.id] || tickerLowerCase } export const blockchainBaseURL = (currency: Currency) => diff --git a/src/bridge/EthereumJSBridge.js b/src/bridge/EthereumJSBridge.js index 653ddb2d..0415b2c8 100644 --- a/src/bridge/EthereumJSBridge.js +++ b/src/bridge/EthereumJSBridge.js @@ -5,8 +5,8 @@ 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 getAddressCommand from 'commands/getAddress' +import signTransactionCommand from 'commands/signTransaction' import type { EditProps, WalletBridge } from './types' // TODO in future it would be neat to support eip55 diff --git a/src/internals/devices/getAddress.js b/src/commands/getAddress.js similarity index 91% rename from src/internals/devices/getAddress.js rename to src/commands/getAddress.js index 8ef7a798..03d9328d 100644 --- a/src/internals/devices/getAddress.js +++ b/src/commands/getAddress.js @@ -3,7 +3,7 @@ import { createCommand, Command } from 'helpers/ipc' import { fromPromise } from 'rxjs/observable/fromPromise' import CommNodeHid from '@ledgerhq/hw-transport-node-hid' -import getAddressForCurrency from './getAddressForCurrency' +import getAddressForCurrency from 'helpers/getAddressForCurrency' type Input = { currencyId: string, diff --git a/src/internals/devices/signTransaction.js b/src/commands/signTransaction.js similarity index 89% rename from src/internals/devices/signTransaction.js rename to src/commands/signTransaction.js index 17139067..b89ad0c5 100644 --- a/src/internals/devices/signTransaction.js +++ b/src/commands/signTransaction.js @@ -3,7 +3,7 @@ import { createCommand, Command } from 'helpers/ipc' import { fromPromise } from 'rxjs/observable/fromPromise' import CommNodeHid from '@ledgerhq/hw-transport-node-hid' -import signTransactionForCurrency from './signTransactionForCurrency' +import signTransactionForCurrency from 'helpers/signTransactionForCurrency' type Input = { currencyId: string, diff --git a/src/components/DeviceCheckAddress.js b/src/components/DeviceCheckAddress.js index ce2353f6..b94dadf2 100644 --- a/src/components/DeviceCheckAddress.js +++ b/src/components/DeviceCheckAddress.js @@ -4,7 +4,7 @@ import { PureComponent } from 'react' import type { Account } from '@ledgerhq/live-common/lib/types' import type { Device } from 'types/common' -import getAddress from 'internals/devices/getAddress' +import getAddress from 'commands/getAddress' type Props = { onCheck: boolean => void, diff --git a/src/components/EnsureDeviceApp/index.js b/src/components/EnsureDeviceApp/index.js index 944456a1..65da5c53 100644 --- a/src/components/EnsureDeviceApp/index.js +++ b/src/components/EnsureDeviceApp/index.js @@ -9,7 +9,7 @@ import type { Device } from 'types/common' import { getDevices } from 'reducers/devices' import type { State as StoreState } from 'reducers/index' -import getAddress from 'internals/devices/getAddress' +import getAddress from 'commands/getAddress' type OwnProps = { currency: ?CryptoCurrency, diff --git a/src/components/modals/AddAccount/index.js b/src/components/modals/AddAccount/index.js index 9a903714..e460b437 100644 --- a/src/components/modals/AddAccount/index.js +++ b/src/components/modals/AddAccount/index.js @@ -101,7 +101,7 @@ class AddAccountModal extends PureComponent { scanSubscription: * startScanAccountsDevice() { - const { visibleAccounts, addAccount } = this.props + const { visibleAccounts } = this.props const { deviceSelected, currency } = this.state if (!deviceSelected || !currency) { @@ -111,7 +111,6 @@ class AddAccountModal extends PureComponent { this.scanSubscription = bridge.scanAccountsOnDevice(currency, deviceSelected.path, { next: account => { if (!visibleAccounts.some(a => a.id === account.id)) { - addAccount(account) this.setState(state => ({ scannedAccounts: [...state.scannedAccounts, account], })) @@ -169,9 +168,9 @@ class AddAccountModal extends PureComponent { handleChangeStatus = (deviceStatus, appStatus) => this.setState({ appStatus }) handleImportAccount = () => { - const { updateAccount } = this.props + const { addAccount } = this.props const { selectedAccounts } = this.state - selectedAccounts.forEach(a => updateAccount({ ...a, archived: false })) + selectedAccounts.forEach(a => addAccount({ ...a, archived: false })) this.setState({ selectedAccounts: [] }) closeModal(MODAL_ADD_ACCOUNT) this.props.counterValuesPolling.poll() diff --git a/src/internals/devices/getAddressForCurrency/btc.js b/src/helpers/getAddressForCurrency/btc.js similarity index 100% rename from src/internals/devices/getAddressForCurrency/btc.js rename to src/helpers/getAddressForCurrency/btc.js diff --git a/src/internals/devices/getAddressForCurrency/ethereum.js b/src/helpers/getAddressForCurrency/ethereum.js similarity index 100% rename from src/internals/devices/getAddressForCurrency/ethereum.js rename to src/helpers/getAddressForCurrency/ethereum.js diff --git a/src/internals/devices/getAddressForCurrency/index.js b/src/helpers/getAddressForCurrency/index.js similarity index 100% rename from src/internals/devices/getAddressForCurrency/index.js rename to src/helpers/getAddressForCurrency/index.js diff --git a/src/internals/devices/signTransactionForCurrency/ethereum.js b/src/helpers/signTransactionForCurrency/ethereum.js similarity index 100% rename from src/internals/devices/signTransactionForCurrency/ethereum.js rename to src/helpers/signTransactionForCurrency/ethereum.js diff --git a/src/internals/devices/signTransactionForCurrency/index.js b/src/helpers/signTransactionForCurrency/index.js similarity index 100% rename from src/internals/devices/signTransactionForCurrency/index.js rename to src/helpers/signTransactionForCurrency/index.js diff --git a/src/internals/devices/index.js b/src/internals/devices/index.js index f1779b0d..7d19f51d 100644 --- a/src/internals/devices/index.js +++ b/src/internals/devices/index.js @@ -1,9 +1,9 @@ // @flow import type { Command } from 'helpers/ipc' -import getAddress from './getAddress' +import getAddress from 'commands/getAddress' +import signTransaction from 'commands/signTransaction' import listen from './listen' -import signTransaction from './signTransaction' // TODO port these to commands export { listen }