Browse Source

Add sync feature, change AppRegionDrag, add total balance

master
Loëck Vézien 7 years ago
parent
commit
5decfccd7f
No known key found for this signature in database GPG Key ID: CBCDCE384E853AC4
  1. 2
      flow-defs/globals.js
  2. 2
      flow-defs/module.js
  3. 3
      flow-defs/process.js
  4. 9
      package.json
  5. 23
      src/actions/accounts.js
  6. 21
      src/components/AccountPage.js
  7. 6
      src/components/AppRegionDrag.js
  8. 17
      src/components/DashboardPage.js
  9. 8
      src/components/SideBar/index.js
  10. 64
      src/components/TopBar.js
  11. 18
      src/components/modals/AddAccount.js
  12. 111
      src/helpers/btc.js
  13. 1
      src/internals/accounts/index.js
  14. 26
      src/internals/accounts/sync.js
  15. 29
      src/internals/index.js
  16. 28
      src/internals/usb/index.js
  17. 118
      src/internals/usb/wallet/accounts.js
  18. 8
      src/internals/usb/wallet/index.js
  19. 8
      src/main/app.js
  20. 14
      src/main/bridge.js
  21. 12
      src/reducers/accounts.js
  22. 23
      src/renderer/events.js
  23. 7
      src/renderer/index.js
  24. 5
      webpack/internals.config.js
  25. 10
      yarn.lock

2
flow-defs/globals.js

@ -1,3 +1,5 @@
/* eslint-disable */
declare var __DEV__: boolean declare var __DEV__: boolean
declare var __PROD__: boolean declare var __PROD__: boolean
declare var __ENV__: string declare var __ENV__: string

2
flow-defs/module.js

