diff --git a/.storybook/addons.js b/.storybook/addons.js index 41a31c20..05510780 100644 --- a/.storybook/addons.js +++ b/.storybook/addons.js @@ -1,3 +1,3 @@ -import '@storybook/addon-actions/register' import '@storybook/addon-knobs/register' +import '@storybook/addon-actions/register' import '@storybook/addon-options/register' diff --git a/.storybook/config.js b/.storybook/config.js index 1d9e1c33..9f1d36ec 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -1,6 +1,7 @@ import React from 'react' import { configure, addDecorator } from '@storybook/react' import { withKnobs } from '@storybook/addon-knobs' +import { setOptions } from '@storybook/addon-options' import { ThemeProvider } from 'styled-components' import 'styles/global' @@ -19,4 +20,11 @@ addDecorator(story => ( addDecorator(withKnobs) +const { name, repository: url } = require('../package.json') + +setOptions({ + name, + url, +}) + configure(loadStories, module) diff --git a/flow-defs/module.js b/flow-defs/module.js index dd0331a6..55df3acd 100644 --- a/flow-defs/module.js +++ b/flow-defs/module.js @@ -1,3 +1,5 @@ +/* eslint-disable */ + declare var module: { hot: { accept(path: string, callback: () => void): void, diff --git a/flow-defs/process.js b/flow-defs/process.js index 06b5a40b..3a089aeb 100644 --- a/flow-defs/process.js +++ b/flow-defs/process.js @@ -1,6 +1,10 @@ +/* eslint-disable */ + declare var process: { send(args: any): void, on(event: string, args: any): void, + nextTick(callback: Function): void, + setMaxListeners(any): void, title: string, env: Object, } diff --git a/package.json b/package.json index dca3ead3..44467a12 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,7 @@ "storybook": "start-storybook -p 4444" }, "lint-staged": { - "*.js": [ - "eslint --fix", - "prettier --write", - "git add" - ] + "*.js": ["eslint --fix", "prettier --write", "git add"] }, "electronWebpack": { "renderer": { @@ -42,8 +38,10 @@ "@ledgerhq/hw-app-eth": "^1.1.2-beta.068e2a14", "@ledgerhq/hw-transport": "^1.1.2-beta.068e2a14", "@ledgerhq/hw-transport-node-hid": "^1.1.2-beta.068e2a14", + "axios": "^0.17.1", "bcryptjs": "^2.4.3", - "blockchain.info": "^2.11.0", + "bitcoinjs-lib": "^3.3.2", + "bs58check": "^2.1.1", "color": "^2.0.1", "downshift": "^1.25.0", "electron-store": "^1.3.0", @@ -69,17 +67,17 @@ "redux-actions": "^2.2.1", "redux-thunk": "^2.2.0", "shortid": "^2.2.8", - "source-map-support": "^0.5.0", + "source-map-support": "^0.5.2", "styled-components": "^2.2.4", "styled-system": "^1.1.1" }, "devDependencies": { - "@storybook/addon-actions": "^3.3.9", - "@storybook/addon-knobs": "^3.3.9", - "@storybook/addon-links": "^3.3.9", - "@storybook/addon-options": "^3.3.9", - "@storybook/addons": "^3.3.9", - "@storybook/react": "^3.3.9", + "@storybook/addon-actions": "^3.3.10", + "@storybook/addon-knobs": "^3.3.10", + "@storybook/addon-links": "^3.3.10", + "@storybook/addon-options": "^3.3.10", + "@storybook/addons": "^3.3.10", + "@storybook/react": "^3.3.10", "babel-core": "^6.26.0", "babel-eslint": "^8.2.1", "babel-loader": "^7.1.2", @@ -94,7 +92,7 @@ "electron-builder": "^19.54.0", "electron-devtools-installer": "^2.2.3", "electron-webpack": "1.11.0", - "eslint": "^4.13.1", + "eslint": "^4.16.0", "eslint-config-airbnb": "^16.1.0", "eslint-config-prettier": "^2.9.0", "eslint-import-resolver-babel-module": "^4.0.0", diff --git a/src/actions/accounts.js b/src/actions/accounts.js index 8d19539f..a0642bbb 100644 --- a/src/actions/accounts.js +++ b/src/actions/accounts.js @@ -1,6 +1,5 @@ // @flow -import values from 'lodash/values' import { createAction } from 'redux-actions' import type { Dispatch } from 'redux' @@ -8,10 +7,6 @@ import type { Dispatch } from 'redux' import db from 'helpers/db' import type { Account } from 'types/common' -import type { State } from 'reducers' - -import { getAccounts } from 'reducers/accounts' -import { getAddressData } from 'helpers/btc' export type AddAccount = Account => { type: string, payload: Account } export const addAccount: AddAccount = payload => ({ @@ -28,19 +23,6 @@ export const fetchAccounts: FetchAccounts = () => ({ const setAccountData = createAction('SET_ACCOUNT_DATA', (accountID, data) => ({ accountID, data })) export const syncAccount: Function = account => async (dispatch: Dispatch<*>) => { - const { address } = account - const addressData = await getAddressData(address) - dispatch(setAccountData(account.id, addressData)) -} - -export const syncAccounts = () => async (dispatch: Dispatch<*>, getState: () => State) => { - const state = getState() - const accountsMap = getAccounts(state) - const accounts = values(accountsMap) - - console.log(`syncing accounts...`) - - await Promise.all(accounts.map(account => dispatch(syncAccount(account)))) - - console.log(`all accounts synced`) + const { id, ...data } = account + dispatch(setAccountData(id, data)) } diff --git a/src/components/AccountPage.js b/src/components/AccountPage.js index 9f5217a9..bfcc454c 100644 --- a/src/components/AccountPage.js +++ b/src/components/AccountPage.js @@ -2,11 +2,12 @@ import React, { PureComponent, Fragment } from 'react' import { connect } from 'react-redux' -import { formatCurrencyUnit } from 'ledger-wallet-common/lib/data/currency' import type { MapStateToProps } from 'react-redux' import type { Account, AccountData } from 'types/common' +import { format } from 'helpers/btc' + import { getAccountById, getAccountData } from 'reducers/accounts' import Box, { Card } from 'components/base/Box' @@ -22,20 +23,6 @@ const mapStateToProps: MapStateToProps<*, *, *> = (state, props) => ({ accountData: getAccountData(state, props.match.params.id), }) -function formatBTC(v) { - return formatCurrencyUnit( - { - name: 'bitcoin', - code: 'BTC', - symbol: 'b', - magnitude: 8, - }, - v, - true, - true, - ) -} - class AccountPage extends PureComponent { render() { const { account, accountData } = this.props @@ -49,7 +36,7 @@ class AccountPage extends PureComponent { - {formatBTC(accountData.balance)} + {format(accountData.balance)} @@ -59,7 +46,7 @@ class AccountPage extends PureComponent { {accountData.transactions.map(tr => ( {'-'} - {formatBTC(tr.balance)} + {format(tr.balance)} ))} diff --git a/src/components/AppRegionDrag.js b/src/components/AppRegionDrag.js index 0bd42763..bb22212f 100644 --- a/src/components/AppRegionDrag.js +++ b/src/components/AppRegionDrag.js @@ -4,10 +4,6 @@ import styled from 'styled-components' export default styled.div` -webkit-app-region: drag; + background: ${p => p.theme.colors.white}; height: 40px; - left: 0; - position: absolute; - right: 0; - top: 0; - z-index: -1; ` diff --git a/src/components/DashboardPage.js b/src/components/DashboardPage.js index 1544391a..0ead5e39 100644 --- a/src/components/DashboardPage.js +++ b/src/components/DashboardPage.js @@ -4,28 +4,25 @@ import React, { PureComponent } from 'react' import { connect } from 'react-redux' import type { MapStateToProps } from 'react-redux' -import type { Device } from 'types/common' -import { getCurrentDevice } from 'reducers/devices' +import { format } from 'helpers/btc' + +import { getTotalBalance } from 'reducers/accounts' import Box from 'components/base/Box' const mapStateToProps: MapStateToProps<*, *, *> = state => ({ - currentDevice: getCurrentDevice(state), + totalBalance: getTotalBalance(state), }) type Props = { - currentDevice: Device | null, + totalBalance: number, } class DashboardPage extends PureComponent { render() { - const { currentDevice } = this.props - return currentDevice !== null ? ( - - Your current device: {currentDevice.path} - - ) : null + const { totalBalance } = this.props + return Your balance: {format(totalBalance)} } } diff --git a/src/components/SideBar/index.js b/src/components/SideBar/index.js index f3ae5bd6..07da6439 100644 --- a/src/components/SideBar/index.js +++ b/src/components/SideBar/index.js @@ -10,6 +10,7 @@ import type { Accounts } from 'types/common' import { openModal } from 'reducers/modals' import { getAccounts } from 'reducers/accounts' +import { format } from 'helpers/btc' import { rgba } from 'styles/helpers' import Box, { GrowScroll } from 'components/base/Box' @@ -29,7 +30,6 @@ const Container = styled(Box).attrs({ noShrink: true, })` background-color: ${p => rgba(p.theme.colors[p.bg], process.platform === 'darwin' ? 0.4 : 1)}; - padding-top: 40px; width: 250px; ` @@ -77,11 +77,7 @@ class SideBar extends PureComponent { {'Accounts'}
{Object.entries(accounts).map(([id, account]: [string, any]) => ( - + {account.name} ))} diff --git a/src/components/TopBar.js b/src/components/TopBar.js index 29e03298..a311425a 100644 --- a/src/components/TopBar.js +++ b/src/components/TopBar.js @@ -2,6 +2,7 @@ import React, { PureComponent, Fragment } from 'react' import { connect } from 'react-redux' +import { ipcRenderer } from 'electron' import type { MapStateToProps, MapDispatchToProps } from 'react-redux' import type { Device, Devices } from 'types/common' @@ -35,6 +36,10 @@ type Props = { } type State = { changeDevice: boolean, + sync: { + progress: null | boolean, + fail: boolean, + }, } const hasDevices = props => props.currentDevice === null && props.devices.length > 0 @@ -42,6 +47,14 @@ const hasDevices = props => props.currentDevice === null && props.devices.length class TopBar extends PureComponent { state = { changeDevice: hasDevices(this.props), + sync: { + progress: null, + fail: false, + }, + } + + componentDidMount() { + ipcRenderer.on('msg', this.handleAccountSync) } componentWillReceiveProps(nextProps) { @@ -52,6 +65,39 @@ class TopBar extends PureComponent { } } + componentWillUnmount() { + ipcRenderer.removeListener('msg', this.handleAccountSync) + } + + handleAccountSync = (e, { type }) => { + if (type === 'accounts.sync.progress') { + this.setState({ + sync: { + progress: true, + fail: false, + }, + }) + } + + if (type === 'accounts.sync.fail') { + this.setState({ + sync: { + progress: null, + fail: true, + }, + }) + } + + if (type === 'accounts.sync.success') { + this.setState({ + sync: { + progress: false, + fail: false, + }, + }) + } + } + handleChangeDevice = () => { const { devices } = this.props @@ -76,7 +122,7 @@ class TopBar extends PureComponent { render() { const { devices, hasPassword } = this.props - const { changeDevice } = this.state + const { changeDevice, sync } = this.state return ( @@ -94,16 +140,16 @@ class TopBar extends PureComponent { ))} )} - - {hasPassword && } - + + + {sync.progress === true + ? 'sync...' + : sync.fail === true ? 'sync fail :(' : 'sync finish!'} + + + {hasPassword && } + + ) diff --git a/src/components/base/Modal/stories.js b/src/components/base/Modal/stories.js index a532d3fb..09eb8217 100644 --- a/src/components/base/Modal/stories.js +++ b/src/components/base/Modal/stories.js @@ -7,7 +7,7 @@ import { Modal, ModalBody } from 'components/base/Modal' const stories = storiesOf('Modal', module) stories.add('basic', () => { - const isOpened = boolean('isOpened', false) + const isOpened = boolean('isOpened', true) return (
Connect your Ledger
, startWallet: (props: Object) =>
Select {props.wallet.toUpperCase()} App on your Ledger
, - confirmation: (props: Object) => ( + inProgress: (props: Object) => (
- Add {props.wallet.toUpperCase()} - {props.accountName} - {props.walletAddress} ? - + In progress. + {props.progress !== null && ( +
+ Account: {props.progress.account} / Transactions: {props.progress.transactions} +
+ )}
), + listAccounts: (props: Object) => { + const accounts = Object.entries(props.accounts) + return ( +
+ {accounts.length > 0 + ? accounts.map(([index, account]: [string, any]) => ( +
+
Balance: {account.balance}
+
Transactions: {account.transactions.length}
+
+ +
+
+ )) + : 'No accounts'} +
+ ) + }, } type InputValue = { @@ -62,20 +86,23 @@ type InputValue = { wallet: string, } -type Step = 'createAccount' | 'connectDevice' | 'startWallet' | 'confirmation' +type Step = 'createAccount' | 'connectDevice' | 'inProgress' | 'startWallet' | 'listAccounts' type Props = { addAccount: Function, closeModal: Function, currentDevice: Device | null, + accounts: Accounts, } type State = { inputValue: InputValue, step: Step, - walletAddress: string, + accounts: Object, + progress: null | Object, } const mapStateToProps: MapStateToProps<*, *, *> = state => ({ + accounts: getAccounts(state), currentDevice: getCurrentDevice(state), }) @@ -89,7 +116,8 @@ const defaultState = { accountName: '', wallet: '', }, - walletAddress: '', + accounts: {}, + progress: null, step: 'createAccount', } @@ -98,6 +126,10 @@ class AddAccountModal extends PureComponent { ...defaultState, } + componentDidMount() { + ipcRenderer.on('msg', this.handleWalletRequest) + } + componentWillReceiveProps(nextProps) { const { currentDevice } = nextProps @@ -119,33 +151,28 @@ class AddAccountModal extends PureComponent { } } + componentWillUnmount() { + ipcRenderer.removeListener('msg', this.handleWalletRequest) + clearTimeout(this._timeout) + } + getWalletInfos() { const { inputValue } = this.state - const { currentDevice } = this.props + const { currentDevice, accounts } = this.props if (currentDevice === null) { return } - const { data: { data }, type } = sendSyncEvent('usb', 'wallet.infos.request', { + sendEvent('usb', 'wallet.getAccounts', { path: currentDevice.path, wallet: inputValue.wallet, + currentAccounts: Object.keys(accounts), }) - - if (type === 'wallet.infos.fail') { - this._timeout = setTimeout(() => this.getWalletInfos(), 1e3) - } - - if (type === 'wallet.infos.success') { - this.setState({ - walletAddress: data.bitcoinAddress, - step: 'confirmation', - }) - } } getStepProps() { - const { inputValue, walletAddress, step } = this.state + const { inputValue, step, progress, accounts } = this.state const props = (predicate, props) => (predicate ? props : {}) @@ -158,26 +185,52 @@ class AddAccountModal extends PureComponent { ...props(step === 'startWallet', { wallet: inputValue.wallet, }), - ...props(step === 'confirmation', { - accountName: inputValue.accountName, - onConfirm: this.handleAddAccount, - wallet: inputValue.wallet, - walletAddress, + ...props(step === 'inProgress', { + progress, + }), + ...props(step === 'listAccounts', { + accounts, + onAddAccount: this.handleAddAccount, }), } } - handleAddAccount = () => { - const { inputValue, walletAddress } = this.state + handleWalletRequest = (e, { data, type }) => { + if (type === 'wallet.getAccounts.progress') { + this.setState({ + step: 'inProgress', + progress: data, + }) + } + + if (type === 'wallet.getAccounts.fail') { + this._timeout = setTimeout(() => this.getWalletInfos(), 1e3) + } + + if (type === 'wallet.getAccounts.success') { + this.setState({ + accounts: data, + step: 'listAccounts', + }) + } + } + + handleAddAccount = index => () => { + const { inputValue, accounts } = this.state const { addAccount, closeModal } = this.props - const account = { + const { id, balance, transactions } = accounts[index] + + addAccount({ + id, name: inputValue.accountName, type: inputValue.wallet, - address: walletAddress, - } + data: { + balance, + transactions, + }, + }) - addAccount(account) closeModal('add-account') } diff --git a/src/helpers/btc.js b/src/helpers/btc.js index 4e167743..30269a07 100644 --- a/src/helpers/btc.js +++ b/src/helpers/btc.js @@ -1,15 +1,42 @@ -import blockexplorer from 'blockchain.info/blockexplorer' +// @flow -const explorer = blockexplorer.usingNetwork(3) +import axios from 'axios' +import bitcoin from 'bitcoinjs-lib' +import { formatCurrencyUnit } from 'ledger-wallet-common/lib/data/currency' -function computeTransaction(address) { - return transaction => { - const outputVal = transaction.out - .filter(o => o.addr === address) +export function format(v: string | number, options: Object = { alwaysShowSign: true }) { + return formatCurrencyUnit( + { + name: 'bitcoin', + code: 'BTC', + symbol: 'b', + magnitude: 8, + }, + Number(v), + options.alwaysShowSign, + true, + ) +} + +export const networks = [ + { + ...bitcoin.networks.bitcoin, + family: 1, + }, + { + ...bitcoin.networks.testnet, + family: 1, + }, +] + +export function computeTransaction(addresses: Array<*>) { + return (transaction: Object) => { + const outputVal = transaction.outputs + .filter(o => addresses.includes(o.address)) .reduce((acc, cur) => acc + cur.value, 0) const inputVal = transaction.inputs - .filter(i => i.prev_out.addr === address) - .reduce((acc, cur) => acc + cur.prev_out.value, 0) + .filter(i => addresses.includes(i.address)) + .reduce((acc, cur) => acc + cur.value, 0) const balance = outputVal - inputVal return { ...transaction, @@ -18,12 +45,78 @@ function computeTransaction(address) { } } -export async function getAddressData(address) { - const addressData = await explorer.getAddress(address) - const unifiedData = { - address, - balance: addressData.final_balance, - transactions: addressData.txs.map(computeTransaction(address)), +export function getTransactions(addresses: Array) { + return axios.get( + `http://api.ledgerwallet.com/blockchain/v2/btc_testnet/addresses/${addresses.join( + ',', + )}/transactions?noToken=true`, + ) +} + +export async function getAccount({ + currentIndex = 0, + hdnode, + segwit, + network, +}: { + currentIndex?: number, + hdnode: Object, + segwit: boolean, + network: Object, +}) { + const script = segwit ? parseInt(network.scriptHash, 10) : parseInt(network.pubKeyHash, 10) + + let transactions = [] + + const pubKeyToSegwitAddress = (pubKey, scriptVersion) => { + const script = [0x00, 0x14].concat(Array.from(bitcoin.crypto.hash160(pubKey))) + const hash160 = bitcoin.crypto.hash160(new Uint8Array(script)) + return bitcoin.address.toBase58Check(hash160, scriptVersion) + } + + const getPublicAddress = ({ hdnode, path, script, segwit }) => { + hdnode = hdnode.derivePath(path) + if (!segwit) { + return hdnode.getAddress().toString() + } + return pubKeyToSegwitAddress(hdnode.getPublicKeyBuffer(), script) } - return unifiedData + + const nextPath = (index = 0) => { + const count = 20 + const getAddress = path => getPublicAddress({ hdnode, path, script, segwit }) + + return Promise.all( + Array.from(Array(count).keys()).map(v => + Promise.all([ + getAddress(`0/${v + index}`), // external chain + getAddress(`1/${v + index}`), // internal chain + ]), + ), + ).then(async results => { + const currentAddresses = results.reduce((result, v) => [...result, ...v], []) + + const { data: { txs } } = await getTransactions(currentAddresses) + + transactions = [...transactions, ...txs.map(computeTransaction(currentAddresses))] + + if (txs.length > 0) { + return nextPath(index + (count - 1)) + } + + return { + balance: transactions.reduce((result, v) => { + result += v.balance + return result + }, 0), + transactions, + } + }) + } + + return nextPath(currentIndex) +} + +export function getHDNode({ xpub58, network }: { xpub58: string, network: Object }) { + return bitcoin.HDNode.fromBase58(xpub58, network) } diff --git a/src/internals/accounts/index.js b/src/internals/accounts/index.js new file mode 100644 index 00000000..3a7b6968 --- /dev/null +++ b/src/internals/accounts/index.js @@ -0,0 +1 @@ +export sync from './sync' diff --git a/src/internals/accounts/sync.js b/src/internals/accounts/sync.js new file mode 100644 index 00000000..f5ccb044 --- /dev/null +++ b/src/internals/accounts/sync.js @@ -0,0 +1,26 @@ +// @flow + +import { getAccount, getHDNode, networks } from 'helpers/btc' + +export default (send: Function) => ({ + all: async ({ accounts }: { accounts: Array }) => { + const network = networks[1] + + send('accounts.sync.progress', null, { kill: false }) + + const syncAccount = ({ id }) => { + const hdnode = getHDNode({ xpub58: id, network }) + return getAccount({ hdnode, network, segwit: true }).then(account => ({ + id, + ...account, + })) + } + + try { + const result = await Promise.all(accounts.map(syncAccount)) + send('accounts.sync.success', result) + } catch (err) { + send('accounts.sync.fail', err.stack || err) + } + }, +}) diff --git a/src/internals/index.js b/src/internals/index.js new file mode 100644 index 00000000..67eabe1b --- /dev/null +++ b/src/internals/index.js @@ -0,0 +1,29 @@ +// @flow + +import objectPath from 'object-path' + +process.title = `ledger-wallet-desktop-${process.env.FORK_TYPE}` + +process.setMaxListeners(Infinity) + +function sendEvent(type: string, data: any, options: Object = { kill: true }) { + process.send({ type, data, options }) +} + +// $FlowFixMe +const func = require(`./${process.env.FORK_TYPE}`) // eslint-disable-line import/no-dynamic-require + +const handlers = Object.keys(func).reduce((result, key) => { + result[key] = func[key](sendEvent) + return result +}, {}) + +process.on('message', payload => { + const { type, data } = payload + + const handler = objectPath.get(handlers, type) + if (!handler) { + return + } + handler(data) +}) diff --git a/src/internals/usb/index.js b/src/internals/usb/index.js index e9a57995..582551f1 100644 --- a/src/internals/usb/index.js +++ b/src/internals/usb/index.js @@ -1,26 +1,2 @@ -// @flow - -import objectPath from 'object-path' - -import devices from './devices' -import wallet from './wallet' - -process.title = 'ledger-wallet-desktop-usb' - -function sendEvent(type: string, data: any, options: Object = { kill: true }) { - process.send({ type, data, options }) -} - -const handlers = { - devices: devices(sendEvent), - wallet: wallet(sendEvent), -} - -process.on('message', payload => { - const { type, data } = payload - const handler = objectPath.get(handlers, type) - if (!handler) { - return - } - handler(data) -}) +export devices from './devices' +export wallet from './wallet' diff --git a/src/internals/usb/wallet.js b/src/internals/usb/wallet.js deleted file mode 100644 index 59433b89..00000000 --- a/src/internals/usb/wallet.js +++ /dev/null @@ -1,27 +0,0 @@ -// @flow - -import CommNodeHid from '@ledgerhq/hw-transport-node-hid' -import Btc from '@ledgerhq/hw-app-btc' - -async function getWalletInfos(path, wallet) { - if (wallet === 'btc') { - const comm = await CommNodeHid.open(path) - const btc = new Btc(comm) - const walletInfos = await btc.getWalletPublicKey(`44'/0'/0'/0`) - return walletInfos - } - throw new Error('invalid wallet') -} - -export default (sendEvent: Function) => ({ - infos: { - request: async ({ path, wallet }: { path: string, wallet: string }) => { - try { - const data = await getWalletInfos(path, wallet) - sendEvent('wallet.infos.success', { path, wallet, data }) - } catch (err) { - sendEvent('wallet.infos.fail', { path, wallet, err: err.stack || err }) - } - }, - }, -}) diff --git a/src/internals/usb/wallet/accounts.js b/src/internals/usb/wallet/accounts.js new file mode 100644 index 00000000..c6202284 --- /dev/null +++ b/src/internals/usb/wallet/accounts.js @@ -0,0 +1,142 @@ +// @flow + +/* eslint-disable no-bitwise */ + +import bitcoin from 'bitcoinjs-lib' +import bs58check from 'bs58check' +import Btc from '@ledgerhq/hw-app-btc' + +import { getAccount, getHDNode, networks } from 'helpers/btc' + +type Coin = 0 | 1 + +function getCompressPublicKey(publicKey) { + let compressedKeyIndex + if (parseInt(publicKey.substring(128, 130), 16) % 2 !== 0) { + compressedKeyIndex = '03' + } else { + compressedKeyIndex = '02' + } + const result = compressedKeyIndex + publicKey.substring(2, 66) + return result +} + +function parseHexString(str: any) { + const result = [] + while (str.length >= 2) { + result.push(parseInt(str.substring(0, 2), 16)) + str = str.substring(2, str.length) + } + return result +} + +function createXpub({ depth, fingerprint, childnum, chainCode, publicKey, network }) { + return [ + network.toString(16).padStart(8, '0'), + depth.toString(16).padStart(2, '0'), + fingerprint.toString(16).padStart(8, '0'), + childnum.toString(16).padStart(8, '0'), + chainCode, + publicKey, + ].join('') +} + +function encodeBase58Check(vchIn) { + vchIn = parseHexString(vchIn) + + return bs58check.encode(Buffer.from(vchIn)) +} + +function getPath({ coin, account, segwit }: { coin: Coin, account?: any, segwit: boolean }) { + return `${segwit ? 49 : 44}'/${coin}'${account !== undefined ? `/${account}'` : ''}` +} + +export default async ({ + transport, + currentAccounts, + onProgress, + coin = 1, + segwit = true, +}: { + transport: Object, + currentAccounts: Array<*>, + onProgress: Function, + coin?: Coin, + segwit?: boolean, +}) => { + const btc = new Btc(transport) + + const network = networks[coin] + + const [p2pkh, p2sh, fam] = [network.pubKeyHash, network.scriptHash, network.family].map(v => + v.toString(16).padStart(4, 0), + ) + + await transport.exchange(`e014000005${p2pkh}${p2sh}${fam.substr(-2)}`, [0x9000]) + + const getPublicKey = path => btc.getWalletPublicKey(path) + + let result = bitcoin.crypto.sha256( + await getPublicKey(getPath({ segwit, coin })).then( + ({ publicKey }) => new Uint8Array(parseHexString(getCompressPublicKey(publicKey))), + ), + ) + result = bitcoin.crypto.ripemd160(result) + + onProgress(null) + + const fingerprint = ((result[0] << 24) | (result[1] << 16) | (result[2] << 8) | result[3]) >>> 0 + + const getXpub58ByAccount = async ({ account, network }) => { + const { publicKey, chainCode } = await getPublicKey(getPath({ segwit, coin, account })) + const compressPublicKey = getCompressPublicKey(publicKey) + + const childnum = (0x80000000 | account) >>> 0 + + const xpub = createXpub({ + depth: 3, + fingerprint, + childnum, + chainCode, + publicKey: compressPublicKey, + network: network.bip32.public, + }) + + return encodeBase58Check(xpub) + } + + const getAllAccounts = async (currentAccount = 0, accounts = {}) => { + const xpub58 = await getXpub58ByAccount({ account: currentAccount, network }) + + if (currentAccounts.includes(xpub58)) { + return getAllAccounts(currentAccount + 1, accounts) // Skip existing account + } + + const hdnode = getHDNode({ xpub58, network }) + const { transactions, balance } = await getAccount({ hdnode, network, segwit }) + + onProgress({ + account: currentAccount, + transactions: transactions.length, + }) + + const hasTransactions = transactions.length > 0 + + // If the first account is empty we still add it + if (currentAccount === 0 || hasTransactions) { + accounts[currentAccount] = { + id: xpub58, + balance, + transactions, + } + } + + if (hasTransactions) { + return getAllAccounts(currentAccount + 1, accounts) + } + + return accounts + } + + return getAllAccounts() +} diff --git a/src/internals/usb/wallet/index.js b/src/internals/usb/wallet/index.js new file mode 100644 index 00000000..f00ab756 --- /dev/null +++ b/src/internals/usb/wallet/index.js @@ -0,0 +1,39 @@ +// @flow + +import CommNodeHid from '@ledgerhq/hw-transport-node-hid' + +import getAllAccounts from './accounts' + +async function getAllAccountsByWallet({ path, wallet, currentAccounts, onProgress }) { + const transport = await CommNodeHid.open(path) + + if (wallet === 'btc') { + return getAllAccounts({ transport, currentAccounts, onProgress }) + } + + throw new Error('invalid wallet') +} + +export default (sendEvent: Function) => ({ + getAccounts: async ({ + path, + wallet, + currentAccounts, + }: { + path: string, + wallet: string, + currentAccounts: Array<*>, + }) => { + try { + const data = await getAllAccountsByWallet({ + path, + wallet, + currentAccounts, + onProgress: progress => sendEvent('wallet.getAccounts.progress', progress, { kill: false }), + }) + sendEvent('wallet.getAccounts.success', data) + } catch (err) { + sendEvent('wallet.getAccounts.fail', err.stack || err) + } + }, +}) diff --git a/src/main/app.js b/src/main/app.js index 0403bd5f..e3651213 100644 --- a/src/main/app.js +++ b/src/main/app.js @@ -29,14 +29,10 @@ function createMainWindow() { window.loadURL(url) - window.on('closed', () => { + window.on('close', () => { mainWindow = null }) - ipcMain.on('renderer-ready', () => { - window.show() - }) - window.webContents.on('devtools-opened', () => { window.focus() setImmediate(() => { @@ -76,4 +72,6 @@ app.on('ready', async () => { } mainWindow = createMainWindow() + + ipcMain.on('renderer-ready', () => mainWindow && mainWindow.show()) }) diff --git a/src/main/bridge.js b/src/main/bridge.js index 06cbdaf5..d1bb2883 100644 --- a/src/main/bridge.js +++ b/src/main/bridge.js @@ -7,11 +7,15 @@ import { resolve } from 'path' import setupAutoUpdater from './autoUpdate' -function onChannelUsb(callType) { +function onForkChannel(forkType, callType) { return (event: any, payload) => { const { type, data } = payload - const compute = fork(resolve(__dirname, `${__DEV__ ? '../../' : './'}dist/internals/usb`)) + const compute = fork(resolve(__dirname, `${__DEV__ ? '../../' : './'}dist/internals`), [], { + env: { + FORK_TYPE: forkType, + }, + }) compute.send({ type, data }) compute.on('message', payload => { @@ -31,9 +35,9 @@ function onChannelUsb(callType) { } } -// Forwards every usb message to usb process -ipcMain.on('usb', onChannelUsb('async')) -ipcMain.on('usb:sync', onChannelUsb('sync')) +// Forwards every `type` messages to another process +ipcMain.on('usb', onForkChannel('usb', 'async')) +ipcMain.on('accounts', onForkChannel('accounts', 'async')) const handlers = { updater: { diff --git a/src/reducers/accounts.js b/src/reducers/accounts.js index f7b6634b..17e2569c 100644 --- a/src/reducers/accounts.js +++ b/src/reducers/accounts.js @@ -1,8 +1,8 @@ // @flow import { handleActions } from 'redux-actions' -import shortid from 'shortid' import get from 'lodash/get' +import reduce from 'lodash/reduce' import type { State } from 'reducers' import type { Account, Accounts, AccountData } from 'types/common' @@ -12,17 +12,12 @@ export type AccountsState = Accounts const state: AccountsState = {} const handlers: Object = { - ADD_ACCOUNT: (state: AccountsState, { payload: account }: { payload: Account }) => { - const id = shortid.generate() - - return { - ...state, - [id]: { - id, - ...account, - }, - } - }, + ADD_ACCOUNT: (state: AccountsState, { payload: account }: { payload: Account }) => ({ + ...state, + [account.id]: { + ...account, + }, + }), FETCH_ACCOUNTS: (state: AccountsState, { payload: accounts }: { payload: Accounts }) => accounts, SET_ACCOUNT_DATA: ( state: AccountsState, @@ -38,6 +33,17 @@ const handlers: Object = { // Selectors +export function getTotalBalance(state: { accounts: AccountsState }) { + return reduce( + state.accounts, + (result, account) => { + result += account.data.balance + return result + }, + 0, + ) +} + export function getAccounts(state: { accounts: AccountsState }) { return state.accounts } diff --git a/src/renderer/events.js b/src/renderer/events.js index eea7325a..231e6357 100644 --- a/src/renderer/events.js +++ b/src/renderer/events.js @@ -4,6 +4,7 @@ import { ipcRenderer } from 'electron' import objectPath from 'object-path' import { updateDevices, addDevice, removeDevice } from 'actions/devices' +import { syncAccount } from 'actions/accounts' import { setUpdateStatus } from 'reducers/update' type MsgPayload = { @@ -13,6 +14,7 @@ type MsgPayload = { // wait a bit before launching update check const CHECK_UPDATE_TIMEOUT = 3e3 +const SYNC_ACCOUNT_TIMEOUT = 1e3 export function sendEvent(channel: string, msgType: string, data: any) { ipcRenderer.send(channel, { @@ -28,8 +30,24 @@ export function sendSyncEvent(channel: string, msgType: string, data: any): any }) } +function syncAccounts(accounts) { + sendEvent('accounts', 'sync.all', { + accounts: Object.entries(accounts).map(([id]: [string, any]) => ({ + id, + })), + }) +} + export default (store: Object) => { const handlers = { + accounts: { + sync: { + success: accounts => { + accounts.forEach(account => store.dispatch(syncAccount(account))) + setTimeout(() => syncAccounts(store.getState().accounts), SYNC_ACCOUNT_TIMEOUT) + }, + }, + }, devices: { update: devices => { store.dispatch(updateDevices(devices)) @@ -58,12 +76,17 @@ export default (store: Object) => { handler(data) }) + const state = store.getState() + // First time, we get all devices sendEvent('usb', 'devices.all') // Start detection when we plug/unplug devices sendEvent('usb', 'devices.listen') + // Start accounts sync + syncAccounts(state.accounts) + if (__PROD__) { // Start check of eventual updates setTimeout(() => sendEvent('msg', 'updater.init'), CHECK_UPDATE_TIMEOUT) diff --git a/src/renderer/index.js b/src/renderer/index.js index f31608fe..532a0a3b 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -9,7 +9,7 @@ import createHistory from 'history/createHashHistory' import createStore from 'renderer/createStore' import events from 'renderer/events' -import { fetchAccounts, syncAccounts } from 'actions/accounts' +import { fetchAccounts } from 'actions/accounts' import { fetchSettings } from 'actions/settings' import { isLocked } from 'reducers/application' @@ -26,17 +26,16 @@ const history = createHistory() const store = createStore(history) const rootNode = document.getElementById('app') -events(store) - store.dispatch(fetchSettings()) const state = store.getState() || {} if (!isLocked(state)) { store.dispatch(fetchAccounts()) - store.dispatch(syncAccounts()) } +events(store) + function r(Comp) { if (rootNode) { render({Comp}, rootNode) diff --git a/src/types/common.js b/src/types/common.js index 1e356976..d1fe6d13 100644 --- a/src/types/common.js +++ b/src/types/common.js @@ -18,7 +18,6 @@ export type Transaction = { // -------------------- Accounts export type AccountData = { - address: string, balance: number, transactions: Array, } @@ -27,7 +26,6 @@ export type Account = { id: string, name: string, type: string, - address: string, data?: AccountData, } diff --git a/webpack/internals.config.js b/webpack/internals.config.js index 9bcc0893..05f886b4 100644 --- a/webpack/internals.config.js +++ b/webpack/internals.config.js @@ -20,7 +20,10 @@ module.exports = webpackMain().then(config => ({ devtool: config.devtool, target: config.target, - entry: dirs(path.resolve(__dirname, '../src/internals')), + entry: { + ...dirs(path.resolve(__dirname, '../src/internals')), + index: path.resolve(__dirname, '../src/internals/index'), + }, resolve: { extensions: config.resolve.extensions, diff --git a/yarn.lock b/yarn.lock index 7573862e..427e4986 100644 --- a/yarn.lock +++ b/yarn.lock @@ -104,9 +104,9 @@ events "^1.1.1" invariant "^2.2.0" -"@storybook/addon-actions@^3.3.9": - version "3.3.9" - resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-3.3.9.tgz#2b191548928467fe1dd26dcba606feafbf182d36" +"@storybook/addon-actions@^3.3.10": + version "3.3.10" + resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-3.3.10.tgz#f3e4b538d8260364c55a3ba1e301a2fab9d8d3f2" dependencies: deep-equal "^1.0.1" global "^4.3.2" @@ -115,9 +115,9 @@ react-inspector "^2.2.2" uuid "^3.1.0" -"@storybook/addon-knobs@^3.3.9": - version "3.3.9" - resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-3.3.9.tgz#855575557868a97c00ce8de8e2e6960f1580f21c" +"@storybook/addon-knobs@^3.3.10": + version "3.3.10" + resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-3.3.10.tgz#25f32cd3d32a1667dc9d8ae484ddfd6223fcffef" dependencies: babel-runtime "^6.26.0" deep-equal "^1.0.1" @@ -131,41 +131,41 @@ react-textarea-autosize "^5.2.1" util-deprecate "^1.0.2" -"@storybook/addon-links@^3.3.9": - version "3.3.9" - resolved "https://registry.yarnpkg.com/@storybook/addon-links/-/addon-links-3.3.9.tgz#13781ac1c21ddfe347ece6ceab8518c8d1f98a0f" +"@storybook/addon-links@^3.3.10": + version "3.3.10" + resolved "https://registry.yarnpkg.com/@storybook/addon-links/-/addon-links-3.3.10.tgz#4e6c1a0b0bf5b18101bc5001b858b33202ae8209" dependencies: - "@storybook/components" "^3.3.9" + "@storybook/components" "^3.3.10" global "^4.3.2" prop-types "^15.5.10" -"@storybook/addon-options@^3.3.9": - version "3.3.9" - resolved "https://registry.yarnpkg.com/@storybook/addon-options/-/addon-options-3.3.9.tgz#8dca85ae5c6713ca13bead0e8c39b23c7e2138c0" +"@storybook/addon-options@^3.3.10": + version "3.3.10" + resolved "https://registry.yarnpkg.com/@storybook/addon-options/-/addon-options-3.3.10.tgz#536796b6223616a4a8b3c2851c7efbe66036bc8a" -"@storybook/addons@^3.3.9": - version "3.3.9" - resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-3.3.9.tgz#356ce7f1de892d88ca4bc5f686d06e07dd8c2108" +"@storybook/addons@^3.3.10": + version "3.3.10" + resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-3.3.10.tgz#8753007d872013d2376ba71b14396eef3159673b" -"@storybook/channel-postmessage@^3.3.9": - version "3.3.9" - resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-3.3.9.tgz#a59220f9ecbdbe05deac6ac4339715aa587d41dd" +"@storybook/channel-postmessage@^3.3.10": + version "3.3.10" + resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-3.3.10.tgz#4f22b5a665d3c95eb61cf41bbb06872009ace7b5" dependencies: - "@storybook/channels" "^3.3.9" + "@storybook/channels" "^3.3.10" global "^4.3.2" json-stringify-safe "^5.0.1" -"@storybook/channels@^3.3.9": - version "3.3.9" - resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-3.3.9.tgz#3116a6c5e441fd057558870b254c34fe3a9fbfb0" +"@storybook/channels@^3.3.10": + version "3.3.10" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-3.3.10.tgz#0b15d47c2ea0cb1c7b735955d74e9d3ca99cdc42" -"@storybook/client-logger@^3.3.9": - version "3.3.9" - resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-3.3.9.tgz#a73e382c383c1bfa6d2ff7fa5cae77cd09efa524" +"@storybook/client-logger@^3.3.10": + version "3.3.10" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-3.3.10.tgz#6f8b85c3dfad229794fee88f930df59b163ee144" -"@storybook/components@^3.3.9": - version "3.3.9" - resolved "https://registry.yarnpkg.com/@storybook/components/-/components-3.3.9.tgz#1f7ced8b10a0e405c1d3fd6fe7ef7b8957ddf89f" +"@storybook/components@^3.3.10": + version "3.3.10" + resolved "https://registry.yarnpkg.com/@storybook/components/-/components-3.3.10.tgz#f213a129ed49de33cdaf116da2c2b662b8eb3ea0" dependencies: glamor "^2.20.40" glamorous "^4.11.2" @@ -179,9 +179,9 @@ "@storybook/react-simple-di" "^1.2.1" babel-runtime "6.x.x" -"@storybook/node-logger@^3.3.9": - version "3.3.9" - resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-3.3.9.tgz#c070ef5ced91b1b1aa7bb3e402855db277ed426b" +"@storybook/node-logger@^3.3.10": + version "3.3.10" + resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-3.3.10.tgz#d9c09a622713ec4726cdd292e798aa98c0503c15" dependencies: chalk "^2.3.0" npmlog "^4.1.2" @@ -211,17 +211,17 @@ dependencies: babel-runtime "^6.5.0" -"@storybook/react@^3.3.9": - version "3.3.9" - resolved "https://registry.yarnpkg.com/@storybook/react/-/react-3.3.9.tgz#2bd203a5b3c5e5fad4a756ca41d78e62cd49b160" - dependencies: - "@storybook/addon-actions" "^3.3.9" - "@storybook/addon-links" "^3.3.9" - "@storybook/addons" "^3.3.9" - "@storybook/channel-postmessage" "^3.3.9" - "@storybook/client-logger" "^3.3.9" - "@storybook/node-logger" "^3.3.9" - "@storybook/ui" "^3.3.9" +"@storybook/react@^3.3.10": + version "3.3.10" + resolved "https://registry.yarnpkg.com/@storybook/react/-/react-3.3.10.tgz#a55f8f804f3f01d76f1b7e8675e818ee4c107324" + dependencies: + "@storybook/addon-actions" "^3.3.10" + "@storybook/addon-links" "^3.3.10" + "@storybook/addons" "^3.3.10" + "@storybook/channel-postmessage" "^3.3.10" + "@storybook/client-logger" "^3.3.10" + "@storybook/node-logger" "^3.3.10" + "@storybook/ui" "^3.3.10" airbnb-js-shims "^1.4.0" autoprefixer "^7.2.3" babel-loader "^7.1.2" @@ -273,11 +273,11 @@ webpack-dev-middleware "^1.12.2" webpack-hot-middleware "^2.21.0" -"@storybook/ui@^3.3.9": - version "3.3.9" - resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-3.3.9.tgz#abb1df557131b174bf3c0879863a309ee85de8e3" +"@storybook/ui@^3.3.10": + version "3.3.10" + resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-3.3.10.tgz#99a83b988b01cde1df61b87a58227a50ed196dd1" dependencies: - "@storybook/components" "^3.3.9" + "@storybook/components" "^3.3.10" "@storybook/mantra-core" "^1.7.2" "@storybook/react-komposer" "^2.0.3" babel-runtime "^6.26.0" @@ -682,6 +682,13 @@ aws4@^1.2.1, aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" +axios@^0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.17.1.tgz#2d8e3e5d0bdbd7327f91bc814f5c57660f81824d" + dependencies: + follow-redirects "^1.2.5" + is-buffer "^1.1.5" + axobject-query@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-0.1.0.tgz#62f59dbc59c9f9242759ca349960e7a2fe3c36c0" @@ -1671,6 +1678,12 @@ balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" +base-x@^3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.4.tgz#94c1788736da065edb1d68808869e357c977fa77" + dependencies: + safe-buffer "^5.0.1" + base64-js@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1" @@ -1705,10 +1718,18 @@ bcryptjs@^2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" +bech32@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.3.tgz#bd47a8986bbb3eec34a56a097a84b8d3e9a2dfcd" + big.js@^3.1.3: version "3.2.0" resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" +bigi@^1.1.0, bigi@^1.4.0: + version "1.4.2" + resolved "https://registry.yarnpkg.com/bigi/-/bigi-1.4.2.tgz#9c665a95f88b8b08fc05cfd731f561859d725825" + binary-extensions@^1.0.0: version "1.11.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" @@ -1724,6 +1745,36 @@ bindings@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7" +bip66@^1.1.0: + version "1.1.5" + resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22" + dependencies: + safe-buffer "^5.0.1" + +bitcoin-ops@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/bitcoin-ops/-/bitcoin-ops-1.4.0.tgz#90d9c95c067e9a34a6ccaa3dd324ffa84e31e3d8" + +bitcoinjs-lib@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-3.3.2.tgz#780c9c53ecb1222adb463b58bef26386067b609a" + dependencies: + bech32 "^1.1.2" + bigi "^1.4.0" + bip66 "^1.1.0" + bitcoin-ops "^1.3.0" + bs58check "^2.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.3" + ecurve "^1.0.0" + merkle-lib "^2.0.10" + pushdata-bitcoin "^1.0.1" + randombytes "^2.0.1" + safe-buffer "^5.0.1" + typeforce "^1.11.3" + varuint-bitcoin "^1.0.4" + wif "^2.0.1" + bl@^1.0.0: version "1.2.1" resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e" @@ -1736,28 +1787,12 @@ block-stream@*: dependencies: inherits "~2.0.0" -blockchain.info@^2.11.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/blockchain.info/-/blockchain.info-2.11.0.tgz#63b46617e194164d377e183e6c667d3ef38ad5b6" - dependencies: - q "^1.4.1" - request-promise "^0.4.3" - url-join "0.0.1" - url-parse "^1.0.5" - url-pattern "^0.10.2" - optionalDependencies: - ws "^1.1.2" - bluebird-lst@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/bluebird-lst/-/bluebird-lst-1.0.5.tgz#bebc83026b7e92a72871a3dc599e219cbfb002a9" dependencies: bluebird "^3.5.1" -bluebird@^2.3: - version "2.11.0" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" - bluebird@^3.4.7, bluebird@^3.5.0, bluebird@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" @@ -1935,6 +1970,19 @@ browserslist@^2.1.2, browserslist@^2.11.1: caniuse-lite "^1.0.30000792" electron-to-chromium "^1.3.30" +bs58@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" + dependencies: + base-x "^3.0.2" + +bs58check@<3.0.0, bs58check@^2.0.0, bs58check@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.1.tgz#8a5d0e587af97b784bf9cbf1b29f454d82bc0222" + dependencies: + bs58 "^4.0.0" + create-hash "^1.1.0" + buffer-indexof@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" @@ -2131,7 +2179,7 @@ chalk@0.5.1: strip-ansi "^0.3.0" supports-color "^0.2.0" -chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: +chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" dependencies: @@ -2583,7 +2631,7 @@ create-hash@^1.1.0, create-hash@^1.1.2: ripemd160 "^2.0.0" sha.js "^2.4.0" -create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.3, create-hmac@^1.1.4: version "1.1.6" resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06" dependencies: @@ -2979,7 +3027,7 @@ doctrine@1.5.0: esutils "^2.0.2" isarray "^1.0.0" -doctrine@^2.0.0, doctrine@^2.0.2: +doctrine@^2.0.0, doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" dependencies: @@ -3080,6 +3128,13 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" +ecurve@^1.0.0: + version "1.0.6" + resolved "https://registry.yarnpkg.com/ecurve/-/ecurve-1.0.6.tgz#dfdabbb7149f8d8b78816be5a7d5b83fcf6de797" + dependencies: + bigi "^1.1.0" + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -3556,9 +3611,9 @@ eslint-visitor-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" -eslint@^4.13.1: - version "4.15.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.15.0.tgz#89ab38c12713eec3d13afac14e4a89e75ef08145" +eslint@^4.16.0: + version "4.16.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.16.0.tgz#934ada9e98715e1d7bbfd6f6f0519ed2fab35cc1" dependencies: ajv "^5.3.0" babel-code-frame "^6.22.0" @@ -3566,7 +3621,7 @@ eslint@^4.13.1: concat-stream "^1.6.0" cross-spawn "^5.1.0" debug "^3.1.0" - doctrine "^2.0.2" + doctrine "^2.1.0" eslint-scope "^3.7.1" eslint-visitor-keys "^1.0.0" espree "^3.5.2" @@ -4024,6 +4079,12 @@ flush-write-stream@^1.0.0: inherits "^2.0.1" readable-stream "^2.0.4" +follow-redirects@^1.2.5: + version "1.4.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.0.tgz#a146a3a5d402201c7a3e6128643f0e336d212b10" + dependencies: + debug "^3.1.0" + for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -5604,7 +5665,7 @@ lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" -lodash@^3.10.0, lodash@^3.10.1: +lodash@^3.10.1: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" @@ -5776,6 +5837,10 @@ merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" +merkle-lib@^2.0.10: + version "2.0.10" + resolved "https://registry.yarnpkg.com/merkle-lib/-/merkle-lib-2.0.10.tgz#82b8dbae75e27a7785388b73f9d7725d0f6f3326" + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -6301,10 +6366,6 @@ optionator@^0.8.2: type-check "~0.3.2" wordwrap "~1.0.0" -options@>=0.0.5: - version "0.0.6" - resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" - ora@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4" @@ -7068,7 +7129,13 @@ punycode@^1.2.4, punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" -q@^1.1.2, q@^1.4.1: +pushdata-bitcoin@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz#15931d3cd967ade52206f523aa7331aef7d43af7" + dependencies: + bitcoin-ops "^1.3.0" + +q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -7703,15 +7770,6 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -request-promise@^0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-0.4.3.tgz#3c8ddc82f06f8908d720aede1d6794258e22121c" - dependencies: - bluebird "^2.3" - chalk "^1.1.0" - lodash "^3.10.0" - request "^2.34" - request@2.81.0: version "2.81.0" resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" @@ -7739,7 +7797,7 @@ request@2.81.0: tunnel-agent "^0.6.0" uuid "^3.0.0" -request@^2.34, request@^2.45.0, request@^2.81.0, request@^2.83.0: +request@^2.45.0, request@^2.81.0, request@^2.83.0: version "2.83.0" resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" dependencies: @@ -8206,6 +8264,12 @@ source-map-support@^0.5.0, source-map-support@^0.5.1: dependencies: source-map "^0.6.0" +source-map-support@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.2.tgz#1a6297fd5b2e762b39688c7fc91233b60984f0a5" + dependencies: + source-map "^0.6.0" + source-map-url@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" @@ -8797,6 +8861,10 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" +typeforce@^1.11.3: + version "1.12.0" + resolved "https://registry.yarnpkg.com/typeforce/-/typeforce-1.12.0.tgz#ca40899919f1466d7819e37be039406beb912a2e" + ua-parser-js@^0.7.9: version "0.7.17" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac" @@ -8853,10 +8921,6 @@ uid-number@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" -ultron@1.0.x: - version "1.0.2" - resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" - union-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" @@ -8954,10 +9018,6 @@ urix@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" -url-join@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/url-join/-/url-join-0.0.1.tgz#1db48ad422d3402469a87f7d97bdebfe4fb1e3c8" - url-loader@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.6.2.tgz#a007a7109620e9d988d14bce677a1decb9a993f7" @@ -8979,17 +9039,13 @@ url-parse@1.0.x: querystringify "0.0.x" requires-port "1.0.x" -url-parse@^1.0.5, url-parse@^1.1.8: +url-parse@^1.1.8: version "1.2.0" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.2.0.tgz#3a19e8aaa6d023ddd27dcc44cb4fc8f7fec23986" dependencies: querystringify "~1.0.0" requires-port "~1.0.0" -url-pattern@^0.10.2: - version "0.10.2" - resolved "https://registry.yarnpkg.com/url-pattern/-/url-pattern-0.10.2.tgz#e9f07104982b72312db4473dd86a527b580015da" - url-to-options@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" @@ -9054,6 +9110,12 @@ value-equal@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7" +varuint-bitcoin@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/varuint-bitcoin/-/varuint-bitcoin-1.1.0.tgz#7a343f50537607af6a3059312b9782a170894540" + dependencies: + safe-buffer "^5.1.1" + vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -9253,6 +9315,12 @@ widest-line@^2.0.0: dependencies: string-width "^2.1.1" +wif@^2.0.1: + version "2.0.6" + resolved "https://registry.yarnpkg.com/wif/-/wif-2.0.6.tgz#08d3f52056c66679299726fade0d432ae74b4704" + dependencies: + bs58check "<3.0.0" + window-size@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" @@ -9301,13 +9369,6 @@ write@^0.2.1: dependencies: mkdirp "^0.5.1" -ws@^1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.5.tgz#cbd9e6e75e09fc5d2c90015f21f0c40875e0dd51" - dependencies: - options ">=0.0.5" - ultron "1.0.x" - xdg-basedir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"