Browse Source

Implement Ethereum

master
Gaëtan Renaudeau 7 years ago
parent
commit
7e3f581385
  1. 3
      package.json
  2. 69
      src/api/Ethereum.js
  3. 13
      src/api/Fees.js
  4. 18
      src/api/Ledger.js
  5. 297
      src/bridge/EthereumJSBridge.js
  6. 22
      src/bridge/EthereumMockJSBridge.js
  7. 2
      src/bridge/LibcoreBridge.js
  8. 2
      src/bridge/UnsupportedBridge.js
  9. 8
      src/bridge/index.js
  10. 24
      src/components/DeviceConnect/index.js
  11. 23
      src/components/EnsureDeviceApp/index.js
  12. 22
      src/components/modals/AddAccount/index.js
  13. 3
      src/components/modals/StepConnectDevice.js
  14. 32
      src/helpers/bip32path.js
  15. 22
      src/internals/accounts/helpers.js
  16. 10
      src/internals/devices/ensureDeviceApp.js
  17. 32
      src/internals/devices/getAddress.js
  18. 8
      src/internals/devices/getAddressForCurrency/btc.js
  19. 8
      src/internals/devices/getAddressForCurrency/ethereum.js
  20. 11
      src/internals/devices/getAddressForCurrency/index.js
  21. 2
      src/internals/devices/index.js
  22. 32
      src/internals/devices/signTransaction.js
  23. 68
      src/internals/devices/signTransactionForCurrency/ethereum.js
  24. 27
      src/internals/devices/signTransactionForCurrency/index.js
  25. 6
      src/renderer/events.js
  26. 579
      yarn.lock

3
package.json

@ -40,7 +40,7 @@
"@ledgerhq/hw-transport": "^4.12.0",
"@ledgerhq/hw-transport-node-hid": "^4.12.0",
"@ledgerhq/ledger-core": "^1.0.1",
"@ledgerhq/live-common": "^2.6.0",
"@ledgerhq/live-common": "^2.7.2",
"axios": "^0.18.0",
"babel-runtime": "^6.26.0",
"bcryptjs": "^2.4.3",
@ -53,6 +53,7 @@
"downshift": "^1.31.9",
"electron-store": "^1.3.0",
"electron-updater": "^2.21.8",
"ethereumjs-tx": "^1.3.4",
"fuse.js": "^3.2.0",
"history": "^4.7.2",
"i18next": "^11.2.2",

69
src/api/Ethereum.js

@ -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
},
}
}

13
src/api/Fees.js