@ -1,3 +1,5 @@
/* eslint-disable */
declare var module: { declare var module: {
hot: { hot: {
accept(path: string, callback: () => void): void, accept(path: string, callback: () => void): void,

3
flow-defs/process.js

@ -1,7 +1,10 @@
/* eslint-disable */
declare var process: { declare var process: {
send(args: any): void, send(args: any): void,
on(event: string, args: any): void, on(event: string, args: any): void,
nextTick(callback: Function): void, nextTick(callback: Function): void,
setMaxListeners(any): void,
title: string, title: string,
env: Object, env: Object,
} }

9
package.json

@ -20,17 +20,16 @@
"storybook": "start-storybook -p 4444" "storybook": "start-storybook -p 4444"
}, },
"lint-staged": { "lint-staged": {
"*.js": [ "*.js": ["eslint --fix", "prettier --write", "git add"]
"eslint --fix",
"prettier --write",
"git add"
]
}, },
"electronWebpack": { "electronWebpack": {
"renderer": { "renderer": {
"webpackConfig": "./webpack/renderer.config.js" "webpackConfig": "./webpack/renderer.config.js"
} }
}, },
"resolutions": {
"webpack-sources": "1.0.1"
},
"dependencies": { "dependencies": {
"@ledgerhq/hw-app-btc": "^1.1.2-beta.068e2a14", "@ledgerhq/hw-app-btc": "^1.1.2-beta.068e2a14",
"@ledgerhq/hw-app-eth": "^1.1.2-beta.068e2a14", "@ledgerhq/hw-app-eth": "^1.1.2-beta.068e2a14",

23
src/actions/accounts.js

@ -7,10 +7,6 @@ import type { Dispatch } from 'redux'
import db from 'helpers/db' import db from 'helpers/db'
import type { Account } from 'types/common' 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 type AddAccount = Account => { type: string, payload: Account }
export const addAccount: AddAccount = payload => ({ export const addAccount: AddAccount = payload => ({
@ -24,22 +20,9 @@ export const fetchAccounts: FetchAccounts = () => ({
payload: db('accounts'), 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<*>) => { export const syncAccount: Function = account => async (dispatch: Dispatch<*>) => {
// const { address } = account const { id, ...data } = account
// const addressData = await getAddressData(address) dispatch(setAccountData(id, data))
// 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`)
} }

21
src/components/AccountPage.js

@ -2,11 +2,12 @@
import React, { PureComponent, Fragment } from 'react' import React, { PureComponent, Fragment } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { formatCurrencyUnit } from 'ledger-wallet-common/lib/data/currency'
import type { MapStateToProps } from 'react-redux' import type { MapStateToProps } from 'react-redux'
import type { Account, AccountData } from 'types/common' import type { Account, AccountData } from 'types/common'
import { format } from 'helpers/btc'
import { getAccountById, getAccountData } from 'reducers/accounts' import { getAccountById, getAccountData } from 'reducers/accounts'
import Box, { Card } from 'components/base/Box' import Box, { Card } from 'components/base/Box'
@ -22,20 +23,6 @@ const mapStateToProps: MapStateToProps<*, *, *> = (state, props) => ({
accountData: getAccountData(state, props.match.params.id), 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<Props> { class AccountPage extends PureComponent<Props> {
render() { render() {
const { account, accountData } = this.props const { account, accountData } = this.props
@ -49,7 +36,7 @@ class AccountPage extends PureComponent<Props> {
<Fragment> <Fragment>
<Box horizontal flow={3}> <Box horizontal flow={3}>
<Box flex={1}> <Box flex={1}>
<Card title="Balance">{formatBTC(accountData.balance)}</Card> <Card title="Balance">{format(accountData.balance)}</Card>
</Box> </Box>
<Box flex={1}> <Box flex={1}>
<Card title="Receive" /> <Card title="Receive" />
@ -59,7 +46,7 @@ class AccountPage extends PureComponent<Props> {
{accountData.transactions.map(tr => ( {accountData.transactions.map(tr => (
<Box horizontal key={tr.hash}> <Box horizontal key={tr.hash}>
<Box grow>{'-'}</Box> <Box grow>{'-'}</Box>
<Box>{formatBTC(tr.balance)}</Box> <Box>{format(tr.balance)}</Box>
</Box> </Box>
))} ))}
</Card> </Card>

6
src/components/AppRegionDrag.js

@ -4,10 +4,6 @@ import styled from 'styled-components'
export default styled.div` export default styled.div`
-webkit-app-region: drag; -webkit-app-region: drag;
background: ${p => p.theme.colors.white};
height: 40px; height: 40px;
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: -1;
` `

17
src/components/DashboardPage.js

@ -4,28 +4,25 @@ import React, { PureComponent } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import type { MapStateToProps } 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' import Box from 'components/base/Box'
const mapStateToProps: MapStateToProps<*, *, *> = state => ({ const mapStateToProps: MapStateToProps<*, *, *> = state => ({
currentDevice: getCurrentDevice(state), totalBalance: getTotalBalance(state),
}) })
type Props = { type Props = {
currentDevice: Device | null, totalBalance: number,
} }
class DashboardPage extends PureComponent<Props> { class DashboardPage extends PureComponent<Props> {
render() { render() {
const { currentDevice } = this.props const { totalBalance } = this.props
return currentDevice !== null ? ( return <Box p={20}>Your balance: {format(totalBalance)}</Box>
<Box style={{ wordBreak: 'break-word' }} p={20}>
Your current device: {currentDevice.path}
</Box>
) : null
} }
} }

8
src/components/SideBar/index.js

@ -10,6 +10,7 @@ import type { Accounts } from 'types/common'
import { openModal } from 'reducers/modals' import { openModal } from 'reducers/modals'
import { getAccounts } from 'reducers/accounts' import { getAccounts } from 'reducers/accounts'
import { format } from 'helpers/btc'
import { rgba } from 'styles/helpers' import { rgba } from 'styles/helpers'
import Box, { GrowScroll } from 'components/base/Box' import Box, { GrowScroll } from 'components/base/Box'
@ -29,7 +30,6 @@ const Container = styled(Box).attrs({
noShrink: true, noShrink: true,
})` })`
background-color: ${p => rgba(p.theme.colors[p.bg], process.platform === 'darwin' ? 0.4 : 1)}; background-color: ${p => rgba(p.theme.colors[p.bg], process.platform === 'darwin' ? 0.4 : 1)};
padding-top: 40px;
width: 250px; width: 250px;
` `
@ -77,11 +77,7 @@ class SideBar extends PureComponent<Props> {
<CapsSubtitle>{'Accounts'}</CapsSubtitle> <CapsSubtitle>{'Accounts'}</CapsSubtitle>
<div> <div>
{Object.entries(accounts).map(([id, account]: [string, any]) => ( {Object.entries(accounts).map(([id, account]: [string, any]) => (
<Item <Item linkTo={`/account/${id}`} desc={format(account.data.balance)} key={id}>
linkTo={`/account/${id}`}
desc={`${account.type.toUpperCase()} ${account.data.balance}`}
key={id}
>
{account.name} {account.name}
</Item> </Item>
))} ))}

64
src/components/TopBar.js

@ -2,6 +2,7 @@
import React, { PureComponent, Fragment } from 'react' import React, { PureComponent, Fragment } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { ipcRenderer } from 'electron'
import type { MapStateToProps, MapDispatchToProps } from 'react-redux' import type { MapStateToProps, MapDispatchToProps } from 'react-redux'
import type { Device, Devices } from 'types/common' import type { Device, Devices } from 'types/common'
@ -35,6 +36,10 @@ type Props = {
} }
type State = { type State = {
changeDevice: boolean, changeDevice: boolean,
sync: {
progress: null | boolean,
fail: boolean,
},
} }
const hasDevices = props => props.currentDevice === null && props.devices.length > 0 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<Props, State> { class TopBar extends PureComponent<Props, State> {
state = { state = {
changeDevice: hasDevices(this.props), changeDevice: hasDevices(this.props),
sync: {
progress: null,
fail: false,
},
}
componentDidMount() {
ipcRenderer.on('msg', this.handleAccountSync)
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
@ -52,6 +65,39 @@ class TopBar extends PureComponent<Props, State> {
} }
} }
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 = () => { handleChangeDevice = () => {
const { devices } = this.props const { devices } = this.props
@ -76,7 +122,7 @@ class TopBar extends PureComponent<Props, State> {
render() { render() {
const { devices, hasPassword } = this.props const { devices, hasPassword } = this.props
const { changeDevice } = this.state const { changeDevice, sync } = this.state
return ( return (
<Fragment> <Fragment>
@ -94,17 +140,17 @@ class TopBar extends PureComponent<Props, State> {
))} ))}
</Overlay> </Overlay>
)} )}
<Box <Box bg="white" noShrink style={{ height: 60 }} align="center" horizontal>
bg="white" <Box grow>
noShrink {sync.progress === true
style={{ height: 60 }} ? 'sync...'
justify="flex-end" : sync.fail === true ? 'sync fail :(' : 'sync finish!'}
align="center" </Box>
horizontal <Box justify="flex-end" horizontal>
>
{hasPassword && <LockApplication onLock={this.handleLock} />} {hasPassword && <LockApplication onLock={this.handleLock} />}
<CountDevices count={devices.length} onChangeDevice={this.handleChangeDevice} /> <CountDevices count={devices.length} onChangeDevice={this.handleChangeDevice} />
</Box> </Box>
</Box>
</Fragment> </Fragment>
) )
} }

18
src/components/modals/AddAccount.js

@ -151,6 +151,11 @@ class AddAccountModal extends PureComponent<Props, State> {
} }
} }
componentWillUnmount() {
ipcRenderer.removeListener('msg', this.handleWalletRequest)
clearTimeout(this._timeout)
}
getWalletInfos() { getWalletInfos() {
const { inputValue } = this.state const { inputValue } = this.state
const { currentDevice, accounts } = this.props const { currentDevice, accounts } = this.props
@ -159,7 +164,7 @@ class AddAccountModal extends PureComponent<Props, State> {
return return
} }
sendEvent('usb', 'wallet.request', { sendEvent('usb', 'wallet.getAccounts', {
path: currentDevice.path, path: currentDevice.path,
wallet: inputValue.wallet, wallet: inputValue.wallet,
currentAccounts: Object.keys(accounts), currentAccounts: Object.keys(accounts),
@ -190,24 +195,19 @@ class AddAccountModal extends PureComponent<Props, State> {
} }
} }
componentWillUmount() {
ipcRenderer.removeListener('msg', this.handleWalletRequest)
clearTimeout(this._timeout)
}
handleWalletRequest = (e, { data, type }) => { handleWalletRequest = (e, { data, type }) => {
if (type === 'wallet.request.progress') { if (type === 'wallet.getAccounts.progress') {
this.setState({ this.setState({
step: 'inProgress', step: 'inProgress',
progress: data, progress: data,
}) })
} }
if (type === 'wallet.request.fail') { if (type === 'wallet.getAccounts.fail') {
this._timeout = setTimeout(() => this.getWalletInfos(), 1e3) this._timeout = setTimeout(() => this.getWalletInfos(), 1e3)
} }
if (type === 'wallet.request.success') { if (type === 'wallet.getAccounts.success') {
this.setState({ this.setState({
accounts: data, accounts: data,
step: 'listAccounts', step: 'listAccounts',

111
src/helpers/btc.js

@ -1,5 +1,36 @@
export function computeTransaction(addresses) { // @flow
return transaction => {
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 const outputVal = transaction.outputs
.filter(o => addresses.includes(o.address)) .filter(o => addresses.includes(o.address))
.reduce((acc, cur) => acc + cur.value, 0) .reduce((acc, cur) => acc + cur.value, 0)
@ -13,3 +44,79 @@ export function computeTransaction(addresses) {
} }
} }
} }
export function getTransactions(addresses: Array<string>) {
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)
}

1
src/internals/accounts/index.js

@ -0,0 +1 @@
export sync from './sync'

26
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<Object> }) => {
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)
}
},
})

29
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)
})

28
src/internals/usb/index.js

@ -1,26 +1,2 @@
// @flow export devices from './devices'
export wallet from './wallet'
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)
})

118
src/internals/usb/wallet/accounts.js

@ -1,22 +1,14 @@
// @flow
/* eslint-disable no-bitwise */ /* eslint-disable no-bitwise */
import axios from 'axios'
import bitcoin from 'bitcoinjs-lib' import bitcoin from 'bitcoinjs-lib'
import bs58check from 'bs58check' import bs58check from 'bs58check'
import Btc from '@ledgerhq/hw-app-btc' import Btc from '@ledgerhq/hw-app-btc'
import { computeTransaction } from 'helpers/btc' import { getAccount, getHDNode, networks } from 'helpers/btc'
export const networks = [ type Coin = 0 | 1
{
...bitcoin.networks.bitcoin,
family: 1,
},
{
...bitcoin.networks.testnet,
family: 1,
},
]
function getCompressPublicKey(publicKey) { function getCompressPublicKey(publicKey) {
let compressedKeyIndex let compressedKeyIndex
@ -29,7 +21,7 @@ function getCompressPublicKey(publicKey) {
return result return result
} }
function parseHexString(str) { function parseHexString(str: any) {
const result = [] const result = []
while (str.length >= 2) { while (str.length >= 2) {
result.push(parseInt(str.substring(0, 2), 16)) result.push(parseInt(str.substring(0, 2), 16))
@ -40,10 +32,10 @@ function parseHexString(str) {
function createXpub({ depth, fingerprint, childnum, chainCode, publicKey, network }) { function createXpub({ depth, fingerprint, childnum, chainCode, publicKey, network }) {
return [ return [
network.toString(16).padStart(8, 0), network.toString(16).padStart(8, '0'),
depth.toString(16).padStart(2, 0), depth.toString(16).padStart(2, '0'),
fingerprint.toString(16).padStart(8, 0), fingerprint.toString(16).padStart(8, '0'),
childnum.toString(16).padStart(8, 0), childnum.toString(16).padStart(8, '0'),
chainCode, chainCode,
publicKey, publicKey,
].join('') ].join('')
@ -55,77 +47,23 @@ function encodeBase58Check(vchIn) {
return bs58check.encode(Buffer.from(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}'` : ''}` return `${segwit ? 49 : 44}'/${coin}'${account !== undefined ? `/${account}'` : ''}`
} }
function pubKeyToSegwitAddress(pubKey, scriptVersion) { export default async ({
const script = [0x00, 0x14].concat(Array.from(bitcoin.crypto.hash160(pubKey))) transport,
const hash160 = bitcoin.crypto.hash160(new Uint8Array(script)) currentAccounts,
return bitcoin.address.toBase58Check(hash160, scriptVersion) onProgress,
} coin = 1,
segwit = true,
function getPublicAddress(hdnode, path, script, segwit) { }: {
hdnode = hdnode.derivePath(path) transport: Object,
if (!segwit) { currentAccounts: Array<*>,
return hdnode.getAddress().toString() onProgress: Function,
} coin?: Coin,
return pubKeyToSegwitAddress(hdnode.getPublicKeyBuffer(), script) segwit?: boolean,
} }) => {
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 }) => {
const btc = new Btc(transport) const btc = new Btc(transport)
const network = networks[coin] const network = networks[coin]
@ -171,7 +109,7 @@ export default async ({ transport, currentAccounts, onProgress, coin = 1, segwit
const xpub58 = await getXpub58ByAccount({ account: currentAccount, network }) const xpub58 = await getXpub58ByAccount({ account: currentAccount, network })
if (currentAccounts.includes(xpub58)) { 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 }) const hdnode = getHDNode({ xpub58, network })
@ -182,12 +120,18 @@ export default async ({ transport, currentAccounts, onProgress, coin = 1, segwit
transactions: transactions.length, 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] = { accounts[currentAccount] = {
id: xpub58, id: xpub58,
balance, balance,
transactions, transactions,
} }
}
if (hasTransactions) {
return getAllAccounts(currentAccount + 1, accounts) return getAllAccounts(currentAccount + 1, accounts)
} }

8
src/internals/usb/wallet/index.js

@ -15,7 +15,7 @@ async function getAllAccountsByWallet({ path, wallet, currentAccounts, onProgres
} }
export default (sendEvent: Function) => ({ export default (sendEvent: Function) => ({
request: async ({ getAccounts: async ({
path, path,
wallet, wallet,
currentAccounts, currentAccounts,
@ -29,11 +29,11 @@ export default (sendEvent: Function) => ({
path, path,
wallet, wallet,
currentAccounts, 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) { } catch (err) {
sendEvent('wallet.request.fail', err.stack || err) sendEvent('wallet.getAccounts.fail', err.stack || err)
} }
}, },
}) })

8
src/main/app.js

@ -29,14 +29,10 @@ function createMainWindow() {
window.loadURL(url) window.loadURL(url)
window.on('closed', () => { window.on('close', () => {
mainWindow = null mainWindow = null
}) })
ipcMain.on('renderer-ready', () => {
window.show()
})
window.webContents.on('devtools-opened', () => { window.webContents.on('devtools-opened', () => {
window.focus() window.focus()
setImmediate(() => { setImmediate(() => {
@ -76,4 +72,6 @@ app.on('ready', async () => {
} }
mainWindow = createMainWindow() mainWindow = createMainWindow()
ipcMain.on('renderer-ready', () => mainWindow && mainWindow.show())
}) })

14
src/main/bridge.js

@ -7,11 +7,15 @@ import { resolve } from 'path'
import setupAutoUpdater from './autoUpdate' import setupAutoUpdater from './autoUpdate'
function onChannelUsb(callType) { function onForkChannel(forkType, callType) {
return (event: any, payload) => { return (event: any, payload) => {
const { type, data } = 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.send({ type, data })
compute.on('message', payload => { compute.on('message', payload => {
@ -31,9 +35,9 @@ function onChannelUsb(callType) {
} }
} }
// Forwards every usb message to usb process // Forwards every `type` messages to another process
ipcMain.on('usb', onChannelUsb('async')) ipcMain.on('usb', onForkChannel('usb', 'async'))
ipcMain.on('usb:sync', onChannelUsb('sync')) ipcMain.on('accounts', onForkChannel('accounts', 'async'))
const handlers = { const handlers = {
updater: { updater: {

12
src/reducers/accounts.js

@ -2,6 +2,7 @@
import { handleActions } from 'redux-actions' import { handleActions } from 'redux-actions'
import get from 'lodash/get' import get from 'lodash/get'
import reduce from 'lodash/reduce'
import type { State } from 'reducers' import type { State } from 'reducers'
import type { Account, Accounts, AccountData } from 'types/common' import type { Account, Accounts, AccountData } from 'types/common'
@ -32,6 +33,17 @@ const handlers: Object = {
// Selectors // 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 }) { export function getAccounts(state: { accounts: AccountsState }) {
return state.accounts return state.accounts
} }

23
src/renderer/events.js

@ -4,6 +4,7 @@ import { ipcRenderer } from 'electron'
import objectPath from 'object-path' import objectPath from 'object-path'
import { updateDevices, addDevice, removeDevice } from 'actions/devices' import { updateDevices, addDevice, removeDevice } from 'actions/devices'
import { syncAccount } from 'actions/accounts'
import { setUpdateStatus } from 'reducers/update' import { setUpdateStatus } from 'reducers/update'
type MsgPayload = { type MsgPayload = {
@ -13,6 +14,7 @@ type MsgPayload = {
// wait a bit before launching update check // wait a bit before launching update check
const CHECK_UPDATE_TIMEOUT = 3e3 const CHECK_UPDATE_TIMEOUT = 3e3
const SYNC_ACCOUNT_TIMEOUT = 1e3
export function sendEvent(channel: string, msgType: string, data: any) { export function sendEvent(channel: string, msgType: string, data: any) {
ipcRenderer.send(channel, { 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) => { export default (store: Object) => {
const handlers = { const handlers = {
accounts: {
sync: {
success: accounts => {
accounts.forEach(account => store.dispatch(syncAccount(account)))
setTimeout(() => syncAccounts(store.getState().accounts), SYNC_ACCOUNT_TIMEOUT)
},
},
},
devices: { devices: {
update: devices => { update: devices => {
store.dispatch(updateDevices(devices)) store.dispatch(updateDevices(devices))
@ -58,12 +76,17 @@ export default (store: Object) => {
handler(data) handler(data)
}) })
const state = store.getState()
// First time, we get all devices // First time, we get all devices
sendEvent('usb', 'devices.all') sendEvent('usb', 'devices.all')
// Start detection when we plug/unplug devices // Start detection when we plug/unplug devices
sendEvent('usb', 'devices.listen') sendEvent('usb', 'devices.listen')
// Start accounts sync
syncAccounts(state.accounts)
if (__PROD__) { if (__PROD__) {
// Start check of eventual updates // Start check of eventual updates
setTimeout(() => sendEvent('msg', 'updater.init'), CHECK_UPDATE_TIMEOUT) setTimeout(() => sendEvent('msg', 'updater.init'), CHECK_UPDATE_TIMEOUT)

7
src/renderer/index.js

@ -8,7 +8,7 @@ import createHistory from 'history/createHashHistory'
import createStore from 'renderer/createStore' import createStore from 'renderer/createStore'
import events from 'renderer/events' import events from 'renderer/events'
import { fetchAccounts, syncAccounts } from 'actions/accounts' import { fetchAccounts } from 'actions/accounts'
import { fetchSettings } from 'actions/settings' import { fetchSettings } from 'actions/settings'
import { isLocked } from 'reducers/application' import { isLocked } from 'reducers/application'
@ -20,17 +20,16 @@ const history = createHistory()
const store = createStore(history) const store = createStore(history)
const rootNode = document.getElementById('app') const rootNode = document.getElementById('app')
events(store)
store.dispatch(fetchSettings()) store.dispatch(fetchSettings())
const state = store.getState() || {} const state = store.getState() || {}
if (!isLocked(state)) { if (!isLocked(state)) {
store.dispatch(fetchAccounts()) store.dispatch(fetchAccounts())
store.dispatch(syncAccounts())
} }
events(store)
function r(Comp) { function r(Comp) {
if (rootNode) { if (rootNode) {
render(<AppContainer>{Comp}</AppContainer>, rootNode) render(<AppContainer>{Comp}</AppContainer>, rootNode)

5
webpack/internals.config.js

@ -20,7 +20,10 @@ module.exports = webpackMain().then(config => ({
devtool: config.devtool, devtool: config.devtool,
target: config.target, 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: { resolve: {
extensions: config.resolve.extensions, extensions: config.resolve.extensions,

10
yarn.lock

@ -8260,7 +8260,7 @@ source-map@0.5.6:
version "0.5.6" version "0.5.6"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" 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" version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
@ -9210,12 +9210,12 @@ webpack-merge@^4.1.0:
dependencies: dependencies:
lodash "^4.17.4" lodash "^4.17.4"
webpack-sources@^1.0.1, webpack-sources@^1.1.0: webpack-sources@1.0.1, webpack-sources@^1.0.1, webpack-sources@^1.1.0:
version "1.1.0" version "1.0.1"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf"
dependencies: dependencies:
source-list-map "^2.0.0" source-list-map "^2.0.0"
source-map "~0.6.1" source-map "~0.5.3"
webpack@^3.10.0: webpack@^3.10.0:
version "3.10.0" version "3.10.0"

Loading…
Cancel
Save