From 038701e8019764319bdeb7dcbeb88f3b2e32685c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=ABck=20V=C3=A9zien?= Date: Fri, 19 Jan 2018 13:33:50 +0100 Subject: [PATCH 1/5] Open modal per defaut on Storybook --- src/components/base/Modal/stories.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ( Date: Fri, 19 Jan 2018 13:39:43 +0100 Subject: [PATCH 2/5] Configure addon-options for Storybook --- .storybook/addons.js | 2 +- .storybook/config.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) 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) From 467e99b0ad807193d9b687c57a9122ebeaea8e56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=ABck=20V=C3=A9zien?= Date: Mon, 22 Jan 2018 10:07:34 +0100 Subject: [PATCH 3/5] WIP --- package.json | 19 +- src/components/modals/AddAccount.js | 6 +- src/internals/usb/wallet.js | 27 --- src/internals/usb/wallet/getAddresses.js | 148 ++++++++++++++++ src/internals/usb/wallet/index.js | 24 +++ yarn.lock | 215 +++++++++++++++++------ 6 files changed, 348 insertions(+), 91 deletions(-) delete mode 100644 src/internals/usb/wallet.js create mode 100644 src/internals/usb/wallet/getAddresses.js create mode 100644 src/internals/usb/wallet/index.js diff --git a/package.json b/package.json index 97e241d8..2b7c518d 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,11 @@ "@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", + "bitcoinjs-lib": "^3.3.2", "blockchain.info": "^2.11.0", + "bs58check": "^2.1.1", "color": "^2.0.1", "downshift": "^1.25.0", "electron-store": "^1.3.0", @@ -61,17 +64,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", @@ -85,7 +88,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/components/modals/AddAccount.js b/src/components/modals/AddAccount.js index 2669b5dd..dd7ed1bf 100644 --- a/src/components/modals/AddAccount.js +++ b/src/components/modals/AddAccount.js @@ -127,16 +127,16 @@ class AddAccountModal extends PureComponent { return } - const { data: { data }, type } = sendSyncEvent('usb', 'wallet.infos.request', { + const { data: { data }, type } = sendSyncEvent('usb', 'wallet.request', { path: currentDevice.path, wallet: inputValue.wallet, }) - if (type === 'wallet.infos.fail') { + if (type === 'wallet.request.fail') { this._timeout = setTimeout(() => this.getWalletInfos(), 1e3) } - if (type === 'wallet.infos.success') { + if (type === 'wallet.request.success') { this.setState({ walletAddress: data.bitcoinAddress, step: 'confirmation', 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/getAddresses.js b/src/internals/usb/wallet/getAddresses.js new file mode 100644 index 00000000..6978bfc8 --- /dev/null +++ b/src/internals/usb/wallet/getAddresses.js @@ -0,0 +1,148 @@ +/* eslint-disable no-bitwise */ + +import axios from 'axios' +import bitcoin from 'bitcoinjs-lib' +import bs58check from 'bs58check' +import Btc from '@ledgerhq/hw-app-btc' + +const networks = [ + { + ...bitcoin.networks.bitcoin, + family: 1, + }, + { + ...bitcoin.networks.testnet, + family: 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) { + 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(new Uint8Array(vchIn)) +} + +function getPath({ coin, account, segwit }) { + return `${segwit ? 49 : 44}'/${coin}'${account !== undefined ? `/${account}'` : ''}` +} + +function 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) +} + +function getPublicAddress(hdnode, path, script, segwit) { + hdnode = hdnode.derivePath(path) + if (!segwit) { + return hdnode.getAddress().toString() + } + return pubKeyToSegwitAddress(hdnode.getPublicKeyBuffer(), script) +} + +function getTransactions(addresses) { + return axios.get( + `http://api.ledgerwallet.com/blockchain/v2/btc_testnet/addresses/${addresses.join( + ',', + )}/transactions?noToken=true`, + ) +} + +export default async transport => { + const coin = 1 + const account = 0 + const segwit = true + + 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 btc = new Btc(transport) + + 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) + + const fingerprint = ((result[0] << 24) | (result[1] << 16) | (result[2] << 8) | result[3]) >>> 0 + + 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, + }) + + const xpub58 = encodeBase58Check(xpub) + + const hdnode = bitcoin.HDNode.fromBase58(xpub58, network) + + const script = segwit ? parseInt(network.scriptHash, 10) : parseInt(network.pubKeyHash, 10) + + const nextPath = async i => { + if (i <= 0x7fffffff) { + for (let j = 0; j < 2; j++) { + const path = `${j}/${i}` + + const address = getPublicAddress(hdnode, path, script, segwit) + console.log('address', address) + + const { data: { txs } } = await getTransactions(address) // eslint-disable-line no-await-in-loop + + console.log('txs', txs.length) + + if (j === 1 && i < 10) { + nextPath(++i) + } + } + } else { + console.log('meeeh') + } + } + + nextPath(0) +} diff --git a/src/internals/usb/wallet/index.js b/src/internals/usb/wallet/index.js new file mode 100644 index 00000000..aee8fe2a --- /dev/null +++ b/src/internals/usb/wallet/index.js @@ -0,0 +1,24 @@ +// @flow + +import CommNodeHid from '@ledgerhq/hw-transport-node-hid' +import getAddresses from './getAddresses' + +async function getWallet(path, wallet) { + const transport = await CommNodeHid.open(path) + console.log('getWallet', path) + if (wallet === 'btc') { + await getAddresses(transport) + } + throw new Error('invalid wallet') +} + +export default (sendEvent: Function) => ({ + request: async ({ path, wallet }: { path: string, wallet: string }) => { + try { + const data = await getWallet(path, wallet) + sendEvent('wallet.request.success', { path, wallet, data }) + } catch (err) { + sendEvent('wallet.request.fail', { path, wallet, err: err.stack || err }) + } + }, +}) diff --git a/yarn.lock b/yarn.lock index ddd2614e..84989a3e 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" @@ -1935,6 +1986,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" @@ -2583,7 +2647,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 +3043,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 +3144,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 +3627,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 +3637,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 +4095,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" @@ -5772,6 +5849,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" @@ -7064,6 +7145,12 @@ punycode@^1.2.4, punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" +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, q@^1.4.1: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -8188,6 +8275,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" @@ -8775,6 +8868,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" @@ -9028,6 +9125,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" @@ -9227,6 +9330,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" From 300c866d5b8c1a6201bf08898fb80930db2e4d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=ABck=20V=C3=A9zien?= Date: Mon, 22 Jan 2018 18:24:37 +0100 Subject: [PATCH 4/5] Add import feature --- flow-defs/process.js | 1 + package.json | 1 - src/actions/accounts.js | 31 +++-- src/components/SideBar/index.js | 2 +- src/components/modals/AddAccount.js | 121 ++++++++++++----- src/helpers/btc.js | 24 +--- .../wallet/{getAddresses.js => accounts.js} | 126 ++++++++++++------ src/internals/usb/wallet/index.js | 31 +++-- src/reducers/accounts.js | 18 +-- src/types/common.js | 2 - yarn.lock | 58 +------- 11 files changed, 231 insertions(+), 184 deletions(-) rename src/internals/usb/wallet/{getAddresses.js => accounts.js} (50%) diff --git a/flow-defs/process.js b/flow-defs/process.js index 06b5a40b..02e1b6a0 100644 --- a/flow-defs/process.js +++ b/flow-defs/process.js @@ -1,6 +1,7 @@ declare var process: { send(args: any): void, on(event: string, args: any): void, + nextTick(callback: Function): void, title: string, env: Object, } diff --git a/package.json b/package.json index 2b7c518d..7c6e070b 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "axios": "^0.17.1", "bcryptjs": "^2.4.3", "bitcoinjs-lib": "^3.3.2", - "blockchain.info": "^2.11.0", "bs58check": "^2.1.1", "color": "^2.0.1", "downshift": "^1.25.0", diff --git a/src/actions/accounts.js b/src/actions/accounts.js index 8d19539f..e6cca59b 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' @@ -10,8 +9,8 @@ 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' +// import { getAccounts } from 'reducers/accounts' +// import { getAddressData } from 'helpers/btc' export type AddAccount = Account => { type: string, payload: Account } export const addAccount: AddAccount = payload => ({ @@ -25,22 +24,22 @@ export const fetchAccounts: FetchAccounts = () => ({ payload: db('accounts'), }) -const setAccountData = createAction('SET_ACCOUNT_DATA', (accountID, data) => ({ accountID, data })) +// 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)) + // 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 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`) } diff --git a/src/components/SideBar/index.js b/src/components/SideBar/index.js index f3ae5bd6..125d30ed 100644 --- a/src/components/SideBar/index.js +++ b/src/components/SideBar/index.js @@ -79,7 +79,7 @@ class SideBar extends PureComponent { {Object.entries(accounts).map(([id, account]: [string, any]) => ( {account.name} diff --git a/src/components/modals/AddAccount.js b/src/components/modals/AddAccount.js index dd7ed1bf..e7e29c9e 100644 --- a/src/components/modals/AddAccount.js +++ b/src/components/modals/AddAccount.js @@ -3,13 +3,15 @@ import React, { PureComponent } from 'react' import styled from 'styled-components' import { connect } from 'react-redux' +import { ipcRenderer } from 'electron' import type { MapStateToProps } from 'react-redux' -import type { Device } from 'types/common' +import type { Accounts, Device } from 'types/common' -import { sendSyncEvent } from 'renderer/events' -import { getCurrentDevice } from 'reducers/devices' import { closeModal } from 'reducers/modals' +import { getAccounts } from 'reducers/accounts' +import { getCurrentDevice } from 'reducers/devices' +import { sendEvent } from 'renderer/events' import { addAccount } from 'actions/accounts' @@ -49,12 +51,34 @@ const Steps = { ), connectDevice: () =>
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 @@ -121,31 +153,21 @@ class AddAccountModal extends PureComponent { 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.request', { + sendEvent('usb', 'wallet.request', { path: currentDevice.path, wallet: inputValue.wallet, + currentAccounts: Object.keys(accounts), }) - - if (type === 'wallet.request.fail') { - this._timeout = setTimeout(() => this.getWalletInfos(), 1e3) - } - - if (type === 'wallet.request.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 +180,57 @@ 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 + componentWillUmount() { + ipcRenderer.removeListener('msg', this.handleWalletRequest) + clearTimeout(this._timeout) + } + + handleWalletRequest = (e, { data, type }) => { + if (type === 'wallet.request.progress') { + this.setState({ + step: 'inProgress', + progress: data, + }) + } + + if (type === 'wallet.request.fail') { + this._timeout = setTimeout(() => this.getWalletInfos(), 1e3) + } + + if (type === 'wallet.request.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..0eff1d1e 100644 --- a/src/helpers/btc.js +++ b/src/helpers/btc.js @@ -1,15 +1,11 @@ -import blockexplorer from 'blockchain.info/blockexplorer' - -const explorer = blockexplorer.usingNetwork(3) - -function computeTransaction(address) { +export function computeTransaction(addresses) { return transaction => { - const outputVal = transaction.out - .filter(o => o.addr === address) + 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, @@ -17,13 +13,3 @@ 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)), - } - return unifiedData -} diff --git a/src/internals/usb/wallet/getAddresses.js b/src/internals/usb/wallet/accounts.js similarity index 50% rename from src/internals/usb/wallet/getAddresses.js rename to src/internals/usb/wallet/accounts.js index 6978bfc8..802515b4 100644 --- a/src/internals/usb/wallet/getAddresses.js +++ b/src/internals/usb/wallet/accounts.js @@ -5,7 +5,9 @@ import bitcoin from 'bitcoinjs-lib' import bs58check from 'bs58check' import Btc from '@ledgerhq/hw-app-btc' -const networks = [ +import { computeTransaction } from 'helpers/btc' + +export const networks = [ { ...bitcoin.networks.bitcoin, family: 1, @@ -36,7 +38,7 @@ function parseHexString(str) { return result } -function createXPUB({ depth, fingerprint, childnum, chainCode, publicKey, network }) { +function createXpub({ depth, fingerprint, childnum, chainCode, publicKey, network }) { return [ network.toString(16).padStart(8, 0), depth.toString(16).padStart(2, 0), @@ -49,7 +51,8 @@ function createXPUB({ depth, fingerprint, childnum, chainCode, publicKey, networ function encodeBase58Check(vchIn) { vchIn = parseHexString(vchIn) - return bs58check.encode(new Uint8Array(vchIn)) + + return bs58check.encode(Buffer.from(vchIn)) } function getPath({ coin, account, segwit }) { @@ -78,10 +81,52 @@ function getTransactions(addresses) { ) } -export default async transport => { - const coin = 1 - const account = 0 - const segwit = true +export async function getAccount({ hdnode, segwit, network }) { + const script = segwit ? parseInt(network.scriptHash, 10) : parseInt(network.pubKeyHash, 10) + + let transactions = [] + + const nextPath = start => { + 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 + start}`), // external chain + getAddress(`1/${v + start}`), // 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(start + (count - 1)) + } + + return { + balance: transactions.reduce((result, v) => { + result += v.balance + return result + }, 0), + transactions, + } + }) + } + + return nextPath(0) +} + +export function getHDNode({ xpub58, network }) { + return bitcoin.HDNode.fromBase58(xpub58, network) +} + +export default async ({ transport, currentAccounts, onProgress, coin = 1, segwit = true }) => { + const btc = new Btc(transport) const network = networks[coin] @@ -91,8 +136,6 @@ export default async transport => { await transport.exchange(`e014000005${p2pkh}${p2sh}${fam.substr(-2)}`, [0x9000]) - const btc = new Btc(transport) - const getPublicKey = path => btc.getWalletPublicKey(path) let result = bitcoin.crypto.sha256( @@ -102,47 +145,54 @@ export default async transport => { ) result = bitcoin.crypto.ripemd160(result) - const fingerprint = ((result[0] << 24) | (result[1] << 16) | (result[2] << 8) | result[3]) >>> 0 + onProgress(null) - const { publicKey, chainCode } = await getPublicKey(getPath({ segwit, coin, account })) - const compressPublicKey = getCompressPublicKey(publicKey) + const fingerprint = ((result[0] << 24) | (result[1] << 16) | (result[2] << 8) | result[3]) >>> 0 - const childnum = (0x80000000 | account) >>> 0 - const xpub = createXPUB({ - depth: 3, - fingerprint, - childnum, - chainCode, - publicKey: compressPublicKey, - network: network.bip32.public, - }) + const getXpub58ByAccount = async ({ account, network }) => { + const { publicKey, chainCode } = await getPublicKey(getPath({ segwit, coin, account })) + const compressPublicKey = getCompressPublicKey(publicKey) - const xpub58 = encodeBase58Check(xpub) + const childnum = (0x80000000 | account) >>> 0 - const hdnode = bitcoin.HDNode.fromBase58(xpub58, network) + const xpub = createXpub({ + depth: 3, + fingerprint, + childnum, + chainCode, + publicKey: compressPublicKey, + network: network.bip32.public, + }) - const script = segwit ? parseInt(network.scriptHash, 10) : parseInt(network.pubKeyHash, 10) + return encodeBase58Check(xpub) + } - const nextPath = async i => { - if (i <= 0x7fffffff) { - for (let j = 0; j < 2; j++) { - const path = `${j}/${i}` + const getAllAccounts = async (currentAccount = 0, accounts = {}) => { + const xpub58 = await getXpub58ByAccount({ account: currentAccount, network }) - const address = getPublicAddress(hdnode, path, script, segwit) - console.log('address', address) + if (currentAccounts.includes(xpub58)) { + return getAllAccounts(currentAccount + 1, accounts) // skip existing account + } - const { data: { txs } } = await getTransactions(address) // eslint-disable-line no-await-in-loop + const hdnode = getHDNode({ xpub58, network }) + const { transactions, balance } = await getAccount({ hdnode, network, segwit }) - console.log('txs', txs.length) + onProgress({ + account: currentAccount, + transactions: transactions.length, + }) - if (j === 1 && i < 10) { - nextPath(++i) - } + if (transactions.length > 0) { + accounts[currentAccount] = { + id: xpub58, + balance, + transactions, } - } else { - console.log('meeeh') + return getAllAccounts(currentAccount + 1, accounts) } + + return accounts } - nextPath(0) + return getAllAccounts() } diff --git a/src/internals/usb/wallet/index.js b/src/internals/usb/wallet/index.js index aee8fe2a..f465ce31 100644 --- a/src/internals/usb/wallet/index.js +++ b/src/internals/usb/wallet/index.js @@ -1,24 +1,39 @@ // @flow import CommNodeHid from '@ledgerhq/hw-transport-node-hid' -import getAddresses from './getAddresses' -async function getWallet(path, wallet) { +import getAllAccounts from './accounts' + +async function getAllAccountsByWallet({ path, wallet, currentAccounts, onProgress }) { const transport = await CommNodeHid.open(path) - console.log('getWallet', path) + if (wallet === 'btc') { - await getAddresses(transport) + return getAllAccounts({ transport, currentAccounts, onProgress }) } + throw new Error('invalid wallet') } export default (sendEvent: Function) => ({ - request: async ({ path, wallet }: { path: string, wallet: string }) => { + request: async ({ + path, + wallet, + currentAccounts, + }: { + path: string, + wallet: string, + currentAccounts: Array<*>, + }) => { try { - const data = await getWallet(path, wallet) - sendEvent('wallet.request.success', { path, wallet, data }) + const data = await getAllAccountsByWallet({ + path, + wallet, + currentAccounts, + onProgress: progress => sendEvent('wallet.request.progress', progress, { kill: false }), + }) + sendEvent('wallet.request.success', data) } catch (err) { - sendEvent('wallet.request.fail', { path, wallet, err: err.stack || err }) + sendEvent('wallet.request.fail', err.stack || err) } }, }) diff --git a/src/reducers/accounts.js b/src/reducers/accounts.js index f7b6634b..c2a3febe 100644 --- a/src/reducers/accounts.js +++ b/src/reducers/accounts.js @@ -1,7 +1,6 @@ // @flow import { handleActions } from 'redux-actions' -import shortid from 'shortid' import get from 'lodash/get' import type { State } from 'reducers' @@ -12,17 +11,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, 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/yarn.lock b/yarn.lock index 84989a3e..3e8a120f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1787,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" @@ -2195,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: @@ -5681,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" @@ -6378,10 +6362,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" @@ -7151,7 +7131,7 @@ pushdata-bitcoin@^1.0.1: dependencies: bitcoin-ops "^1.3.0" -q@^1.1.2, q@^1.4.1: +q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -7772,15 +7752,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" @@ -7808,7 +7779,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: @@ -8928,10 +8899,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" @@ -9029,10 +8996,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" @@ -9054,17 +9017,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" @@ -9384,13 +9343,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" From 5decfccd7f233190cd210a0c9c46698a4c59dbdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=ABck=20V=C3=A9zien?= Date: Tue, 23 Jan 2018 13:59:49 +0100 Subject: [PATCH 5/5] Add sync feature, change AppRegionDrag, add total balance --- flow-defs/globals.js | 2 + flow-defs/module.js | 2 + flow-defs/process.js | 3 + package.json | 9 +- src/actions/accounts.js | 23 +----- src/components/AccountPage.js | 21 +---- src/components/AppRegionDrag.js | 6 +- src/components/DashboardPage.js | 17 ++-- src/components/SideBar/index.js | 8 +- src/components/TopBar.js | 68 ++++++++++++--- src/components/modals/AddAccount.js | 18 ++-- src/helpers/btc.js | 111 ++++++++++++++++++++++++- src/internals/accounts/index.js | 1 + src/internals/accounts/sync.js | 26 ++++++ src/internals/index.js | 29 +++++++ src/internals/usb/index.js | 28 +------ src/internals/usb/wallet/accounts.js | 118 +++++++-------------------- src/internals/usb/wallet/index.js | 8 +- src/main/app.js | 8 +- src/main/bridge.js | 14 ++-- src/reducers/accounts.js | 12 +++ src/renderer/events.js | 23 ++++++ src/renderer/index.js | 7 +- webpack/internals.config.js | 5 +- yarn.lock | 10 +-- 25 files changed, 355 insertions(+), 222 deletions(-) create mode 100644 src/internals/accounts/index.js create mode 100644 src/internals/accounts/sync.js create mode 100644 src/internals/index.js diff --git a/flow-defs/globals.js b/flow-defs/globals.js index ee6bc5da..b37e6093 100644 --- a/flow-defs/globals.js +++ b/flow-defs/globals.js @@ -1,3 +1,5 @@ +/* eslint-disable */ + declare var __DEV__: boolean declare var __PROD__: boolean declare var __ENV__: string 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 02e1b6a0..3a089aeb 100644 --- a/flow-defs/process.js +++ b/flow-defs/process.js @@ -1,7 +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 7c6e070b..3fc889b6 100644 --- a/package.json +++ b/package.json @@ -20,17 +20,16 @@ "storybook": "start-storybook -p 4444" }, "lint-staged": { - "*.js": [ - "eslint --fix", - "prettier --write", - "git add" - ] + "*.js": ["eslint --fix", "prettier --write", "git add"] }, "electronWebpack": { "renderer": { "webpackConfig": "./webpack/renderer.config.js" } }, + "resolutions": { + "webpack-sources": "1.0.1" + }, "dependencies": { "@ledgerhq/hw-app-btc": "^1.1.2-beta.068e2a14", "@ledgerhq/hw-app-eth": "^1.1.2-beta.068e2a14", diff --git a/src/actions/accounts.js b/src/actions/accounts.js index e6cca59b..a0642bbb 100644 --- a/src/actions/accounts.js +++ b/src/actions/accounts.js @@ -7,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 => ({ @@ -24,22 +20,9 @@ export const fetchAccounts: FetchAccounts = () => ({ payload: db('accounts'), }) -// const setAccountData = createAction('SET_ACCOUNT_DATA', (accountID, data) => ({ accountID, data })) +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 125d30ed..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/modals/AddAccount.js b/src/components/modals/AddAccount.js index e7e29c9e..9e9ef74a 100644 --- a/src/components/modals/AddAccount.js +++ b/src/components/modals/AddAccount.js @@ -151,6 +151,11 @@ class AddAccountModal extends PureComponent { } } + componentWillUnmount() { + ipcRenderer.removeListener('msg', this.handleWalletRequest) + clearTimeout(this._timeout) + } + getWalletInfos() { const { inputValue } = this.state const { currentDevice, accounts } = this.props @@ -159,7 +164,7 @@ class AddAccountModal extends PureComponent { return } - sendEvent('usb', 'wallet.request', { + sendEvent('usb', 'wallet.getAccounts', { path: currentDevice.path, wallet: inputValue.wallet, currentAccounts: Object.keys(accounts), @@ -190,24 +195,19 @@ class AddAccountModal extends PureComponent { } } - componentWillUmount() { - ipcRenderer.removeListener('msg', this.handleWalletRequest) - clearTimeout(this._timeout) - } - handleWalletRequest = (e, { data, type }) => { - if (type === 'wallet.request.progress') { + if (type === 'wallet.getAccounts.progress') { this.setState({ step: 'inProgress', progress: data, }) } - if (type === 'wallet.request.fail') { + if (type === 'wallet.getAccounts.fail') { this._timeout = setTimeout(() => this.getWalletInfos(), 1e3) } - if (type === 'wallet.request.success') { + if (type === 'wallet.getAccounts.success') { this.setState({ accounts: data, step: 'listAccounts', diff --git a/src/helpers/btc.js b/src/helpers/btc.js index 0eff1d1e..30269a07 100644 --- a/src/helpers/btc.js +++ b/src/helpers/btc.js @@ -1,5 +1,36 @@ -export function computeTransaction(addresses) { - return transaction => { +// @flow + +import axios from 'axios' +import bitcoin from 'bitcoinjs-lib' +import { formatCurrencyUnit } from 'ledger-wallet-common/lib/data/currency' + +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) @@ -13,3 +44,79 @@ export function computeTransaction(addresses) { } } } + +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) + } + + 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/accounts.js b/src/internals/usb/wallet/accounts.js index 802515b4..c6202284 100644 --- a/src/internals/usb/wallet/accounts.js +++ b/src/internals/usb/wallet/accounts.js @@ -1,22 +1,14 @@ +// @flow + /* eslint-disable no-bitwise */ -import axios from 'axios' import bitcoin from 'bitcoinjs-lib' import bs58check from 'bs58check' import Btc from '@ledgerhq/hw-app-btc' -import { computeTransaction } from 'helpers/btc' +import { getAccount, getHDNode, networks } from 'helpers/btc' -export const networks = [ - { - ...bitcoin.networks.bitcoin, - family: 1, - }, - { - ...bitcoin.networks.testnet, - family: 1, - }, -] +type Coin = 0 | 1 function getCompressPublicKey(publicKey) { let compressedKeyIndex @@ -29,7 +21,7 @@ function getCompressPublicKey(publicKey) { return result } -function parseHexString(str) { +function parseHexString(str: any) { const result = [] while (str.length >= 2) { result.push(parseInt(str.substring(0, 2), 16)) @@ -40,10 +32,10 @@ function parseHexString(str) { 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), + 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('') @@ -55,77 +47,23 @@ function encodeBase58Check(vchIn) { return bs58check.encode(Buffer.from(vchIn)) } -function getPath({ coin, account, segwit }) { +function getPath({ coin, account, segwit }: { coin: Coin, account?: any, segwit: boolean }) { return `${segwit ? 49 : 44}'/${coin}'${account !== undefined ? `/${account}'` : ''}` } -function 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) -} - -function getPublicAddress(hdnode, path, script, segwit) { - hdnode = hdnode.derivePath(path) - if (!segwit) { - return hdnode.getAddress().toString() - } - return pubKeyToSegwitAddress(hdnode.getPublicKeyBuffer(), script) -} - -function getTransactions(addresses) { - return axios.get( - `http://api.ledgerwallet.com/blockchain/v2/btc_testnet/addresses/${addresses.join( - ',', - )}/transactions?noToken=true`, - ) -} - -export async function getAccount({ hdnode, segwit, network }) { - const script = segwit ? parseInt(network.scriptHash, 10) : parseInt(network.pubKeyHash, 10) - - let transactions = [] - - const nextPath = start => { - 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 + start}`), // external chain - getAddress(`1/${v + start}`), // 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(start + (count - 1)) - } - - return { - balance: transactions.reduce((result, v) => { - result += v.balance - return result - }, 0), - transactions, - } - }) - } - - return nextPath(0) -} - -export function getHDNode({ xpub58, network }) { - return bitcoin.HDNode.fromBase58(xpub58, network) -} - -export default async ({ transport, currentAccounts, onProgress, coin = 1, segwit = true }) => { +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] @@ -171,7 +109,7 @@ export default async ({ transport, currentAccounts, onProgress, coin = 1, segwit const xpub58 = await getXpub58ByAccount({ account: currentAccount, network }) if (currentAccounts.includes(xpub58)) { - return getAllAccounts(currentAccount + 1, accounts) // skip existing account + return getAllAccounts(currentAccount + 1, accounts) // Skip existing account } const hdnode = getHDNode({ xpub58, network }) @@ -182,12 +120,18 @@ export default async ({ transport, currentAccounts, onProgress, coin = 1, segwit transactions: transactions.length, }) - if (transactions.length > 0) { + 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) } diff --git a/src/internals/usb/wallet/index.js b/src/internals/usb/wallet/index.js index f465ce31..f00ab756 100644 --- a/src/internals/usb/wallet/index.js +++ b/src/internals/usb/wallet/index.js @@ -15,7 +15,7 @@ async function getAllAccountsByWallet({ path, wallet, currentAccounts, onProgres } export default (sendEvent: Function) => ({ - request: async ({ + getAccounts: async ({ path, wallet, currentAccounts, @@ -29,11 +29,11 @@ export default (sendEvent: Function) => ({ path, wallet, currentAccounts, - onProgress: progress => sendEvent('wallet.request.progress', progress, { kill: false }), + onProgress: progress => sendEvent('wallet.getAccounts.progress', progress, { kill: false }), }) - sendEvent('wallet.request.success', data) + sendEvent('wallet.getAccounts.success', data) } catch (err) { - sendEvent('wallet.request.fail', err.stack || 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 c2a3febe..17e2569c 100644 --- a/src/reducers/accounts.js +++ b/src/reducers/accounts.js @@ -2,6 +2,7 @@ import { handleActions } from 'redux-actions' import get from 'lodash/get' +import reduce from 'lodash/reduce' import type { State } from 'reducers' import type { Account, Accounts, AccountData } from 'types/common' @@ -32,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 b3ea74f3..7666e381 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -8,7 +8,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' @@ -20,17 +20,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/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 3e8a120f..c0de29e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8260,7 +8260,7 @@ source-map@0.5.6: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" -source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1: +source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -9210,12 +9210,12 @@ webpack-merge@^4.1.0: dependencies: lodash "^4.17.4" -webpack-sources@^1.0.1, webpack-sources@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54" +webpack-sources@1.0.1, webpack-sources@^1.0.1, webpack-sources@^1.1.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf" dependencies: source-list-map "^2.0.0" - source-map "~0.6.1" + source-map "~0.5.3" webpack@^3.10.0: version "3.10.0"