@ -1,21 +1,14 @@
// @flow
import axios from 'axios'
import type { Currency } from '@ledgerhq/live-common/lib/types'
import { get } from './Ledger'
const mapping = {
bch: 'abc',
}
const currencyToFeeTicker = (currency: Currency) => {
const tickerLowerCase = currency.ticker.toLowerCase()
return mapping[tickerLowerCase] || tickerLowerCase
}
import { blockchainBaseURL } from './Ledger'
export type Fees = {
[_: string]: number,
}
export const getEstimatedFees = async (currency: Currency): Promise<Fees> => {
const { data, status } = await get(`blockchain/v2/${currencyToFeeTicker(currency)}/fees`)
const { data, status } = await axios.get(`${blockchainBaseURL(currency)}/fees`)
if (data) {
return data
}

18
src/api/Ledger.js

@ -1,9 +1,17 @@
// @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/'
export const get = (url: string, config: *): Promise<*> =>
axios.get(`${BASE_URL}${url}`, {
...config,
})
const mapping = {
bch: 'abc',
etc: 'ethc',
}
export const currencyToFeeTicker = (currency: Currency) => {
const tickerLowerCase = currency.ticker.toLowerCase()
return mapping[tickerLowerCase] || tickerLowerCase
}
export const blockchainBaseURL = (currency: Currency) =>
`${BASE_URL}blockchain/v2/${currencyToFeeTicker(currency)}`

297
src/bridge/EthereumJSBridge.js

@ -1,10 +1,19 @@
// @flow
import React from 'react'
import { ipcRenderer } from 'electron'
import { sendEvent } from 'renderer/events'
import EthereumKind from 'components/FeesField/EthereumKind'
import type { EditProps } from './types'
import makeMockBridge from './makeMockBridge'
import type { Account, Operation } from '@ledgerhq/live-common/lib/types'
import { apiForCurrency } from 'api/Ethereum'
import type { Tx } from 'api/Ethereum'
import { makeBip44Path } from 'helpers/bip32path'
import type { EditProps, WalletBridge } from './types'
const EditFees = ({ account, onChange, value }: EditProps<*>) => (
// TODO in future it would be neat to support eip55
type Transaction = *
const EditFees = ({ account, onChange, value }: EditProps<Transaction>) => (
<EthereumKind
onChange={gasPrice => {
onChange({ ...value, gasPrice })
@ -14,9 +23,285 @@ const EditFees = ({ account, onChange, value }: EditProps<*>) => (
/>
)
export default makeMockBridge({
extraInitialTransactionProps: () => ({ gasPrice: 0 }),
const toAccountOperation = (account: Account) => (tx: Tx): Operation => {
const sending = account.address.toLowerCase() === tx.from.toLowerCase()
return {
id: tx.hash,
hash: tx.hash,
address: sending ? tx.to : tx.from,
amount: (sending ? -1 : 1) * tx.value,
blockHeight: tx.block.height,
blockHash: tx.block.hash,
accountId: account.id,
senders: [tx.from],
recipients: [tx.to],
date: new Date(tx.received_at),
}
}
function isRecipientValid(currency, recipient) {
return !!recipient.match(/^0x[0-9a-fA-F]{40}$/)
}
function mergeOps(existing: Operation[], newFetched: Operation[]) {
const ids = existing.map(o => o.id)
const all = existing.concat(newFetched.filter(o => !ids.includes(o.id)))
return all.sort((a, b) => a.date - b.date)
}
const paginateMoreTransactions = async (
account: Account,
acc: Operation[],
): Promise<Operation[]> => {
const api = apiForCurrency(account.currency)
const { txs } = await api.getTransactions(
account.address,
acc.length ? acc[acc.length - 1].blockHash : undefined,
)
if (txs.length === 0) return acc
return mergeOps(acc, txs.map(toAccountOperation(account)))
}
function signTransactionOnDevice(
a: Account,
t: Transaction,
deviceId: string,
nonce: string,
): Promise<string> {
const transaction = { ...t, nonce }
return new Promise((resolve, reject) => {
const unbind = () => {
ipcRenderer.removeListener('msg', handleMsgEvent)
}
function handleMsgEvent(e, { data, type }) {
if (type === 'devices.signTransaction.success') {
unbind()
resolve(data)
} else if (type === 'devices.signTransaction.fail') {
unbind()
reject(new Error('failed to get address'))
}
}
ipcRenderer.on('msg', handleMsgEvent)
sendEvent('devices', 'signTransaction', {
currencyId: a.currency.id,
devicePath: deviceId,
path: a.path,
transaction,
})
})
}
const EthereumBridge: WalletBridge<Transaction> = {
scanAccountsOnDevice(currency, deviceId, { next, complete, error }) {
let finished = false
const unbind = () => {
finished = true
ipcRenderer.removeListener('msg', handleMsgEvent)
}
const api = apiForCurrency(currency)
// FIXME: THIS IS SPAghetti, we need to move to a more robust approach to get an observable with a sendEvent
// in future ideally what we want is:
// return mergeMap(addressesObservable, address => fetchAccount(address))
let index = 0
let balanceZerosCount = 0
function pollNextAddress() {
sendEvent('devices', 'getAddress', {
currencyId: currency.id,
devicePath: deviceId,
path: makeBip44Path({
currency,
x: index,
}),
})
index++
}
let currentBlockPromise
function lazyCurrentBlock() {
if (!currentBlockPromise) {
currentBlockPromise = api.getCurrentBlock()
}
return currentBlockPromise
}
async function stepAddress({ address, path }) {
try {
const balance = await api.getAccountBalance(address)
if (finished) return
if (balance === 0) {
if (balanceZerosCount === 0) {
// first zero account will emit one account as opportunity to create a new account..
const currentBlock = await lazyCurrentBlock()
const accountId = `${currency.id}_${address}`
const account: Account = {
id: accountId,
xpub: '',
path,
walletPath: String(index),
name: 'New Account',
isSegwit: false,
address,
addresses: [address],
balance,
blockHeight: currentBlock.height,
archived: true,
index,
currency,
operations: [],
unit: currency.units[0],
lastSyncDate: new Date(),
}
next(account)
}
balanceZerosCount++
// NB we currently stop earlier. in future we shouldn't stop here, just continue & user will stop at the end!
// NB (what's the max tho?)
unbind()
complete()
} else {
const currentBlock = await lazyCurrentBlock()
if (finished) return
const { txs } = await api.getTransactions(address)
if (finished) return
const accountId = `${currency.id}_${address}`
const account: Account = {
id: accountId,
xpub: '',
path,
walletPath: String(index),
name: address.slice(32),
isSegwit: false,
address,
addresses: [address],
balance,
blockHeight: currentBlock.height,
archived: true,
index,
currency,
operations: [],
unit: currency.units[0],
lastSyncDate: new Date(),
}
account.operations = txs.map(toAccountOperation(account))
next(account)
pollNextAddress()
}
} catch (e) {
error(e)
}
}
function handleMsgEvent(e, { data, type }) {
if (type === 'devices.getAddress.success') {
stepAddress(data)
} else if (type === 'devices.getAddress.fail') {
error(new Error(data.message))
}
}
ipcRenderer.on('msg', handleMsgEvent)
pollNextAddress()
return {
unsubscribe() {
unbind()
},
}
},
synchronize({ address, blockHeight, currency }, { next, complete, error }) {
let unsubscribed = false
const api = apiForCurrency(currency)
async function main() {
try {
const block = await api.getCurrentBlock()
if (unsubscribed) return
if (block.height === blockHeight) {
complete()
} else {
const balance = await api.getAccountBalance(address)
if (unsubscribed) return
const { txs } = await api.getTransactions(address)
if (unsubscribed) return
next(a => {
const currentOps = a.operations
const newOps = txs.map(toAccountOperation(a))
if (newOps.length === 0 && currentOps.length === 0) return a
if (currentOps[0].id === newOps[0].id) return a
const operations = mergeOps(currentOps, newOps)
return {
...a,
operations,
balance,
blockHeight: block.height,
lastSyncDate: new Date(),
}
})
complete()
}
} catch (e) {
error(e)
}
}
main()
return {
unsubscribe() {
unsubscribed = true
},
}
},
pullMoreOperations: async account => {
const operations = await paginateMoreTransactions(account, account.operations)
return a => ({ ...a, operations })
},
isRecipientValid: (currency, recipient) => Promise.resolve(isRecipientValid(currency, recipient)),
createTransaction: () => ({
amount: 0,
recipient: '',
gasPrice: 0,
}),
editTransactionAmount: (account, t, amount) => ({
...t,
amount,
}),
getTransactionAmount: (a, t) => t.amount,
editTransactionRecipient: (account, t, recipient) => ({
...t,
recipient,
}),
getTransactionRecipient: (a, t) => t.recipient,
isValidTransaction: (a, t) => (t.amount > 0 && t.recipient && true) || false,
// $FlowFixMe
EditFees,
getTotalSpent: (a, t) => Promise.resolve(t.amount + t.gasPrice),
getMaxAmount: (a, t) => Promise.resolve(a.balance - t.gasPrice),
})
signAndBroadcast: async (a, t, deviceId) => {
const api = apiForCurrency(a.currency)
const nonce = await api.getAccountNonce(a.address)
const transaction = await signTransactionOnDevice(a, t, deviceId, nonce)
const result = await api.broadcastTransaction(transaction)
return result
},
}
export default EthereumBridge

22
src/bridge/EthereumMockJSBridge.js

@ -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),
})

2
src/bridge/LibcoreBridge.js

@ -116,8 +116,6 @@ const LibcoreBridge: WalletBridge<Transaction> = {
}
},
refreshLastOperations: () => Promise.reject(notImplemented),
pullMoreOperations: () => Promise.reject(notImplemented),
isRecipientValid: (currency, recipient) => Promise.resolve(recipient.length > 0),

2
src/bridge/UnsupportedBridge.js

@ -14,8 +14,6 @@ const UnsupportedBridge: WalletBridge<*> = {
return { unsubscribe() {} }
},
refreshLastOperations: () => Promise.reject(genericError),
pullMoreOperations: () => Promise.reject(genericError),
isRecipientValid: () => Promise.reject(genericError),

8
src/bridge/index.js

@ -8,7 +8,11 @@ import EthereumJSBridge from './EthereumJSBridge'
const RippleJSBridge = UnsupportedBridge
export const getBridgeForCurrency = (currency: Currency): WalletBridge<any> => {
if (currency.id === 'ethereum') return EthereumJSBridge // polyfill js
if (currency.id === 'ripple') return RippleJSBridge // polyfill js
if (currency.id.indexOf('ethereum') === 0) {
return EthereumJSBridge // polyfill js
}
if (currency.id === 'ripple') {
return RippleJSBridge // polyfill js
}
return LibcoreBridge // libcore for the rest
}

24
src/components/DeviceConnect/index.js

@ -143,6 +143,7 @@ type Props = {
deviceSelected: ?Device,
onChangeDevice: Device => void,
t: T,
errorMessage: ?string,
}
const emitChangeDevice = props => {
@ -180,7 +181,15 @@ class DeviceConnect extends PureComponent<Props> {
}
render() {
const { deviceSelected, accountName, currency, t, onChangeDevice, devices } = this.props
const {
deviceSelected,
errorMessage,
accountName,
currency,
t,
onChangeDevice,
devices,
} = this.props
const appState = this.getAppState()
@ -250,19 +259,24 @@ class DeviceConnect extends PureComponent<Props> {
<StepCheck checked={appState.success} hasErrors={appState.fail} />
</StepContent>
</Step>
{accountName !== null && (
<Info hasErrors={appState.fail}>
{appState.fail ? (
<Info hasErrors>
<Box>
<IconInfoCircle size={12} />
</Box>
<Box>
<Box style={{ userSelect: 'text' }}>
{accountName ? (
<Trans i18nKey="deviceConnect:info" parent="div">
{'You must use the device associated to the account '}
<strong>{accountName}</strong>
</Trans>
) : (
String(errorMessage || '')
)}
</Box>
</Info>
)}
) : null}
</Box>
)
}

23
src/components/EnsureDeviceApp/index.js

@ -3,6 +3,7 @@ import invariant from 'invariant'
import { PureComponent } from 'react'
import { connect } from 'react-redux'
import { ipcRenderer } from 'electron'
import { makeBip44Path } from 'helpers/bip32path'
import type { Account, CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import type { Device } from 'types/common'
@ -15,7 +16,7 @@ type OwnProps = {
currency: ?CryptoCurrency,
deviceSelected: ?Device,
account: ?Account,
onStatusChange?: (DeviceStatus, AppStatus) => void,
onStatusChange?: (DeviceStatus, AppStatus, ?string) => void,
// TODO prefer children function
render?: ({
appStatus: AppStatus,
@ -23,6 +24,7 @@ type OwnProps = {
devices: Device[],
deviceSelected: ?Device,
deviceStatus: DeviceStatus,
errorMessage: ?string,
}) => React$Element<*>,
}
@ -37,6 +39,7 @@ type AppStatus = 'success' | 'fail' | 'progress'
type State = {
deviceStatus: DeviceStatus,
appStatus: AppStatus,
errorMessage: ?string,
}
const mapStateToProps = (state: StoreState) => ({
@ -47,6 +50,7 @@ class EnsureDeviceApp extends PureComponent<Props, State> {
state = {
appStatus: 'progress',
deviceStatus: this.props.deviceSelected ? 'connected' : 'unconnected',
errorMessage: null,
}
componentDidMount() {
@ -100,16 +104,20 @@ class EnsureDeviceApp extends PureComponent<Props, State> {
if (account) {
options = {
currencyId: account.currency.id,
accountPath: account.path,
path: account.path,
accountAddress: account.address,
segwit: account.path.startsWith("49'"), // TODO: store segwit info in account
}
} else if (currency) {
options = {
currencyId: currency.id,
path: makeBip44Path({ currency }),
}
} else {
throw new Error('either currency or account is required')
}
// TODO just use getAddress!
sendEvent('devices', 'ensureDeviceApp', {
devicePath: deviceSelected.path,
...options,
@ -118,11 +126,11 @@ class EnsureDeviceApp extends PureComponent<Props, State> {
_timeout: *
handleStatusChange = (deviceStatus, appStatus) => {
handleStatusChange = (deviceStatus, appStatus, errorMessage = null) => {
const { onStatusChange } = this.props
clearTimeout(this._timeout)
this.setState({ deviceStatus, appStatus })
onStatusChange && onStatusChange(deviceStatus, appStatus)
this.setState({ deviceStatus, appStatus, errorMessage })
onStatusChange && onStatusChange(deviceStatus, appStatus, errorMessage)
}
handleMsgEvent = (e, { type, data }) => {
@ -139,14 +147,14 @@ class EnsureDeviceApp extends PureComponent<Props, State> {
}
if (type === 'devices.ensureDeviceApp.fail' && deviceSelected.path === data.devicePath) {
this.handleStatusChange(deviceStatus, 'fail')
this.handleStatusChange(deviceStatus, 'fail', data.message)
this._timeout = setTimeout(this.checkAppOpened, 1e3)
}
}
render() {
const { currency, account, devices, deviceSelected, render } = this.props
const { appStatus, deviceStatus } = this.state
const { appStatus, deviceStatus, errorMessage } = this.state
if (render) {
const cur = account ? account.currency : currency
@ -157,6 +165,7 @@ class EnsureDeviceApp extends PureComponent<Props, State> {
devices,
deviceSelected: deviceStatus === 'connected' ? deviceSelected : null,
deviceStatus,
errorMessage,
})
}

22
src/components/modals/AddAccount/index.js

@ -4,6 +4,7 @@ import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { compose } from 'redux'
import { translate } from 'react-i18next'
import { createStructuredSelector } from 'reselect'
import type { Account, CryptoCurrency } from '@ledgerhq/live-common/lib/types'
@ -12,7 +13,12 @@ import type { Device, T } from 'types/common'
import { MODAL_ADD_ACCOUNT } from 'config/constants'
import { closeModal } from 'reducers/modals'
import { canCreateAccount, getAccounts, getArchivedAccounts } from 'reducers/accounts'
import {
canCreateAccount,
getAccounts,
getVisibleAccounts,
getArchivedAccounts,
} from 'reducers/accounts'
import { addAccount, updateAccount } from 'actions/accounts'
@ -34,10 +40,11 @@ const GET_STEPS = t => [
{ label: t('addAccount:steps.importAccounts.title'), Comp: StepImport },
]
const mapStateToProps = state => ({
existingAccounts: getAccounts(state),
archivedAccounts: getArchivedAccounts(state),
canCreateAccount: canCreateAccount(state),
const mapStateToProps = createStructuredSelector({
existingAccounts: getAccounts,
visibleAccounts: getVisibleAccounts,
archivedAccounts: getArchivedAccounts,
canCreateAccount,
})
const mapDispatchToProps = {
@ -49,6 +56,7 @@ const mapDispatchToProps = {
type Props = {
existingAccounts: Account[],
addAccount: Function,
visibleAccounts: Account[],
archivedAccounts: Account[],
canCreateAccount: boolean,
closeModal: Function,
@ -93,7 +101,7 @@ class AddAccountModal extends PureComponent<Props, State> {
scanSubscription: *
startScanAccountsDevice() {
const { existingAccounts, addAccount } = this.props
const { visibleAccounts, addAccount } = this.props
const { deviceSelected, currency } = this.state
if (!deviceSelected || !currency) {
@ -102,7 +110,7 @@ class AddAccountModal extends PureComponent<Props, State> {
const bridge = getBridgeForCurrency(currency)
this.scanSubscription = bridge.scanAccountsOnDevice(currency, deviceSelected.path, {
next: account => {
if (!existingAccounts.some(a => a.id === account.id)) {
if (!visibleAccounts.some(a => a.id === account.id)) {
addAccount(account)
this.setState(state => ({
scannedAccounts: [...state.scannedAccounts, account],

3
src/components/modals/StepConnectDevice.js

@ -30,7 +30,7 @@ const StepConnectDevice = ({
currency={currency}
deviceSelected={deviceSelected}
onStatusChange={onStatusChange}
render={({ currency, appStatus, devices, deviceSelected }) => (
render={({ currency, appStatus, devices, deviceSelected, errorMessage }) => (
<DeviceConnect
accountName={accountName}
currency={currency}
@ -38,6 +38,7 @@ const StepConnectDevice = ({
devices={devices}
deviceSelected={deviceSelected}
onChangeDevice={onChangeDevice}
errorMessage={errorMessage}
/>
)}
/>

32
src/helpers/bip32path.js

@ -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
}

22
src/internals/accounts/helpers.js

@ -4,27 +4,6 @@
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,
@ -53,6 +32,7 @@ export function verifyAddress({
path: string,
segwit?: boolean,
}) {
console.warn('DEPRECATED use devices.getAddress with verify option')
const btc = new Btc(transport)
return btc.getWalletPublicKey(path, true, segwit)

10
src/internals/devices/ensureDeviceApp.js

@ -11,13 +11,13 @@ export default async (
{
currencyId,
devicePath,
accountPath,
path,
accountAddress,
...options
}: {
currencyId: string,
devicePath: string,
accountPath: ?string,
path: string,
accountAddress: ?string,
},
) => {
@ -25,12 +25,12 @@ export default async (
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) {
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 })
send('devices.ensureDeviceApp.fail', { devicePath, message: err.message })
}
}

32
src/internals/devices/getAddress.js

@ -0,0 +1,32 @@
// @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,
...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 })
}
}

8
src/internals/devices/getAddressForCurrency/btc.js

@ -2,12 +2,11 @@
import Btc from '@ledgerhq/hw-app-btc'
import type Transport from '@ledgerhq/hw-transport'
import { getPath } from 'internals/accounts/helpers'
export default async (
transport: Transport<*>,
currencyId: string,
bip32path: ?string,
path: string,
{
segwit = true,
verify = false,
@ -17,7 +16,6 @@ export default async (
},
) => {
const btc = new Btc(transport)
const path = bip32path || getPath({ currencyId, segwit })
const { bitcoinAddress } = await btc.getWalletPublicKey(path, verify, segwit)
return bitcoinAddress
const { bitcoinAddress, publicKey } = await btc.getWalletPublicKey(path, verify, segwit)
return { address: bitcoinAddress, path, publicKey }
}

8
src/internals/devices/getAddressForCurrency/ethereum.js

@ -2,16 +2,14 @@
import Eth from '@ledgerhq/hw-app-eth'
import type Transport from '@ledgerhq/hw-transport'
import { getPath } from 'internals/accounts/helpers'
export default async (
transport: Transport<*>,
currencyId: string,
bip32path: ?string,
path: string,
{ verify = false }: { verify: boolean },
) => {
const eth = new Eth(transport)
const path = bip32path || getPath({ currencyId })
const { address } = await eth.getAddress(path, verify)
return address
const { address, publicKey } = await eth.getAddress(path, verify)
return { path, address, publicKey }
}

11
src/internals/devices/getAddressForCurrency/index.js

@ -2,13 +2,14 @@
import type Transport from '@ledgerhq/hw-transport'
import btc from './btc'
import ethereum from './ethereum'
type Resolver = (
transport: Transport<*>,
currencyId: string,
bip32path: ?string, // if provided use this path, otherwise resolve it
path: string,
options: *,
) => Promise<string>
) => Promise<{ address: string, path: string, publicKey: string }>
type Module = (currencyId: string) => Resolver
@ -19,8 +20,10 @@ const all = {
bitcoin: btc,
bitcoin_testnet: btc,
ethereum: btc,
ethereum_testnet: btc,
ethereum,
ethereum_testnet: ethereum,
ethereum_classic: ethereum,
ethereum_classic_testnet: ethereum,
}
const getAddressForCurrency: Module = (currencyId: string) => all[currencyId] || fallback(currencyId)

2
src/internals/devices/index.js

@ -1,2 +1,4 @@
export listen from './listen'
export ensureDeviceApp from './ensureDeviceApp'
export getAddress from './getAddress'
export signTransaction from './signTransaction'

32
src/internals/devices/signTransaction.js

@ -0,0 +1,32 @@
// @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 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')
}
}

68
src/internals/devices/signTransactionForCurrency/ethereum.js

@ -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')}`
}

27
src/internals/devices/signTransactionForCurrency/index.js

@ -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

6
src/renderer/events.js

@ -2,6 +2,12 @@
// FIXME this file is spaghetti. we need one file per usecase.
// TODO to improve current state:
// a sendEventPromise version that returns a promise
// a sendEventObserver version that takes an observer & return a Subscription
// both of these implementation should have a unique requestId to ensure there is no collision
// events should all appear in the promise result / observer msgs as soon as they have this requestId
import { ipcRenderer } from 'electron'
import objectPath from 'object-path'
import debug from 'debug'

579
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save