Gaëtan Renaudeau
7 years ago
committed by
GitHub
32 changed files with 1142 additions and 709 deletions
@ -0,0 +1,69 @@ |
|||||
|
// @flow
|
||||
|
import axios from 'axios' |
||||
|
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types' |
||||
|
import { blockchainBaseURL } from './Ledger' |
||||
|
|
||||
|
export type Block = { height: number } // TODO more fields actually
|
||||
|
export type Tx = { |
||||
|
hash: string, |
||||
|
received_at: string, |
||||
|
nonce: string, |
||||
|
value: number, |
||||
|
gas: number, |
||||
|
gas_price: number, |
||||
|
cumulative_gas_used: number, |
||||
|
gas_used: number, |
||||
|
from: string, |
||||
|
to: string, |
||||
|
input: string, |
||||
|
index: number, |
||||
|
block: { |
||||
|
hash: string, |
||||
|
height: number, |
||||
|
time: string, |
||||
|
}, |
||||
|
confirmations: number, |
||||
|
} |
||||
|
|
||||
|
export type API = { |
||||
|
getTransactions: ( |
||||
|
address: string, |
||||
|
blockHash: ?string, |
||||
|
) => Promise<{ |
||||
|
truncated: boolean, |
||||
|
txs: Tx[], |
||||
|
}>, |
||||
|
getCurrentBlock: () => Promise<Block>, |
||||
|
getAccountNonce: (address: string) => Promise<string>, |
||||
|
broadcastTransaction: (signedTransaction: string) => Promise<string>, |
||||
|
getAccountBalance: (address: string) => Promise<number>, |
||||
|
} |
||||
|
|
||||
|
export const apiForCurrency = (currency: CryptoCurrency): API => { |
||||
|
const baseURL = blockchainBaseURL(currency) |
||||
|
|
||||
|
return { |
||||
|
async getTransactions(address, blockHash) { |
||||
|
const { data } = await axios.get(`${baseURL}/addresses/${address}/transactions`, { |
||||
|
params: { blockHash, noToken: 1 }, |
||||
|
}) |
||||
|
return data |
||||
|
}, |
||||
|
async getCurrentBlock() { |
||||
|
const { data } = await axios.get(`${baseURL}/blocks/current`) |
||||
|
return data |
||||
|
}, |
||||
|
async getAccountNonce(address) { |
||||
|
const { data } = await axios.get(`${baseURL}/addresses/${address}/nonce`) |
||||
|
return data[0].nonce |
||||
|
}, |
||||
|
async broadcastTransaction(tx) { |
||||
|
const { data } = await axios.post(`${baseURL}/transactions/send`, { tx }) |
||||
|
return data.result |
||||
|
}, |
||||
|
async getAccountBalance(address) { |
||||
|
const { data } = await axios.get(`${baseURL}/addresses/${address}/balance`) |
||||
|
return data[0].balance |
||||
|
}, |
||||
|
} |
||||
|
} |
@ -1,9 +1,18 @@ |
|||||
// @flow
|
// @flow
|
||||
import axios from 'axios' |
import type { Currency } from '@ledgerhq/live-common/lib/types' |
||||
|
|
||||
const BASE_URL = process.env.LEDGER_REST_API_BASE || 'https://api.ledgerwallet.com/' |
const BASE_URL = process.env.LEDGER_REST_API_BASE || 'https://api.ledgerwallet.com/' |
||||
|
|
||||
export const get = (url: string, config: *): Promise<*> => |
const mapping = { |
||||
axios.get(`${BASE_URL}${url}`, { |
bitcoin_cash: 'abc', |
||||
...config, |
ethereum_classic: 'ethc', |
||||
}) |
ethereum_testnet: 'eth_testnet', |
||||
|
} |
||||
|
|
||||
|
export const currencyToFeeTicker = (currency: Currency) => { |
||||
|
const tickerLowerCase = currency.ticker.toLowerCase() |
||||
|
return mapping[currency.id] || tickerLowerCase |
||||
|
} |
||||
|
|
||||
|
export const blockchainBaseURL = (currency: Currency) => |
||||
|
`${BASE_URL}blockchain/v2/${currencyToFeeTicker(currency)}` |
||||
|
@ -0,0 +1,22 @@ |
|||||
|
// @flow
|
||||
|
import React from 'react' |
||||
|
import EthereumKind from 'components/FeesField/EthereumKind' |
||||
|
import type { EditProps } from './types' |
||||
|
import makeMockBridge from './makeMockBridge' |
||||
|
|
||||
|
const EditFees = ({ account, onChange, value }: EditProps<*>) => ( |
||||
|
<EthereumKind |
||||
|
onChange={gasPrice => { |
||||
|
onChange({ ...value, gasPrice }) |
||||
|
}} |
||||
|
gasPrice={value.gasPrice} |
||||
|
account={account} |
||||
|
/> |
||||
|
) |
||||
|
|
||||
|
export default makeMockBridge({ |
||||
|
extraInitialTransactionProps: () => ({ gasPrice: 0 }), |
||||
|
EditFees, |
||||
|
getTotalSpent: (a, t) => Promise.resolve(t.amount + t.gasPrice), |
||||
|
getMaxAmount: (a, t) => Promise.resolve(a.balance - t.gasPrice), |
||||
|
}) |
@ -0,0 +1,33 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import { createCommand, Command } from 'helpers/ipc' |
||||
|
import { fromPromise } from 'rxjs/observable/fromPromise' |
||||
|
import CommNodeHid from '@ledgerhq/hw-transport-node-hid' |
||||
|
import getAddressForCurrency from 'helpers/getAddressForCurrency' |
||||
|
|
||||
|
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 |
@ -0,0 +1,28 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import { createCommand, Command } from 'helpers/ipc' |
||||
|
import { fromPromise } from 'rxjs/observable/fromPromise' |
||||
|
import CommNodeHid from '@ledgerhq/hw-transport-node-hid' |
||||
|
import signTransactionForCurrency from 'helpers/signTransactionForCurrency' |
||||
|
|
||||
|
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 |
@ -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,32 @@ |
|||||
|
// @flow
|
||||
|
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types' |
||||
|
|
||||
|
function shouldDerivateChangeFieldInsteadOfAccount(c: CryptoCurrency) { |
||||
|
// ethereum have a special way of derivating things
|
||||
|
return c.id.indexOf('ethereum') === 0 |
||||
|
} |
||||
|
|
||||
|
// https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
|
||||
|
// x is a derivation index. we don't always derivate the same part of the path
|
||||
|
export function makeBip44Path({ |
||||
|
currency, |
||||
|
segwit, |
||||
|
x, |
||||
|
}: { |
||||
|
currency: CryptoCurrency, |
||||
|
segwit?: boolean, |
||||
|
x?: number, |
||||
|
}): string { |
||||
|
const purpose = segwit ? 49 : 44 |
||||
|
const coinType = currency.coinType |
||||
|
let path = `${purpose}'/${coinType}'` |
||||
|
if (shouldDerivateChangeFieldInsteadOfAccount(currency)) { |
||||
|
path += "/0'" |
||||
|
if (x !== undefined) { |
||||
|
path += `/${x}` |
||||
|
} |
||||
|
} else if (x !== undefined) { |
||||
|
path += `/${x}'` |
||||
|
} |
||||
|
return path |
||||
|
} |
@ -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) |
||||
|
} |
@ -0,0 +1,68 @@ |
|||||
|
// @flow
|
||||
|
import Eth from '@ledgerhq/hw-app-eth' |
||||
|
import type Transport from '@ledgerhq/hw-transport' |
||||
|
import EthereumTx from 'ethereumjs-tx' |
||||
|
|
||||
|
// see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
|
||||
|
function getNetworkId(currencyId: string): ?number { |
||||
|
switch (currencyId) { |
||||
|
case 'ethereum': |
||||
|
return 1 |
||||
|
case 'ethereum_classic': |
||||
|
return 61 |
||||
|
case 'ethereum_classic_testnet': |
||||
|
return 62 |
||||
|
case 'ethereum_testnet': |
||||
|
return 3 // Ropsten by convention
|
||||
|
default: |
||||
|
return null |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default async ( |
||||
|
transport: Transport<*>, |
||||
|
currencyId: string, |
||||
|
path: string, |
||||
|
t: { |
||||
|
nonce: string, |
||||
|
recipient: string, |
||||
|
gasPrice: number, |
||||
|
amount: number, |
||||
|
}, |
||||
|
) => { |
||||
|
// First, we need to create a partial tx and send to the device
|
||||
|
|
||||
|
const chainId = getNetworkId(currencyId) |
||||
|
if (!chainId) throw new Error(`chainId not found for currency=${currencyId}`) |
||||
|
const gasLimit = '0x5208' // cost of a simple send
|
||||
|
const tx = new EthereumTx({ |
||||
|
nonce: t.nonce, |
||||
|
gasPrice: `0x${t.gasPrice.toString(16)}`, |
||||
|
gasLimit, |
||||
|
to: t.recipient, |
||||
|
value: `0x${t.amount.toString(16)}`, |
||||
|
chainId, |
||||
|
}) |
||||
|
tx.raw[6] = Buffer.from([chainId]) // v
|
||||
|
tx.raw[7] = Buffer.from([]) // r
|
||||
|
tx.raw[8] = Buffer.from([]) // s
|
||||
|
|
||||
|
const eth = new Eth(transport) |
||||
|
const result = await eth.signTransaction(path, tx.serialize().toString('hex')) |
||||
|
|
||||
|
// Second, we re-set some tx fields from the device signature
|
||||
|
|
||||
|
tx.v = Buffer.from(result.v, 'hex') |
||||
|
tx.r = Buffer.from(result.r, 'hex') |
||||
|
tx.s = Buffer.from(result.s, 'hex') |
||||
|
const signedChainId = Math.floor((tx.v[0] - 35) / 2) // EIP155: v should be chain_id * 2 + {35, 36}
|
||||
|
const validChainId = chainId & 0xff // eslint-disable-line no-bitwise
|
||||
|
if (signedChainId !== validChainId) { |
||||
|
throw new Error( |
||||
|
`Invalid chainId signature returned. Expected: ${chainId}, Got: ${signedChainId}`, |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// Finally, we can send the transaction string to broadcast
|
||||
|
return `0x${tx.serialize().toString('hex')}` |
||||
|
} |
@ -0,0 +1,27 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import type Transport from '@ledgerhq/hw-transport' |
||||
|
import ethereum from './ethereum' |
||||
|
|
||||
|
type Resolver = ( |
||||
|
transport: Transport<*>, |
||||
|
currencyId: string, |
||||
|
path: string, // if provided use this path, otherwise resolve it
|
||||
|
transaction: *, // any data
|
||||
|
) => Promise<string> |
||||
|
|
||||
|
type Module = (currencyId: string) => Resolver |
||||
|
|
||||
|
const fallback: string => Resolver = currencyId => () => |
||||
|
Promise.reject(new Error(`${currencyId} device support not implemented`)) |
||||
|
|
||||
|
const all = { |
||||
|
ethereum, |
||||
|
ethereum_testnet: ethereum, |
||||
|
ethereum_classic: ethereum, |
||||
|
ethereum_classic_testnet: ethereum, |
||||
|
} |
||||
|
|
||||
|
const m: Module = (currencyId: string) => all[currencyId] || fallback(currencyId) |
||||
|
|
||||
|
export default m |
@ -1,59 +0,0 @@ |
|||||
// @flow
|
|
||||
|
|
||||
/* eslint-disable no-bitwise */ |
|
||||
|
|
||||
import Btc from '@ledgerhq/hw-app-btc' |
|
||||
|
|
||||
import { findCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' |
|
||||
|
|
||||
export function coinTypeForId(id: string) { |
|
||||
const currency = findCryptoCurrencyById(id) |
|
||||
return currency ? currency.coinType : 0 |
|
||||
} |
|
||||
|
|
||||
export function getPath({ |
|
||||
currencyId, |
|
||||
account, |
|
||||
segwit = true, |
|
||||
}: { |
|
||||
currencyId: string, |
|
||||
account?: any, |
|
||||
segwit?: boolean, |
|
||||
}) { |
|
||||
return `${segwit ? 49 : 44}'/${coinTypeForId(currencyId)}'${ |
|
||||
account !== undefined ? `/${account}'` : '' |
|
||||
}` |
|
||||
} |
|
||||
|
|
||||
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, |
|
||||
}) { |
|
||||
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, |
|
||||
accountPath, |
|
||||
accountAddress, |
|
||||
...options |
|
||||
}: { |
|
||||
currencyId: string, |
|
||||
devicePath: string, |
|
||||
accountPath: ?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, accountPath, options) |
|
||||
if (accountPath && 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 }) |
|
||||
} |
|
||||
} |
|
@ -1,2 +1,11 @@ |
|||||
export listen from './listen' |
// @flow
|
||||
export ensureDeviceApp from './ensureDeviceApp' |
import type { Command } from 'helpers/ipc' |
||||
|
|
||||
|
import getAddress from 'commands/getAddress' |
||||
|
import signTransaction from 'commands/signTransaction' |
||||
|
import listen from './listen' |
||||
|
|
||||
|
// TODO port these to commands
|
||||
|
export { listen } |
||||
|
|
||||
|
export const commands: Array<Command<any, any>> = [getAddress, signTransaction] |
||||
|
File diff suppressed because it is too large
Loading…
Reference in new issue