Gaëtan Renaudeau
7 years ago
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
|
|||
|
|||
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 type Transport from '@ledgerhq/hw-transport' |
|||
import type { IPCSend } from 'types/electron' |
|||
import getAddressForCurrency from './getAddressForCurrency' |
|||
|
|||
export default async ( |
|||
send: IPCSend, |
|||
{ |
|||
currencyId, |
|||
devicePath, |
|||
path, |
|||
...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 Input = { |
|||
currencyId: string, |
|||
devicePath: string, |
|||
path: string, |
|||
verify?: boolean, |
|||
segwit?: boolean, |
|||
} |
|||
|
|||
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' |
|||
export ensureDeviceApp from './ensureDeviceApp' |
|||
export getAddress from './getAddress' |
|||
export signTransaction from './signTransaction' |
|||
// @flow
|
|||
import type { Command } from 'helpers/ipc' |
|||
|
|||
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
|
|||
|
|||
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 type Transport from '@ledgerhq/hw-transport' |
|||
import type { IPCSend } from 'types/electron' |
|||
import signTransactionForCurrency from './signTransactionForCurrency' |
|||
|
|||
export default async ( |
|||
send: IPCSend, |
|||
{ |
|||
currencyId, |
|||
devicePath, |
|||
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 Input = { |
|||
currencyId: string, |
|||
devicePath: string, |
|||
path: string, |
|||
transaction: *, |
|||
} |
|||
|
|||
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