14 changed files with 338 additions and 549 deletions
@ -1,184 +0,0 @@ |
|||||
// @flow
|
|
||||
|
|
||||
import React, { PureComponent } from 'react' |
|
||||
import { connect } from 'react-redux' |
|
||||
import styled from 'styled-components' |
|
||||
import { ipcRenderer } from 'electron' |
|
||||
import type { Account } from '@ledgerhq/live-common/lib/types' |
|
||||
|
|
||||
import type { Device } from 'types/common' |
|
||||
|
|
||||
import { getCurrentDevice } from 'reducers/devices' |
|
||||
import { sendEvent } from 'renderer/events' |
|
||||
|
|
||||
import Box from 'components/base/Box' |
|
||||
import Button from 'components/base/Button' |
|
||||
import CopyToClipboard from 'components/base/CopyToClipboard' |
|
||||
import Print from 'components/base/Print' |
|
||||
import QRCode from 'components/base/QRCode' |
|
||||
import Text from 'components/base/Text' |
|
||||
|
|
||||
export const AddressBox = styled(Box).attrs({ |
|
||||
bg: 'lightGrey', |
|
||||
p: 2, |
|
||||
})` |
|
||||
border-radius: ${p => p.theme.radii[1]}px; |
|
||||
border: 1px solid ${p => p.theme.colors.fog}; |
|
||||
cursor: text; |
|
||||
text-align: center; |
|
||||
user-select: text; |
|
||||
word-break: break-all; |
|
||||
` |
|
||||
|
|
||||
const Action = styled(Box).attrs({ |
|
||||
alignItems: 'center', |
|
||||
color: 'fog', |
|
||||
flex: 1, |
|
||||
flow: 1, |
|
||||
fontSize: 0, |
|
||||
})` |
|
||||
font-weight: bold; |
|
||||
text-align: center; |
|
||||
cursor: pointer; |
|
||||
text-transform: uppercase; |
|
||||
|
|
||||
&:hover { |
|
||||
color: ${p => p.theme.colors.grey}; |
|
||||
} |
|
||||
` |
|
||||
|
|
||||
const mapStateToProps = state => ({ |
|
||||
currentDevice: getCurrentDevice(state), |
|
||||
}) |
|
||||
|
|
||||
type Props = { |
|
||||
currentDevice: Device | null, |
|
||||
account: Account, |
|
||||
amount?: string, |
|
||||
} |
|
||||
|
|
||||
type State = { |
|
||||
isVerified: null | boolean, |
|
||||
isDisplay: boolean, |
|
||||
} |
|
||||
|
|
||||
const defaultState = { |
|
||||
isVerified: null, |
|
||||
isDisplay: false, |
|
||||
} |
|
||||
|
|
||||
class ReceiveBox extends PureComponent<Props, State> { |
|
||||
static defaultProps = { |
|
||||
amount: undefined, |
|
||||
} |
|
||||
|
|
||||
state = { |
|
||||
...defaultState, |
|
||||
} |
|
||||
|
|
||||
componentDidMount() { |
|
||||
ipcRenderer.on('msg', this.handleMsgEvent) |
|
||||
} |
|
||||
|
|
||||
componentWillReceiveProps(nextProps: Props) { |
|
||||
if (this.props.account !== nextProps.account) { |
|
||||
this.setState({ |
|
||||
...defaultState, |
|
||||
}) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
componentWillUnmount() { |
|
||||
ipcRenderer.removeListener('msg', this.handleMsgEvent) |
|
||||
this.setState({ |
|
||||
...defaultState, |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
handleMsgEvent = (e, { type }) => { |
|
||||
if (type === 'wallet.verifyAddress.success') { |
|
||||
this.setState({ |
|
||||
isVerified: true, |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
if (type === 'wallet.verifyAddress.fail') { |
|
||||
this.setState({ |
|
||||
isVerified: false, |
|
||||
}) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
handleVerifyAddress = () => { |
|
||||
const { currentDevice, account } = this.props |
|
||||
|
|
||||
if (currentDevice !== null) { |
|
||||
sendEvent('usb', 'wallet.verifyAddress', { |
|
||||
pathDevice: currentDevice.path, |
|
||||
path: `${account.walletPath}${account.path}`, |
|
||||
}) |
|
||||
|
|
||||
this.setState({ |
|
||||
isDisplay: true, |
|
||||
}) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
const { amount, account } = this.props |
|
||||
const { isVerified, isDisplay } = this.state |
|
||||
|
|
||||
if (!isDisplay) { |
|
||||
return ( |
|
||||
<Box grow alignItems="center" justifyContent="center"> |
|
||||
<Button onClick={this.handleVerifyAddress}>Display address on device</Button> |
|
||||
</Box> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
const { address } = account |
|
||||
|
|
||||
return ( |
|
||||
<Box flow={3}> |
|
||||
<Box> |
|
||||
isVerified:{' '} |
|
||||
{isVerified === null |
|
||||
? 'not yet...' |
|
||||
: isVerified === true |
|
||||
? 'ok!' |
|
||||
: '/!\\ contact support'} |
|
||||
</Box> |
|
||||
<Box alignItems="center"> |
|
||||
<QRCode size={150} data={`bitcoin:${address}${amount ? `?amount=${amount}` : ''}`} /> |
|
||||
</Box> |
|
||||
<Box alignItems="center" flow={2}> |
|
||||
<Text fontSize={1}>{'Current address'}</Text> |
|
||||
<AddressBox>{address}</AddressBox> |
|
||||
</Box> |
|
||||
<Box horizontal> |
|
||||
<CopyToClipboard |
|
||||
data={address} |
|
||||
render={copy => ( |
|
||||
<Action onClick={copy}> |
|
||||
<span>{'Copy'}</span> |
|
||||
</Action> |
|
||||
)} |
|
||||
/> |
|
||||
<Print |
|
||||
data={{ address, amount }} |
|
||||
render={(print, isLoading) => ( |
|
||||
<Action onClick={print}> |
|
||||
<span>{isLoading ? '...' : 'Print'}</span> |
|
||||
</Action> |
|
||||
)} |
|
||||
/> |
|
||||
<Action> |
|
||||
<span>{'Share'}</span> |
|
||||
</Action> |
|
||||
</Box> |
|
||||
</Box> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default connect(mapStateToProps, null)(ReceiveBox) |
|
@ -0,0 +1,118 @@ |
|||||
|
// @flow
|
||||
|
import { ipcRenderer } from 'electron' |
||||
|
import { Observable } from 'rxjs' |
||||
|
import uuidv4 from 'uuid/v4' |
||||
|
|
||||
|
type Msg<A> = { |
||||
|
type: string, |
||||
|
data?: A, |
||||
|
options?: *, |
||||
|
} |
||||
|
|
||||
|
function send<A>(msg: Msg<A>) { |
||||
|
process.send(msg) |
||||
|
} |
||||
|
|
||||
|
export class Command<In, A> { |
||||
|
channel: string |
||||
|
type: string |
||||
|
id: string |
||||
|
impl: In => Observable<A> |
||||
|
constructor(channel: string, type: string, impl: In => Observable<A>) { |
||||
|
this.channel = channel |
||||
|
this.type = type |
||||
|
this.id = `${channel}.${type}` |
||||
|
this.impl = impl |
||||
|
} |
||||
|
|
||||
|
// ~~~ On exec side we can:
|
||||
|
|
||||
|
exec(data: In, requestId: string) { |
||||
|
return this.impl(data).subscribe({ |
||||
|
next: (data: A) => { |
||||
|
send({ |
||||
|
type: `NEXT_${requestId}`, |
||||
|
data, |
||||
|
}) |
||||
|
}, |
||||
|
complete: () => { |
||||
|
send({ |
||||
|
type: `COMPLETE_${requestId}`, |
||||
|
options: { kill: true }, |
||||
|
}) |
||||
|
}, |
||||
|
error: error => { |
||||
|
send({ |
||||
|
type: `ERROR_${requestId}`, |
||||
|
data: { |
||||
|
name: error && error.name, |
||||
|
message: error && error.message, |
||||
|
}, |
||||
|
options: { kill: true }, |
||||
|
}) |
||||
|
}, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// ~~~ On renderer side we can:
|
||||
|
|
||||
|
/** |
||||
|
* Usage example: |
||||
|
* sub = send(data).subscribe({ next: ... }) |
||||
|
* // or
|
||||
|
* const res = await send(data).toPromise() |
||||
|
*/ |
||||
|
send(data: In): Observable<A> { |
||||
|
return Observable.create(o => { |
||||
|
const { channel, type, id } = this |
||||
|
const requestId: string = uuidv4() |
||||
|
|
||||
|
const unsubscribe = () => { |
||||
|
ipcRenderer.removeListener('msg', handleMsgEvent) |
||||
|
} |
||||
|
|
||||
|
function handleMsgEvent(e, msg: Msg<A>) { |
||||
|
switch (msg.type) { |
||||
|
case `NEXT_${requestId}`: |
||||
|
if (msg.data) { |
||||
|
o.next(msg.data) |
||||
|
} |
||||
|
break |
||||
|
|
||||
|
case `COMPLETE_${requestId}`: |
||||
|
o.complete() |
||||
|
unsubscribe() |
||||
|
break |
||||
|
|
||||
|
case `ERROR_${requestId}`: |
||||
|
o.error(msg.data) |
||||
|
unsubscribe() |
||||
|
break |
||||
|
|
||||
|
default: |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ipcRenderer.on('msg', handleMsgEvent) |
||||
|
|
||||
|
ipcRenderer.send(channel, { |
||||
|
type, |
||||
|
data: { |
||||
|
id, |
||||
|
data, |
||||
|
requestId, |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
return unsubscribe |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function createCommand<In, A>( |
||||
|
channel: string, |
||||
|
type: string, |
||||
|
impl: In => Observable<A>, |
||||
|
): Command<In, A> { |
||||
|
return new Command(channel, type, impl) |
||||
|
} |
@ -1,39 +0,0 @@ |
|||||
// @flow
|
|
||||
|
|
||||
/* eslint-disable no-bitwise */ |
|
||||
|
|
||||
import Btc from '@ledgerhq/hw-app-btc' |
|
||||
|
|
||||
export async function getFreshReceiveAddress({ |
|
||||
currencyId, |
|
||||
accountIndex, |
|
||||
}: { |
|
||||
currencyId: string, |
|
||||
accountIndex: number, |
|
||||
}) { |
|
||||
// TODO: investigate why importing it on file scope causes trouble
|
|
||||
const core = require('init-ledger-core')() |
|
||||
|
|
||||
const wallet = await core.getWallet(currencyId) |
|
||||
const account = await wallet.getAccount(accountIndex) |
|
||||
const addresses = await account.getFreshPublicAddresses() |
|
||||
if (!addresses.length) { |
|
||||
throw new Error('No fresh addresses') |
|
||||
} |
|
||||
return addresses[0] |
|
||||
} |
|
||||
|
|
||||
export function verifyAddress({ |
|
||||
transport, |
|
||||
path, |
|
||||
segwit = true, |
|
||||
}: { |
|
||||
transport: Object, |
|
||||
path: string, |
|
||||
segwit?: boolean, |
|
||||
}) { |
|
||||
console.warn('DEPRECATED use devices.getAddress with verify option') |
|
||||
const btc = new Btc(transport) |
|
||||
|
|
||||
return btc.getWalletPublicKey(path, true, segwit) |
|
||||
} |
|
@ -1,36 +0,0 @@ |
|||||
// @flow
|
|
||||
|
|
||||
import invariant from 'invariant' |
|
||||
import CommNodeHid from '@ledgerhq/hw-transport-node-hid' |
|
||||
import type Transport from '@ledgerhq/hw-transport' |
|
||||
import type { IPCSend } from 'types/electron' |
|
||||
import getAddressForCurrency from './getAddressForCurrency' |
|
||||
|
|
||||
export default async ( |
|
||||
send: IPCSend, |
|
||||
{ |
|
||||
currencyId, |
|
||||
devicePath, |
|
||||
path, |
|
||||
accountAddress, |
|
||||
...options |
|
||||
}: { |
|
||||
currencyId: string, |
|
||||
devicePath: string, |
|
||||
path: string, |
|
||||
accountAddress: ?string, |
|
||||
}, |
|
||||
) => { |
|
||||
try { |
|
||||
invariant(currencyId, 'currencyId "%s" not defined', currencyId) |
|
||||
const transport: Transport<*> = await CommNodeHid.open(devicePath) |
|
||||
const resolver = getAddressForCurrency(currencyId) |
|
||||
const { address } = await resolver(transport, currencyId, path, options) |
|
||||
if (accountAddress && address !== accountAddress) { |
|
||||
throw new Error('Account address is different than device address') |
|
||||
} |
|
||||
send('devices.ensureDeviceApp.success', { devicePath }) |
|
||||
} catch (err) { |
|
||||
send('devices.ensureDeviceApp.fail', { devicePath, message: err.message }) |
|
||||
} |
|
||||
} |
|
@ -1,32 +1,33 @@ |
|||||
// @flow
|
// @flow
|
||||
|
|
||||
import invariant from 'invariant' |
import { createCommand, Command } from 'helpers/ipc' |
||||
|
import { fromPromise } from 'rxjs/observable/fromPromise' |
||||
import CommNodeHid from '@ledgerhq/hw-transport-node-hid' |
import CommNodeHid from '@ledgerhq/hw-transport-node-hid' |
||||
import type Transport from '@ledgerhq/hw-transport' |
|
||||
import type { IPCSend } from 'types/electron' |
|
||||
import getAddressForCurrency from './getAddressForCurrency' |
import getAddressForCurrency from './getAddressForCurrency' |
||||
|
|
||||
export default async ( |
type Input = { |
||||
send: IPCSend, |
currencyId: string, |
||||
{ |
devicePath: string, |
||||
currencyId, |
path: string, |
||||
devicePath, |
verify?: boolean, |
||||
path, |
segwit?: boolean, |
||||
...options |
|
||||
}: { |
|
||||
currencyId: string, |
|
||||
devicePath: string, |
|
||||
path: string, |
|
||||
verify?: boolean, |
|
||||
}, |
|
||||
) => { |
|
||||
try { |
|
||||
invariant(currencyId, 'currencyId "%s" not defined', currencyId) |
|
||||
const transport: Transport<*> = await CommNodeHid.open(devicePath) |
|
||||
const resolver = getAddressForCurrency(currencyId) |
|
||||
const res = await resolver(transport, currencyId, path, options) |
|
||||
send('devices.getAddress.success', res) |
|
||||
} catch (err) { |
|
||||
send('devices.getAddress.fail', { message: err.message }) |
|
||||
} |
|
||||
} |
} |
||||
|
|
||||
|
type Result = { |
||||
|
address: string, |
||||
|
path: string, |
||||
|
publicKey: string, |
||||
|
} |
||||
|
|
||||
|
const cmd: Command<Input, Result> = createCommand( |
||||
|
'devices', |
||||
|
'getAddress', |
||||
|
({ currencyId, devicePath, path, ...options }) => |
||||
|
fromPromise( |
||||
|
CommNodeHid.open(devicePath).then(transport => |
||||
|
getAddressForCurrency(currencyId)(transport, currencyId, path, options), |
||||
|
), |
||||
|
), |
||||
|
) |
||||
|
|
||||
|
export default cmd |
||||
|
@ -1,4 +1,11 @@ |
|||||
export listen from './listen' |
// @flow
|
||||
export ensureDeviceApp from './ensureDeviceApp' |
import type { Command } from 'helpers/ipc' |
||||
export getAddress from './getAddress' |
|
||||
export signTransaction from './signTransaction' |
import getAddress from './getAddress' |
||||
|
import listen from './listen' |
||||
|
import signTransaction from './signTransaction' |
||||
|
|
||||
|
// TODO port these to commands
|
||||
|
export { listen } |
||||
|
|
||||
|
export const commands: Array<Command<any, any>> = [getAddress, signTransaction] |
||||
|
@ -1,32 +1,28 @@ |
|||||
// @flow
|
// @flow
|
||||
|
|
||||
import invariant from 'invariant' |
import { createCommand, Command } from 'helpers/ipc' |
||||
|
import { fromPromise } from 'rxjs/observable/fromPromise' |
||||
import CommNodeHid from '@ledgerhq/hw-transport-node-hid' |
import CommNodeHid from '@ledgerhq/hw-transport-node-hid' |
||||
import type Transport from '@ledgerhq/hw-transport' |
|
||||
import type { IPCSend } from 'types/electron' |
|
||||
import signTransactionForCurrency from './signTransactionForCurrency' |
import signTransactionForCurrency from './signTransactionForCurrency' |
||||
|
|
||||
export default async ( |
type Input = { |
||||
send: IPCSend, |
currencyId: string, |
||||
{ |
devicePath: string, |
||||
currencyId, |
path: string, |
||||
devicePath, |
transaction: *, |
||||
path, |
|
||||
transaction, |
|
||||
}: { |
|
||||
currencyId: string, |
|
||||
devicePath: string, |
|
||||
path: string, |
|
||||
transaction: *, |
|
||||
}, |
|
||||
) => { |
|
||||
try { |
|
||||
invariant(currencyId, 'currencyId "%s" not defined', currencyId) |
|
||||
const transport: Transport<*> = await CommNodeHid.open(devicePath) |
|
||||
const signer = signTransactionForCurrency(currencyId) |
|
||||
const res = await signer(transport, currencyId, path, transaction) |
|
||||
send('devices.signTransaction.success', res) |
|
||||
} catch (err) { |
|
||||
send('devices.signTransaction.fail') |
|
||||
} |
|
||||
} |
} |
||||
|
|
||||
|
type Result = string |
||||
|
|
||||
|
const cmd: Command<Input, Result> = createCommand( |
||||
|
'devices', |
||||
|
'signTransaction', |
||||
|
({ currencyId, devicePath, path, transaction }) => |
||||
|
fromPromise( |
||||
|
CommNodeHid.open(devicePath).then(transport => |
||||
|
signTransactionForCurrency(currencyId)(transport, currencyId, path, transaction), |
||||
|
), |
||||
|
), |
||||
|
) |
||||
|
|
||||
|
export default cmd |
||||
|
Loading…
Reference in new issue