Browse Source

Merge pull request #3 from loeck/next

Add USB bridge for multithreading, and a simple way for select device
master
Meriadec Pillet 7 years ago
committed by GitHub
parent
commit
1eb31581e0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      package.json
  2. 14
      src/actions/devices.js
  3. 26
      src/components/Home.js
  4. 119
      src/components/TopBar.js
  5. 31
      src/components/Wrapper.js
  6. 4
      src/components/base/Overlay.js
  7. 2
      src/i18n/en/translation.yml
  8. 29
      src/main/bridge.js
  9. 2
      src/main/index.js
  10. 70
      src/main/ledger.js
  11. 66
      src/main/usb.js
  12. 56
      src/reducers/devices.js
  13. 5
      src/renderer/i18n.js
  14. 16
      src/renderer/initEvents.js
  15. 1
      src/styles/global.js
  16. 5
      src/types/common.js
  17. 17
      yarn.lock

4
package.json

@ -57,7 +57,7 @@
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"electron-builder": "^19.49.0",
"electron-rebuild": "^1.6.0",
"electron-rebuild": "^1.6.1",
"electron-webpack": "1.11.0",
"eslint": "^4.13.1",
"eslint-config-airbnb": "^16.1.0",
@ -69,7 +69,7 @@
"eslint-plugin-react": "^7.5.1",
"flow-bin": "^0.63.1",
"flow-typed": "^2.2.3",
"prettier": "^1.9.2",
"prettier": "^1.10.2",
"react-hot-loader": "^4.0.0-beta.12"
}
}

14
src/actions/devices.js

@ -2,11 +2,11 @@
// eslint-disable import/prefer-default-export
import type { Device } from 'types/common'
import type { Device, Devices } from 'types/common'
type devicesUpdateType = (Array<Device>) => { type: string, payload: Array<Device> }
export const devicesUpdate: devicesUpdateType = payload => ({
type: 'DEVICES_UPDATE',
export type deviceChooseType = (Device | null) => { type: string, payload: Device | null }
export const deviceChoose: deviceChooseType = payload => ({
type: 'DEVICE_CHOOSE',
payload,
})
@ -21,3 +21,9 @@ export const deviceRemove: devicesRemoveType = payload => ({
type: 'DEVICE_REMOVE',
payload,
})
type devicesUpdateType = Devices => { type: string, payload: Devices }
export const devicesUpdate: devicesUpdateType = payload => ({
type: 'DEVICES_UPDATE',
payload,
})

26
src/components/Home.js

@ -1,20 +1,32 @@
// @flow
import React, { PureComponent } from 'react'
import { compose } from 'redux'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import type { MapStateToProps } from 'react-redux'
import type { Device } from 'types/common'
import { getCurrentDevice } from 'reducers/devices'
import Box from 'components/base/Box'
const mapStateToProps: MapStateToProps<*, *, *> = state => ({
currentDevice: getCurrentDevice(state),
})
type Props = {
devices: Array<Object>,
t: (string, ?Object) => string,
currentDevice: Device | null,
}
class Home extends PureComponent<Props> {
render() {
const { devices, t } = this.props
return <div>{t('common.connectedDevices', { count: devices.length })}</div>
const { currentDevice } = this.props
return currentDevice !== null ? (
<Box style={{ wordBreak: 'break-word' }} p={20}>
Your current device: {currentDevice.path}
</Box>
) : null
}
}
export default compose(connect(({ devices }: Props): Object => ({ devices })), translate())(Home)
export default connect(mapStateToProps)(Home)

119
src/components/TopBar.js

@ -1,17 +1,124 @@
// @flow
import React, { PureComponent } from 'react'
import React, { PureComponent, Fragment } from 'react'
import { connect } from 'react-redux'
import type { MapStateToProps, MapDispatchToProps } from 'react-redux'
import type { Device, Devices } from 'types/common'
import type { deviceChooseType } from 'actions/devices'
import { getDevices, getCurrentDevice } from 'reducers/devices'
import { deviceChoose } from 'actions/devices'
import Box from 'components/base/Box'
import Overlay from 'components/base/Overlay'
const mapStateToProps: MapStateToProps<*, *, *> = state => ({
devices: getDevices(state),
currentDevice: getCurrentDevice(state),
})
const mapDispatchToProps: MapDispatchToProps<*, *, *> = {
deviceChoose,
}
type Props = {
devices: Devices,
currentDevice: Device,
deviceChoose: deviceChooseType,
}
type State = {
changeDevice: boolean,
}
const hasDevices = props => props.currentDevice === null && props.devices.length > 0
class TopBar extends PureComponent<Props, State> {
state = {
changeDevice: hasDevices(this.props),
}
componentWillReceiveProps(nextProps) {
if (hasDevices(nextProps) && this.props.currentDevice !== null) {
this.setState({
changeDevice: true,
})
}
}
handleChangeDevice = () => {
const { devices } = this.props
if (devices.length > 0) {
this.setState({
changeDevice: true,
})
}
}
handleSelectDevice = device => () => {
const { deviceChoose } = this.props
deviceChoose(device)
this.setState({
changeDevice: false,
})
}
class TopBar extends PureComponent<{}> {
render() {
const { devices } = this.props
const { changeDevice } = this.state
return (
<Box bg="white" noShrink style={{ height: 60 }}>
{''}
</Box>
<Fragment>
{changeDevice && (
<Overlay p={20}>
{devices.map(device => (
<Box
key={device.path}
color="white"
bg="night"
onClick={this.handleSelectDevice(device)}
>
{device.path}
</Box>
))}
</Overlay>
)}
<Box bg="white" noShrink style={{ height: 60 }} justify="center" align="flex-end">
<CountDevices count={devices.length} onChangeDevice={this.handleChangeDevice} />
</Box>
</Fragment>
)
}
}
export default TopBar
const CountDevices = ({ count, onChangeDevice } = { count: Number, onChangeDevice: Function }) => (
<Box
color="night"
mr={20}
horizontal
flow={10}
onClick={onChangeDevice}
style={{ cursor: 'pointer' }}
>
<Box>
<DeviceIcon height={20} width={20} />
</Box>
<Box>{count}</Box>
</Box>
)
const DeviceIcon = props => (
<svg {...props} viewBox="0 0 19.781 19.781">
<path
d="M14.507 0L9.8 4.706a2.92 2.92 0 0 0-1.991.854l-6.89 6.889a2.93 2.93 0 0 0 0 4.143l2.33 2.33a2.925 2.925 0 0 0 4.141 0l6.89-6.891c.613-.612.895-1.43.851-2.232l4.589-4.588L14.507 0zm.386 8.792a2.927 2.927 0 0 0-.611-.902l-2.33-2.331a2.945 2.945 0 0 0-1.08-.682l3.637-3.636 3.968 3.969-3.584 3.582zm.693-5.381l-.949.949-1.26-1.26.949-.949 1.26 1.26zm1.881 1.882l-.949.949-1.26-1.26.948-.949 1.261 1.26z"
fill="currentColor"
/>
</svg>
)
export default connect(mapStateToProps, mapDispatchToProps)(TopBar)

31
src/components/Wrapper.js

@ -1,34 +1,23 @@
// @flow
import React, { Fragment } from 'react'
import { compose } from 'redux'
import { connect } from 'react-redux'
import React from 'react'
import { Route } from 'react-router'
import { translate } from 'react-i18next'
import Box from 'components/base/Box'
import Overlay from 'components/base/Overlay'
import Home from 'components/Home'
import SideBar from 'components/SideBar'
import TopBar from 'components/TopBar'
const Wrapper = ({ devices, t }: { devices: Array<Object>, t: string => string }) => (
<Fragment>
{devices.length === 0 ? (
<Overlay align="center" justify="center">
<Box color="white">{t('common.connectDevice')}</Box>
</Overlay>
) : (
<Box grow horizontal>
<SideBar />
<Box grow bg="cream">
<TopBar />
<Route path="/" component={Home} />
</Box>
</Box>
)}
</Fragment>
const Wrapper = () => (
<Box grow horizontal>
<SideBar />
<Box grow bg="cream">
<TopBar />
<Route path="/" component={Home} />
</Box>
</Box>
)
export default compose(connect(({ devices }): Object => ({ devices })), translate())(Wrapper)
export default translate()(Wrapper)

4
src/components/base/Overlay.js

@ -3,10 +3,12 @@
import React from 'react'
import styled from 'styled-components'
import { rgba } from 'styles/helpers'
import Box from 'components/base/Box'
const Overlay = styled(({ sticky, ...props }) => <Box sticky {...props} />)`
background-color: ${p => p.theme.colors.night};
background-color: ${p => rgba(p.theme.colors.night, 0.4)};
position: fixed;
`

2
src/i18n/en/translation.yml

@ -1,6 +1,6 @@
common:
ok: Okay
cancel: Cancel
connectDevice: Please connect your device
connectedDevices: You have {{count}} device connected
connectedDevices_0: You don't have device connected
connectedDevices_plural: You have {{count}} devices connected

29
src/main/bridge.js

@ -0,0 +1,29 @@
// @flow
import { fork } from 'child_process'
import { ipcMain } from 'electron'
import { resolve } from 'path'
ipcMain.on('msg', (event: any, payload) => {
const { type, data } = payload
const compute = fork('./usb', {
cwd: resolve(__dirname, './'),
})
const send = (msgType, data) => {
event.sender.send('msg', {
type: msgType,
data,
})
}
compute.send({ type, data })
compute.on('message', payload => {
const { type, data, options = {} } = payload
send(type, data)
if (options.kill) {
compute.kill()
}
})
})

2
src/main/index.js

@ -1,5 +1,5 @@
// @flow
require('../globals')
require('./ledger')
require('./bridge')
require('./app')

70
src/main/ledger.js

@ -1,70 +0,0 @@
// @flow
import { ipcMain } from 'electron'
import { isLedgerDevice } from 'ledgerco/lib/utils'
import ledgerco, { comm_node } from 'ledgerco'
import objectPath from 'object-path'
import HID from 'ledger-node-js-hid'
async function getWalletInfos(path: string, wallet: string) {
if (wallet === 'btc') {
const comm = new comm_node(new HID.HID(path), true, 0, false)
const btc = new ledgerco.btc(comm)
const walletInfos = await btc.getWalletPublicKey_async("44'/0'/0'/0")
return walletInfos
}
throw new Error('invalid wallet')
}
let isListenDevices = false
const handlers = {
devices: {
listen: send => {
if (isListenDevices) {
return
}
isListenDevices = true
HID.listenDevices.start()
HID.listenDevices.events.on(
'add',
device => isLedgerDevice(device) && send('device.add', device),
)
HID.listenDevices.events.on(
'remove',
device => isLedgerDevice(device) && send('device.remove', device),
)
},
all: send => send('devices.update', HID.devices().filter(isLedgerDevice)),
},
requestWalletInfos: async (send, { path, wallet }) => {
try {
const publicKey = await getWalletInfos(path, wallet)
send('receiveWalletInfos', { path, publicKey })
} catch (err) {
send('failWalletInfos', { path, err: err.stack })
}
},
}
ipcMain.on('msg', (event: *, payload) => {
const { type, data } = payload
const handler = objectPath.get(handlers, type)
if (!handler) {
return
}
const send = (msgType: string, data: *) => {
event.sender.send('msg', {
type: msgType,
data,
})
}
handler(send, data)
})

66
src/main/usb.js

@ -0,0 +1,66 @@
process.title = 'ledger-wallet-desktop-usb'
const HID = require('ledger-node-js-hid')
const objectPath = require('object-path')
const { isLedgerDevice } = require('ledgerco/lib/utils')
const ledgerco = require('ledgerco')
function send(type, data, options = { kill: true }) {
process.send({ type, data, options })
}
async function getWalletInfos(path, wallet) {
if (wallet === 'btc') {
const comm = new ledgerco.comm_node(new HID.HID(path), true, 0, false)
const btc = new ledgerco.btc(comm)
const walletInfos = await btc.getWalletPublicKey_async("44'/0'/0'/0")
return walletInfos
}
throw new Error('invalid wallet')
}
let isListenDevices = false
const handlers = {
devices: {
listen: () => {
if (isListenDevices) {
return
}
isListenDevices = true
const handleChangeDevice = eventName => device =>
isLedgerDevice(device) && send(eventName, device, { kill: false })
HID.listenDevices.start()
HID.listenDevices.events.on('add', handleChangeDevice('device.add'))
HID.listenDevices.events.on('remove', handleChangeDevice('device.remove'))
},
all: () => send('devices.update', HID.devices().filter(isLedgerDevice)),
},
wallet: {
infos: {
request: async ({ path, wallet }) => {
try {
const publicKey = await getWalletInfos(path, wallet)
send('wallet.infos.success', { path, publicKey })
} catch (err) {
send('wallet.infos.fail', { path, err: err.stack || err })
}
},
},
},
}
process.on('message', payload => {
const { type, data } = payload
const handler = objectPath.get(handlers, type)
if (!handler) {
return
}
handler(data)
})

56
src/reducers/devices.js

@ -2,13 +2,57 @@
import { handleActions } from 'redux-actions'
const state = []
import type { Device, Devices } from 'types/common'
const handlers = {
DEVICES_UPDATE: (state, { payload: devices }) => devices,
DEVICE_ADD: (state, { payload: device }) =>
[...state, device].filter((v, i, s) => s.findIndex(t => t.path === v.path) === i),
DEVICE_REMOVE: (state, { payload: device }) => state.filter(d => d.path !== device.path),
type stateType = {
currentDevice: Device | null,
devices: Devices,
}
const state = {
currentDevice: null,
devices: [],
}
function setCurrentDevice(state) {
return {
...state,
currentDevice: state.devices.length === 1 ? state.devices[0] : state.currentDevice,
}
}
const handlers: Object = {
DEVICES_UPDATE: (state: stateType, { payload: devices }: { payload: Devices }) =>
setCurrentDevice({
...state,
devices,
}),
DEVICE_ADD: (state: stateType, { payload: device }: { payload: Device }) =>
setCurrentDevice({
...state,
devices: [...state.devices, device].filter(
(v, i, s) => s.findIndex(t => t.path === v.path) === i,
),
}),
DEVICE_REMOVE: (state: stateType, { payload: device }: { payload: Device }) => ({
...state,
currentDevice:
state.currentDevice !== null && state.currentDevice.path === device.path
? null
: state.currentDevice,
devices: state.devices.filter(d => d.path !== device.path),
}),
DEVICE_CHOOSE: (state: stateType, { payload: currentDevice }: { payload: Device }) => ({
...state,
currentDevice,
}),
}
export function getCurrentDevice(state: Object) {
return state.devices.currentDevice
}
export function getDevices(state: Object) {
return state.devices.devices
}
export default handleActions(handlers, state)

5
src/renderer/i18n.js

@ -16,4 +16,9 @@ i18n.use(Backend).init({
},
})
i18n.services.pluralResolver.addRule('en', {
numbers: [0, 1, 'plural'],
plurals: n => Number(n >= 2 ? 2 : n),
})
export default i18n

16
src/renderer/initEvents.js

@ -23,7 +23,7 @@ export default (store: Object) => {
update: devices => {
store.dispatch(devicesUpdate(devices))
if (devices.length) {
send('requestWalletInfos', {
send('wallet.infos.request', {
path: devices[0].path,
wallet: 'btc',
})
@ -34,11 +34,15 @@ export default (store: Object) => {
add: device => store.dispatch(deviceAdd(device)),
remove: device => store.dispatch(deviceRemove(device)),
},
receiveWalletInfos: ({ path, publicKey }) => {
console.log({ path, publicKey })
},
failWalletInfos: ({ path, err }) => {
console.log({ path, err })
wallet: {
infos: {
success: ({ path, publicKey }) => {
console.log({ path, publicKey })
},
fail: ({ path, err }) => {
console.log({ path, err })
},
},
},
}

1
src/styles/global.js

@ -11,7 +11,6 @@ injectGlobal`
font: inherit;
color: inherit;
user-select: none;
cursor: default;
min-width: 0;
}

5
src/types/common.js

@ -3,4 +3,9 @@
export type Device = {
vendorId: string,
productId: string,
path: string,
}
export type Devices = Array<Device>
export type T = (string, ?Object) => string

17
yarn.lock

@ -2263,6 +2263,10 @@ detect-indent@^4.0.0:
dependencies:
repeating "^2.0.0"
detect-libc@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
detect-node@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127"
@ -2518,12 +2522,13 @@ electron-publish@19.52.0:
fs-extra-p "^4.5.0"
mime "^2.1.0"
electron-rebuild@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/electron-rebuild/-/electron-rebuild-1.6.0.tgz#e8d26f4d8e9fe5388df35864b3658e5cfd4dcb7e"
electron-rebuild@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/electron-rebuild/-/electron-rebuild-1.6.1.tgz#3c7ab64db31e5e78ef76fedd7a53aec087b723c5"
dependencies:
colors "^1.1.2"
debug "^2.6.3"
detect-libc "^1.0.3"
fs-extra "^3.0.1"
node-abi "^2.0.0"
node-gyp "^3.6.0"
@ -5487,9 +5492,9 @@ preserve@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
prettier@^1.9.2:
version "1.9.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.9.2.tgz#96bc2132f7a32338e6078aeb29727178c6335827"
prettier@^1.10.2:
version "1.10.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.10.2.tgz#1af8356d1842276a99a5b5529c82dd9e9ad3cc93"
pretty-bytes@^1.0.2:
version "1.0.4"

Loading…
Cancel
Save