Browse Source

Merge pull request #1511 from LedgerHQ/develop

1.1.11
master
Gaëtan Renaudeau 6 years ago
committed by GitHub
parent
commit
e8f200736f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      package.json
  2. 4
      src/api/network.js
  3. 13
      src/bridge/LibcoreBridge.js
  4. 2
      src/commands/index.js
  5. 32
      src/commands/libcoreGetFees.js
  6. 29
      src/commands/libcoreScanFromXPUB.js
  7. 30
      src/commands/libcoreSignAndBroadcast.js
  8. 2
      src/commands/libcoreSyncAccount.js
  9. 58
      src/components/AdvancedOptions/RippleKind.js
  10. 39
      src/components/CurrentAddress/index.js
  11. 207
      src/components/DevToolsPage/AccountImporter.js
  12. 11
      src/components/DevToolsPage/index.js
  13. 6
      src/components/EnsureDeviceApp.js
  14. 32
      src/components/MainSideBar/index.js
  15. 5
      src/components/ManagerPage/AppsList.js
  16. 26
      src/components/ManagerPage/ManagerApp.js
  17. 2
      src/components/base/Input/index.js
  18. 2
      src/components/layout/Default.js
  19. 2
      src/config/constants.js
  20. 9
      src/config/cryptocurrencies.js
  21. 1
      src/config/errors.js
  22. 2
      src/helpers/errors.js
  23. 13
      src/helpers/getAddressForCurrency/btc.js
  24. 135
      src/helpers/libcore.js
  25. 67
      src/internals/index.js
  26. 3
      src/logger/logger.js
  27. 7
      static/i18n/en/app.json
  28. 10
      static/i18n/en/errors.json
  29. 12
      yarn.lock

4
package.json

@ -38,8 +38,8 @@
"@ledgerhq/hw-app-xrp": "^4.13.0",
"@ledgerhq/hw-transport": "^4.13.0",
"@ledgerhq/hw-transport-node-hid": "4.22.0",
"@ledgerhq/ledger-core": "2.0.0-rc.6",
"@ledgerhq/live-common": "^3.4.0",
"@ledgerhq/ledger-core": "2.0.0-rc.7",
"@ledgerhq/live-common": "^3.5.1",
"animated": "^0.2.2",
"async": "^2.6.1",
"axios": "^0.18.0",

4
src/api/network.js

@ -6,7 +6,7 @@ import logger from 'logger'
import { LedgerAPIErrorWithMessage, LedgerAPIError, NetworkDown } from 'config/errors'
import anonymizer from 'helpers/anonymizer'
const userFriendlyError = <A>(p: Promise<A>, { url, method, startTime }): Promise<A> =>
const userFriendlyError = <A>(p: Promise<A>, { url, method, startTime, ...rest }): Promise<A> =>
p.catch(error => {
let errorToThrow
if (error.response) {
@ -47,6 +47,7 @@ const userFriendlyError = <A>(p: Promise<A>, { url, method, startTime }): Promis
})
}
logger.networkError({
...rest,
status,
url,
method,
@ -80,6 +81,7 @@ let implementation = (arg: Object) => {
const meta = {
url: arg.url,
method: arg.method,
data: arg.data,
startTime: Date.now(),
}
logger.network(meta)

13
src/bridge/LibcoreBridge.js

@ -9,7 +9,7 @@ import FeesBitcoinKind from 'components/FeesField/BitcoinKind'
import libcoreScanAccounts from 'commands/libcoreScanAccounts'
import libcoreSyncAccount from 'commands/libcoreSyncAccount'
import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast'
import libcoreGetFees from 'commands/libcoreGetFees'
import libcoreGetFees, { extractGetFeesInputFromAccount } from 'commands/libcoreGetFees'
import libcoreValidAddress from 'commands/libcoreValidAddress'
import { NotEnoughBalance } from 'config/errors'
import type { WalletBridge, EditProps } from './types'
@ -85,8 +85,7 @@ const getFees = async (a, transaction) => {
if (promise) return promise
promise = libcoreGetFees
.send({
accountId: a.id,
accountIndex: a.index,
...extractGetFeesInputFromAccount(a),
transaction: serializeTransaction(transaction),
})
.toPromise()
@ -127,8 +126,8 @@ const LibcoreBridge: WalletBridge<Transaction> = {
currencyId: account.currency.id,
})
.pipe(
map(rawSyncedAccount => {
const syncedAccount = decodeAccount(rawSyncedAccount)
map(({ rawAccount, requiresCacheFlush }) => {
const syncedAccount = decodeAccount(rawAccount)
return account => {
const accountOps = account.operations
const syncedOps = syncedAccount.operations
@ -142,11 +141,11 @@ const LibcoreBridge: WalletBridge<Transaction> = {
}
const hasChanged =
requiresCacheFlush ||
accountOps.length !== syncedOps.length || // size change, we do a full refresh for now...
(accountOps.length > 0 &&
syncedOps.length > 0 &&
(accountOps[0].accountId !== syncedOps[0].accountId ||
accountOps[0].id !== syncedOps[0].id || // if same size, only check if the last item has changed.
(accountOps[0].id !== syncedOps[0].id || // if same size, only check if the last item has changed.
accountOps[0].blockHeight !== syncedOps[0].blockHeight))
if (hasChanged) {

2
src/commands/index.js

@ -18,6 +18,7 @@ import isDashboardOpen from 'commands/isDashboardOpen'
import libcoreGetFees from 'commands/libcoreGetFees'
import libcoreGetVersion from 'commands/libcoreGetVersion'
import libcoreScanAccounts from 'commands/libcoreScanAccounts'
import libcoreScanFromXPUB from 'commands/libcoreScanFromXPUB'
import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast'
import libcoreSyncAccount from 'commands/libcoreSyncAccount'
import libcoreValidAddress from 'commands/libcoreValidAddress'
@ -49,6 +50,7 @@ const all: Array<Command<any, any>> = [
libcoreGetFees,
libcoreGetVersion,
libcoreScanAccounts,
libcoreScanFromXPUB,
libcoreSignAndBroadcast,
libcoreSyncAccount,
libcoreValidAddress,

32
src/commands/libcoreGetFees.js

@ -4,9 +4,17 @@ import { Observable } from 'rxjs'
import { BigNumber } from 'bignumber.js'
import withLibcore from 'helpers/withLibcore'
import { createCommand, Command } from 'helpers/ipc'
import type { Account } from '@ledgerhq/live-common/lib/types'
import * as accountIdHelper from 'helpers/accountId'
import { isValidAddress, libcoreAmountToBigNumber, bigNumberToLibcoreAmount } from 'helpers/libcore'
import {
isValidAddress,
libcoreAmountToBigNumber,
bigNumberToLibcoreAmount,
getOrCreateWallet,
} from 'helpers/libcore'
import { isSegwitPath, isUnsplitPath } from 'helpers/bip32'
import { InvalidAddress } from 'config/errors'
import { splittedCurrencies } from 'config/cryptocurrencies'
type BitcoinLikeTransaction = {
// TODO we rename this Transaction concept into transactionInput
@ -19,20 +27,38 @@ type Input = {
accountId: string,
accountIndex: number,
transaction: BitcoinLikeTransaction,
currencyId: string,
isSegwit: boolean,
isUnsplit: boolean,
}
export const extractGetFeesInputFromAccount = (a: Account) => {
const currencyId = a.currency.id
return {
accountId: a.id,
accountIndex: a.index,
currencyId,
isSegwit: isSegwitPath(a.freshAddressPath),
isUnsplit: isUnsplitPath(a.freshAddressPath, splittedCurrencies[currencyId]),
}
}
type Result = { totalFees: string }
const cmd: Command<Input, Result> = createCommand(
'libcoreGetFees',
({ accountId, accountIndex, transaction }) =>
({ accountId, currencyId, isSegwit, isUnsplit, accountIndex, transaction }) =>
Observable.create(o => {
let unsubscribed = false
const isCancelled = () => unsubscribed
withLibcore(async core => {
const { walletName } = accountIdHelper.decode(accountId)
const njsWallet = await core.getPoolInstance().getWallet(walletName)
const njsWallet = await getOrCreateWallet(core, walletName, {
currencyId,
isSegwit,
isUnsplit,
})
if (isCancelled()) return
const njsAccount = await njsWallet.getAccount(accountIndex)
if (isCancelled()) return

29
src/commands/libcoreScanFromXPUB.js

@ -0,0 +1,29 @@
// @flow
import { fromPromise } from 'rxjs/observable/fromPromise'
import type { AccountRaw } from '@ledgerhq/live-common/lib/types'
import { createCommand, Command } from 'helpers/ipc'
import withLibcore from 'helpers/withLibcore'
import { scanAccountsFromXPUB } from 'helpers/libcore'
type Input = {
currencyId: string,
xpub: string,
isSegwit: boolean,
isUnsplit: boolean,
}
type Result = AccountRaw
const cmd: Command<Input, Result> = createCommand(
'libcoreScanFromXPUB',
({ currencyId, xpub, isSegwit, isUnsplit }) =>
fromPromise(
withLibcore(async core =>
scanAccountsFromXPUB({ core, currencyId, xpub, isSegwit, isUnsplit }),
),
),
)
export default cmd

30
src/commands/libcoreSignAndBroadcast.js

@ -6,8 +6,13 @@ import type { OperationRaw } from '@ledgerhq/live-common/lib/types'
import Btc from '@ledgerhq/hw-app-btc'
import { Observable } from 'rxjs'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies'
import { isSegwitPath } from 'helpers/bip32'
import { libcoreAmountToBigNumber, bigNumberToLibcoreAmount } from 'helpers/libcore'
import { isSegwitPath, isUnsplitPath } from 'helpers/bip32'
import {
libcoreAmountToBigNumber,
bigNumberToLibcoreAmount,
getOrCreateWallet,
} from 'helpers/libcore'
import { splittedCurrencies } from 'config/cryptocurrencies'
import withLibcore from 'helpers/withLibcore'
import { createCommand, Command } from 'helpers/ipc'
@ -164,7 +169,6 @@ export async function doSignAndBroadcast({
accountId,
currencyId,
xpub,
freshAddress,
freshAddressPath,
index,
transaction,
@ -188,7 +192,10 @@ export async function doSignAndBroadcast({
onOperationBroadcasted: (optimisticOp: $Exact<OperationRaw>) => void,
}): Promise<void> {
const { walletName } = accountIdHelper.decode(accountId)
const njsWallet = await core.getPoolInstance().getWallet(walletName)
const isSegwit = isSegwitPath(freshAddressPath)
const isUnsplit = isUnsplitPath(freshAddressPath, splittedCurrencies[currencyId])
const njsWallet = await getOrCreateWallet(core, walletName, { currencyId, isSegwit, isUnsplit })
if (isCancelled()) return
const njsAccount = await njsWallet.getAccount(index)
if (isCancelled()) return
@ -237,6 +244,16 @@ export async function doSignAndBroadcast({
.asBitcoinLikeAccount()
.broadcastRawTransaction(Array.from(Buffer.from(signedTransaction, 'hex')))
const senders = builded
.getInputs()
.map(input => input.getAddress())
.filter(a => a)
const recipients = builded
.getOutputs()
.map(output => output.getAddress())
.filter(a => a)
const fee = libcoreAmountToBigNumber(builded.getFees())
// NB we don't check isCancelled() because the broadcast is not cancellable now!
@ -250,9 +267,8 @@ export async function doSignAndBroadcast({
fee: fee.toString(),
blockHash: null,
blockHeight: null,
// FIXME for senders and recipients, can we ask the libcore?
senders: [freshAddress],
recipients: [transaction.recipient],
senders,
recipients,
accountId,
date: new Date().toISOString(),
})

2
src/commands/libcoreSyncAccount.js

@ -14,7 +14,7 @@ type Input = {
index: number,
}
type Result = AccountRaw
type Result = { rawAccount: AccountRaw, requiresCacheFlush: boolean }
const cmd: Command<Input, Result> = createCommand('libcoreSyncAccount', accountInfos =>
fromPromise(withLibcore(core => syncAccount({ ...accountInfos, core }))),

58
src/components/AdvancedOptions/RippleKind.js

@ -1,5 +1,6 @@
// @flow
import React from 'react'
import React, { Component } from 'react'
import { BigNumber } from 'bignumber.js'
import { translate } from 'react-i18next'
import Box from 'components/base/Box'
@ -13,24 +14,37 @@ type Props = {
t: *,
}
export default translate()(({ tag, onChangeTag, t }: Props) => (
<Spoiler title={t('app:send.steps.amount.advancedOptions')}>
<Box horizontal align="center" flow={5}>
<Box style={{ width: 200 }}>
<Label>
<span>{t('app:send.steps.amount.rippleTag')}</span>
</Label>
</Box>
<Box grow>
<Input
value={String(tag || '')}
onChange={str => {
const tag = parseInt(str, 10)
if (!isNaN(tag) && isFinite(tag)) onChangeTag(tag)
else onChangeTag(undefined)
}}
/>
</Box>
</Box>
</Spoiler>
))
const uint32maxPlus1 = BigNumber(2).pow(32)
class RippleKind extends Component<Props> {
onChange = str => {
const { onChangeTag } = this.props
const tag = BigNumber(str.replace(/[^0-9]/g, ''))
if (!tag.isNaN() && tag.isFinite()) {
if (tag.isInteger() && tag.isPositive() && tag.lt(uint32maxPlus1)) {
onChangeTag(tag.toNumber())
}
} else {
onChangeTag(undefined)
}
}
render() {
const { tag, t } = this.props
return (
<Spoiler title={t('app:send.steps.amount.advancedOptions')}>
<Box horizontal align="center" flow={5}>
<Box style={{ width: 200 }}>
<Label>
<span>{t('app:send.steps.amount.rippleTag')}</span>
</Label>
</Box>
<Box grow>
<Input value={String(tag || '')} onChange={this.onChange} />
</Box>
</Box>
</Spoiler>
)
}
}
export default translate()(RippleKind)

39
src/components/CurrentAddress/index.js

@ -143,7 +143,26 @@ class CurrentAddress extends PureComponent<Props, { copyFeedback: boolean }> {
copyFeedback: false,
}
_isUnmounted = false
componentWillUnmount() {
if (this._timeout) clearTimeout(this._timeout)
}
renderCopy = copy => {
const { t } = this.props
return (
<FooterButton
icon={<IconCopy size={16} />}
label={t('app:common.copyAddress')}
onClick={() => {
this.setState({ copyFeedback: true })
this._timeout = setTimeout(() => this.setState({ copyFeedback: false }), 1e3)
copy()
}}
/>
)
}
_timeout: ?TimeoutID = null
render() {
const {
@ -214,23 +233,7 @@ class CurrentAddress extends PureComponent<Props, { copyFeedback: boolean }> {
onClick={onVerify}
/>
) : null}
<CopyToClipboard
data={address}
render={copy => (
<FooterButton
icon={<IconCopy size={16} />}
label={t('app:common.copyAddress')}
onClick={() => {
this.setState({ copyFeedback: true })
setTimeout(() => {
if (this._isUnmounted) return
this.setState({ copyFeedback: false })
}, 1e3)
copy()
}}
/>
)}
/>
<CopyToClipboard data={address} render={this.renderCopy} />
</Footer>
</Container>
)

207
src/components/DevToolsPage/AccountImporter.js

@ -0,0 +1,207 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import invariant from 'invariant'
import { connect } from 'react-redux'
import type { Currency, Account } from '@ledgerhq/live-common/lib/types'
import { decodeAccount } from 'reducers/accounts'
import { addAccount } from 'actions/accounts'
import FormattedVal from 'components/base/FormattedVal'
import Switch from 'components/base/Switch'
import Spinner from 'components/base/Spinner'
import Box, { Card } from 'components/base/Box'
import TranslatedError from 'components/TranslatedError'
import Button from 'components/base/Button'
import Input from 'components/base/Input'
import Label from 'components/base/Label'
import SelectCurrency from 'components/SelectCurrency'
import { CurrencyCircleIcon } from 'components/base/CurrencyBadge'
import { idleCallback } from 'helpers/promise'
import { splittedCurrencies } from 'config/cryptocurrencies'
import scanFromXPUB from 'commands/libcoreScanFromXPUB'
const mapDispatchToProps = {
addAccount,
}
type Props = {
addAccount: Account => void,
}
const INITIAL_STATE = {
status: 'idle',
currency: null,
xpub: '',
account: null,
isSegwit: true,
isUnsplit: false,
error: null,
}
type State = {
status: string,
currency: ?Currency,
xpub: string,
account: ?Account,
isSegwit: boolean,
isUnsplit: boolean,
error: ?Error,
}
class AccountImporter extends PureComponent<Props, State> {
state = INITIAL_STATE
onChangeCurrency = currency => {
if (currency.family !== 'bitcoin') return
this.setState({
currency,
isSegwit: !!currency.supportsSegwit,
isUnsplit: false,
})
}
onChangeXPUB = xpub => this.setState({ xpub })
onChangeSegwit = isSegwit => this.setState({ isSegwit })
onChangeUnsplit = isUnsplit => this.setState({ isUnsplit })
isValid = () => {
const { currency, xpub } = this.state
return !!currency && !!xpub
}
scan = async () => {
if (!this.isValid()) return
this.setState({ status: 'scanning' })
try {
const { currency, xpub, isSegwit, isUnsplit } = this.state
invariant(currency, 'no currency')
const rawAccount = await scanFromXPUB
.send({
currencyId: currency.id,
xpub,
isSegwit,
isUnsplit,
})
.toPromise()
const account = decodeAccount(rawAccount)
this.setState({ status: 'finish', account })
} catch (error) {
this.setState({ status: 'error', error })
}
}
import = async () => {
const { account } = this.state
invariant(account, 'no account')
await idleCallback()
this.props.addAccount(account)
this.reset()
}
reset = () => this.setState(INITIAL_STATE)
render() {
const { currency, xpub, isSegwit, isUnsplit, status, account, error } = this.state
const supportsSplit = !!currency && !!splittedCurrencies[currency.id]
return (
<Card title="Import from xpub" flow={3}>
{status === 'idle' ? (
<Fragment>
<Box flow={1}>
<Label>{'currency'}</Label>
<SelectCurrency autoFocus value={currency} onChange={this.onChangeCurrency} />
</Box>
{currency && (currency.supportsSegwit || supportsSplit) ? (
<Box horizontal justify="flex-end" align="center" flow={3}>
{supportsSplit && (
<Box horizontal align="center" flow={1}>
<Box ff="Museo Sans|Bold" fontSize={4}>
{'unsplit'}
</Box>
<Switch isChecked={isUnsplit} onChange={this.onChangeUnsplit} />
</Box>
)}
{currency.supportsSegwit && (
<Box horizontal align="center" flow={1}>
<Box ff="Museo Sans|Bold" fontSize={4}>
{'segwit'}
</Box>
<Switch isChecked={isSegwit} onChange={this.onChangeSegwit} />
</Box>
)}
</Box>
) : null}
<Box flow={1}>
<Label>{'xpub'}</Label>
<Input
placeholder="xpub"
value={xpub}
onChange={this.onChangeXPUB}
onEnter={this.scan}
/>
</Box>
<Box align="flex-end">
<Button primary small disabled={!this.isValid()} onClick={this.scan}>
{'scan'}
</Button>
</Box>
</Fragment>
) : status === 'scanning' ? (
<Box align="center" justify="center" p={5}>
<Spinner size={16} />
</Box>
) : status === 'finish' ? (
account ? (
<Box p={8} align="center" justify="center" flow={5} horizontal>
<Box horizontal flow={4} color="graphite" align="center">
{currency && <CurrencyCircleIcon size={64} currency={currency} />}
<Box>
<Box ff="Museo Sans|Bold">{account.name}</Box>
<FormattedVal
fontSize={2}
alwaysShowSign={false}
color="graphite"
unit={account.unit}
showCode
val={account.balance || 0}
/>
<Box fontSize={2}>{`${account.operations.length} operation(s)`}</Box>
</Box>
</Box>
<Button outline small disabled={!account} onClick={this.import}>
{'import'}
</Button>
</Box>
) : (
<Box align="center" justify="center" p={5} flow={4}>
<Box>{'No accounts found or wrong xpub'}</Box>
<Button primary onClick={this.reset} small autoFocus>
{'Reset'}
</Button>
</Box>
)
) : status === 'error' ? (
<Box align="center" justify="center" p={5} flow={4}>
<Box>
<TranslatedError error={error} />
</Box>
<Button primary onClick={this.reset} small autoFocus>
{'Reset'}
</Button>
</Box>
) : null}
</Card>
)
}
}
export default connect(
null,
mapDispatchToProps,
)(AccountImporter)

11
src/components/DevToolsPage/index.js

@ -0,0 +1,11 @@
import React from 'react'
import Box from 'components/base/Box'
import AccountImporter from './AccountImporter'
export default () => (
<Box flow={2}>
<AccountImporter />
</Box>
)

6
src/components/EnsureDeviceApp.js

@ -20,7 +20,7 @@ import IconUsb from 'icons/Usb'
import type { Device } from 'types/common'
import { WrongDeviceForAccount, CantOpenDevice, BtcUnmatchedApp } from 'config/errors'
import { WrongDeviceForAccount, CantOpenDevice, UpdateYourApp } from 'config/errors'
import { getCurrentDevice } from 'reducers/devices'
const usbIcon = <IconUsb size={16} />
@ -61,10 +61,10 @@ class EnsureDeviceApp extends Component<{
},
{
shouldThrow: (err: Error) => {
const isWrongApp = err instanceof BtcUnmatchedApp
const isWrongDevice = err instanceof WrongDeviceForAccount
const isCantOpenDevice = err instanceof CantOpenDevice
return isWrongApp || isWrongDevice || isCantOpenDevice
const isUpdateYourApp = err instanceof UpdateYourApp
return isWrongDevice || isCantOpenDevice || isUpdateYourApp
},
},
)

32
src/components/MainSideBar/index.js

@ -19,6 +19,7 @@ import { i } from 'helpers/staticPath'
import { accountsSelector } from 'reducers/accounts'
import { openModal } from 'reducers/modals'
import { getUpdateStatus } from 'reducers/update'
import { developerModeSelector } from 'reducers/settings'
import { SideBarList, SideBarListItem } from 'components/base/SideBar'
import Box from 'components/base/Box'
@ -38,6 +39,7 @@ import TopGradient from './TopGradient'
const mapStateToProps = state => ({
accounts: accountsSelector(state),
updateStatus: getUpdateStatus(state),
developerMode: developerModeSelector(state),
})
const mapDispatchToProps = {
@ -52,8 +54,26 @@ type Props = {
push: string => void,
openModal: string => void,
updateStatus: UpdateStatus,
developerMode: boolean,
}
const IconDev = () => (
<div
style={{
width: 16,
height: 16,
fontSize: 10,
fontFamily: 'monospace',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{'DEV'}
</div>
)
class MainSideBar extends PureComponent<Props> {
push = (to: string) => {
const { push } = this.props
@ -78,10 +98,11 @@ class MainSideBar extends PureComponent<Props> {
handleOpenReceiveModal = () => this.props.openModal(MODAL_RECEIVE)
handleClickManager = () => this.push('/manager')
handleClickExchange = () => this.push('/exchange')
handleClickDev = () => this.push('/dev')
handleOpenImportModal = () => this.props.openModal(MODAL_ADD_ACCOUNTS)
render() {
const { t, accounts, location, updateStatus } = this.props
const { t, accounts, location, updateStatus, developerMode } = this.props
const { pathname } = location
const addAccountButton = (
@ -133,6 +154,15 @@ class MainSideBar extends PureComponent<Props> {
onClick={this.handleClickExchange}
isActive={pathname === '/exchange'}
/>
{developerMode && (
<SideBarListItem
label={t('app:sidebar.developer')}
icon={IconDev}
iconActiveColor="wallet"
onClick={this.handleClickDev}
isActive={pathname === '/dev'}
/>
)}
</SideBarList>
<Space of={40} />
<SideBarList

5
src/components/ManagerPage/AppsList.js

@ -71,6 +71,9 @@ type State = {
mode: Mode,
}
const oldAppsInstallDisabled = ['ZenCash', 'Ripple']
const canHandleInstall = c => !oldAppsInstallDisabled.includes(c.name)
const LoadingApp = () => (
<FakeManagerAppContainer noShadow align="center" justify="center" style={{ height: 90 }}>
<Spinner size={16} color="rgba(0, 0, 0, 0.3)" />
@ -285,7 +288,7 @@ class AppsList extends PureComponent<Props, State> {
name={c.name}
version={`Version ${c.version}`}
icon={ICONS_FALLBACK[c.icon] || c.icon}
onInstall={this.handleInstallApp(c)}
onInstall={canHandleInstall(c) ? this.handleInstallApp(c) : null}
onUninstall={this.handleUninstallApp(c)}
/>
))}

26
src/components/ManagerPage/ManagerApp.js

@ -49,7 +49,7 @@ type Props = {
name: string,
version: string,
icon: string,
onInstall: Function,
onInstall?: Function,
onUninstall: Function,
}
@ -64,17 +64,19 @@ function ManagerApp({ name, version, icon, onInstall, onUninstall, t }: Props) {
{version}
</Text>
</Box>
<Button
outline
onClick={onInstall}
event={'Manager Install Click'}
eventProperties={{
appName: name,
appVersion: version,
}}
>
{t('app:manager.apps.install')}
</Button>
{onInstall ? (
<Button
outline
onClick={onInstall}
event={'Manager Install Click'}
eventProperties={{
appName: name,
appVersion: version,
}}
>
{t('app:manager.apps.install')}
</Button>
) : null}
<Button
outline
onClick={onUninstall}

2
src/components/base/Input/index.js

@ -92,7 +92,7 @@ type Props = {
keepEvent?: boolean,
onBlur: (SyntheticInputEvent<HTMLInputElement>) => void,
onChange?: Function,
onEnter?: (SyntheticKeyboardEvent<HTMLInputElement>) => void,
onEnter?: (SyntheticKeyboardEvent<HTMLInputElement>) => *,
onEsc?: (SyntheticKeyboardEvent<HTMLInputElement>) => void,
onFocus: (SyntheticInputEvent<HTMLInputElement>) => void,
renderLeft?: any,

2
src/components/layout/Default.js

@ -19,6 +19,7 @@ import AccountPage from 'components/AccountPage'
import DashboardPage from 'components/DashboardPage'
import ManagerPage from 'components/ManagerPage'
import ExchangePage from 'components/ExchangePage'
import DevToolsPage from 'components/DevToolsPage'
import SettingsPage from 'components/SettingsPage'
import KeyboardContent from 'components/KeyboardContent'
import PerfIndicator from 'components/PerfIndicator'
@ -110,6 +111,7 @@ class Default extends Component<Props> {
<Route path="/manager" component={ManagerPage} />
<Route path="/exchange" component={ExchangePage} />
<Route path="/account/:id" component={AccountPage} />
<Route path="/dev" component={DevToolsPage} />
</Main>
</Box>
</Box>

2
src/config/constants.js

@ -45,7 +45,7 @@ export const SYNC_ALL_INTERVAL = 120 * 1000
export const SYNC_BOOT_DELAY = 2 * 1000
export const SYNC_PENDING_INTERVAL = 10 * 1000
export const SYNC_MAX_CONCURRENT = intFromEnv('LEDGER_SYNC_MAX_CONCURRENT', 1)
export const SYNC_TIMEOUT = intFromEnv('SYNC_TIMEOUT', 60 * 1000)
export const SYNC_TIMEOUT = intFromEnv('SYNC_TIMEOUT', 5 * 60 * 1000)
// Endpoints...

9
src/config/cryptocurrencies.js

@ -35,3 +35,12 @@ export const listCryptoCurrencies = memoize((withDevCrypto?: boolean) =>
.filter(c => supported.includes(c.id))
.sort((a, b) => a.name.localeCompare(b.name)),
)
export const splittedCurrencies = {
bitcoin_cash: {
coinType: 0,
},
bitcoin_gold: {
coinType: 0,
},
}

1
src/config/errors.js

@ -33,6 +33,7 @@ export const NotEnoughBalance = createCustomErrorClass('NotEnoughBalance')
export const PasswordsDontMatchError = createCustomErrorClass('PasswordsDontMatch')
export const PasswordIncorrectError = createCustomErrorClass('PasswordIncorrect')
export const TimeoutTagged = createCustomErrorClass('TimeoutTagged')
export const UpdateYourApp = createCustomErrorClass('UpdateYourApp')
export const UserRefusedAddress = createCustomErrorClass('UserRefusedAddress')
export const UserRefusedFirmwareUpdate = createCustomErrorClass('UserRefusedFirmwareUpdate')
export const UserRefusedOnDevice = createCustomErrorClass('UserRefusedOnDevice') // TODO rename because it's just for transaction refusal

2
src/helpers/errors.js

@ -6,10 +6,10 @@ const errorClasses = {}
export const createCustomErrorClass = (name: string): Class<any> => {
const C = function CustomError(message?: string, fields?: Object) {
Object.assign(this, fields)
this.name = name
this.message = message || name
this.stack = new Error().stack
Object.assign(this, fields)
}
// $FlowFixMe
C.prototype = new Error()

13
src/helpers/getAddressForCurrency/btc.js

@ -3,9 +3,13 @@
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import Btc from '@ledgerhq/hw-app-btc'
import type Transport from '@ledgerhq/hw-transport'
import { BtcUnmatchedApp } from 'config/errors'
import { BtcUnmatchedApp, UpdateYourApp } from 'config/errors'
import getBitcoinLikeInfo from '../devices/getBitcoinLikeInfo'
const oldP2SH = {
digibyte: 5,
}
export default async (
transport: Transport<*>,
currency: CryptoCurrency,
@ -25,6 +29,13 @@ export default async (
if (bitcoinLikeInfo) {
const { P2SH, P2PKH } = await getBitcoinLikeInfo(transport)
if (P2SH !== bitcoinLikeInfo.P2SH || P2PKH !== bitcoinLikeInfo.P2PKH) {
if (
currency.id in oldP2SH &&
P2SH === oldP2SH[currency.id] &&
P2PKH === bitcoinLikeInfo.P2PKH
) {
throw new UpdateYourApp(`UpdateYourApp ${currency.id}`, currency)
}
throw new BtcUnmatchedApp(`BtcUnmatchedApp ${currency.id}`, currency)
}
}

135
src/helpers/libcore.js

@ -7,7 +7,7 @@ import { BigNumber } from 'bignumber.js'
import Btc from '@ledgerhq/hw-app-btc'
import { withDevice } from 'helpers/deviceAccess'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies'
import { SHOW_LEGACY_NEW_ACCOUNT } from 'config/constants'
import { SHOW_LEGACY_NEW_ACCOUNT, SYNC_TIMEOUT } from 'config/constants'
import type { AccountRaw, OperationRaw, OperationType } from '@ledgerhq/live-common/lib/types'
import type { NJSAccount, NJSOperation } from '@ledgerhq/ledger-core/src/ledgercore_doc'
@ -15,20 +15,11 @@ import type { NJSAccount, NJSOperation } from '@ledgerhq/ledger-core/src/ledgerc
import { isSegwitPath, isUnsplitPath } from 'helpers/bip32'
import * as accountIdHelper from 'helpers/accountId'
import { NoAddressesFound } from 'config/errors'
import { splittedCurrencies } from 'config/cryptocurrencies'
import { deserializeError } from './errors'
import { getAccountPlaceholderName, getNewAccountPlaceholderName } from './accountName'
import { timeoutTagged } from './promise'
// TODO: put that info inside currency itself
const SPLITTED_CURRENCIES = {
bitcoin_cash: {
coinType: 0,
},
bitcoin_gold: {
coinType: 0,
},
}
export function isValidAddress(core: *, currency: *, address: string): boolean {
const addr = new core.NJSAddress(address, currency)
return addr.isValid(address, currency)
@ -75,7 +66,7 @@ export async function scanAccountsOnDevice(props: Props): Promise<AccountRaw[]>
}
// TODO: put that info inside currency itself
if (currencyId in SPLITTED_CURRENCIES) {
if (currencyId in splittedCurrencies) {
const splittedAccounts = await scanAccountsOnDeviceBySegwit({
...commonParams,
isSegwit: false,
@ -109,7 +100,7 @@ function encodeWalletName({
isSegwit: boolean,
isUnsplit: boolean,
}) {
const splitConfig = isUnsplit ? SPLITTED_CURRENCIES[currencyId] || null : null
const splitConfig = isUnsplit ? splittedCurrencies[currencyId] || null : null
return `${publicKey}__${currencyId}${isSegwit ? '_segwit' : ''}${splitConfig ? '_unsplit' : ''}`
}
@ -133,7 +124,7 @@ async function scanAccountsOnDeviceBySegwit({
isUnsplit: boolean,
}): Promise<AccountRaw[]> {
const customOpts =
isUnsplit && SPLITTED_CURRENCIES[currencyId] ? SPLITTED_CURRENCIES[currencyId] : null
isUnsplit && splittedCurrencies[currencyId] ? splittedCurrencies[currencyId] : null
const { coinType } = customOpts ? customOpts.coinType : getCryptoCurrencyById(currencyId)
const path = `${isSegwit ? '49' : '44'}'/${coinType}'`
@ -147,7 +138,7 @@ async function scanAccountsOnDeviceBySegwit({
const walletName = encodeWalletName({ publicKey, currencyId, isSegwit, isUnsplit })
// retrieve or create the wallet
const wallet = await getOrCreateWallet(core, walletName, currencyId, isSegwit, isUnsplit)
const wallet = await getOrCreateWallet(core, walletName, { currencyId, isSegwit, isUnsplit })
const accountsCount = await wallet.getAccountCount()
// recursively scan all accounts on device on the given app
@ -155,6 +146,7 @@ async function scanAccountsOnDeviceBySegwit({
const accounts = await scanNextAccount({
core,
wallet,
walletName,
devicePath,
currencyId,
accountsCount,
@ -232,6 +224,7 @@ const coreSyncAccount = (core, account) =>
async function scanNextAccount(props: {
// $FlowFixMe
wallet: NJSWallet,
walletName: string,
core: *,
devicePath: string,
currencyId: string,
@ -247,6 +240,7 @@ async function scanNextAccount(props: {
const {
core,
wallet,
walletName,
devicePath,
currencyId,
accountsCount,
@ -285,6 +279,7 @@ async function scanNextAccount(props: {
isUnsplit,
accountIndex,
wallet,
walletName,
currencyId,
core,
ops,
@ -317,20 +312,26 @@ const createWalletConfig = (core, configMap = {}) => {
return config
}
async function getOrCreateWallet(
export async function getOrCreateWallet(
core: *,
WALLET_IDENTIFIER: string,
currencyId: string,
isSegwit: boolean,
isUnsplit: boolean,
walletName: string,
{
currencyId,
isSegwit,
isUnsplit,
}: {
currencyId: string,
isSegwit: boolean,
isUnsplit: boolean,
},
): NJSWallet {
const pool = core.getPoolInstance()
try {
const wallet = await timeoutTagged('getWallet', 5000, pool.getWallet(WALLET_IDENTIFIER))
const wallet = await timeoutTagged('getWallet', 5000, pool.getWallet(walletName))
return wallet
} catch (err) {
const currency = await timeoutTagged('getCurrency', 5000, pool.getCurrency(currencyId))
const splitConfig = isUnsplit ? SPLITTED_CURRENCIES[currencyId] || null : null
const splitConfig = isUnsplit ? splittedCurrencies[currencyId] || null : null
const coinType = splitConfig ? splitConfig.coinType : '<coin_type>'
const walletConfig = isSegwit
? {
@ -346,7 +347,7 @@ async function getOrCreateWallet(
const wallet = await timeoutTagged(
'createWallet',
10000,
core.getPoolInstance().createWallet(WALLET_IDENTIFIER, currency, njsWalletConfig),
core.getPoolInstance().createWallet(walletName, currency, njsWalletConfig),
)
return wallet
}
@ -357,6 +358,7 @@ async function buildAccountRaw({
isSegwit,
isUnsplit,
wallet,
walletName,
currencyId,
core,
accountIndex,
@ -366,6 +368,7 @@ async function buildAccountRaw({
isSegwit: boolean,
isUnsplit: boolean,
wallet: NJSWallet,
walletName: string,
currencyId: string,
accountIndex: number,
core: *,
@ -430,7 +433,7 @@ async function buildAccountRaw({
type: 'libcore',
version: '1',
xpub,
walletName: wallet.getName(),
walletName,
}),
xpub,
path: walletPath,
@ -511,30 +514,17 @@ export async function syncAccount({
index: number,
}) {
const decodedAccountId = accountIdHelper.decode(accountId)
const { walletName } = decodedAccountId
const isSegwit = isSegwitPath(freshAddressPath)
const isUnsplit = isUnsplitPath(freshAddressPath, SPLITTED_CURRENCIES[currencyId])
let njsWallet
try {
njsWallet = await timeoutTagged(
'getWallet',
10000,
core.getPoolInstance().getWallet(decodedAccountId.walletName),
)
} catch (e) {
logger.warn(`Have to reimport the account... (${e})`)
njsWallet = await getOrCreateWallet(
core,
decodedAccountId.walletName,
currencyId,
isSegwit,
isUnsplit,
)
}
const isUnsplit = isUnsplitPath(freshAddressPath, splittedCurrencies[currencyId])
const njsWallet = await getOrCreateWallet(core, walletName, { currencyId, isSegwit, isUnsplit })
let njsAccount
let requiresCacheFlush = false
try {
njsAccount = await timeoutTagged('getAccount', 10000, njsWallet.getAccount(index))
} catch (e) {
requiresCacheFlush = true
logger.warn(`Have to recreate the account... (${e.message})`)
const extendedInfos = await timeoutTagged(
'getEKACI',
@ -553,7 +543,7 @@ export async function syncAccount({
unsub()
const query = njsAccount.queryOperations()
const ops = await timeoutTagged('ops', 30000, query.complete().execute())
const ops = await timeoutTagged('ops', SYNC_TIMEOUT, query.complete().execute())
const njsBalance = await timeoutTagged('getBalance', 10000, njsAccount.getBalance())
const syncedRawAccount = await buildAccountRaw({
@ -562,6 +552,7 @@ export async function syncAccount({
isUnsplit,
accountIndex: index,
wallet: njsWallet,
walletName,
currencyId,
core,
ops,
@ -571,7 +562,7 @@ export async function syncAccount({
logger.log(`Synced account [${syncedRawAccount.name}]: ${syncedRawAccount.balance}`)
return syncedRawAccount
return { rawAccount: syncedRawAccount, requiresCacheFlush }
}
export function libcoreAmountToBigNumber(njsAmount: *): BigNumber {
@ -581,3 +572,59 @@ export function libcoreAmountToBigNumber(njsAmount: *): BigNumber {
export function bigNumberToLibcoreAmount(core: *, njsWalletCurrency: *, bigNumber: BigNumber) {
return new core.NJSAmount(njsWalletCurrency, 0).fromHex(njsWalletCurrency, bigNumber.toString(16))
}
export async function scanAccountsFromXPUB({
core,
currencyId,
xpub,
isSegwit,
isUnsplit,
}: {
core: *,
currencyId: string,
xpub: string,
isSegwit: boolean,
isUnsplit: boolean,
}) {
const currency = getCryptoCurrencyById(currencyId)
const walletName = encodeWalletName({
publicKey: `debug_${xpub}`,
currencyId,
isSegwit,
isUnsplit,
})
const wallet = await getOrCreateWallet(core, walletName, { currencyId, isSegwit, isUnsplit })
await wallet.eraseDataSince(new Date(0))
const index = 0
const extendedInfos = {
index,
owners: ['main'],
derivations: [
`${isSegwit ? '49' : '44'}'/${currency.coinType}'`,
`${isSegwit ? '49' : '44'}'/${currency.coinType}'/0`,
],
extendedKeys: [xpub],
}
const account = await wallet.newAccountWithExtendedKeyInfo(extendedInfos)
await coreSyncAccount(core, account)
const query = account.queryOperations()
const ops = await query.complete().execute()
const rawAccount = await buildAccountRaw({
njsAccount: account,
isSegwit,
isUnsplit,
accountIndex: index,
wallet,
walletName,
currencyId,
core,
ops,
})
return rawAccount
}

67
src/internals/index.js

@ -63,34 +63,45 @@ process.on('message', m => {
}
const startTime = Date.now()
logger.onCmd('cmd.START', id, 0, data)
subscriptions[requestId] = cmd.impl(data).subscribe({
next: data => {
logger.onCmd('cmd.NEXT', id, Date.now() - startTime, data)
process.send({
type: 'cmd.NEXT',
requestId,
data,
})
},
complete: () => {
delete subscriptions[requestId]
logger.onCmd('cmd.COMPLETE', id, Date.now() - startTime)
process.send({
type: 'cmd.COMPLETE',
requestId,
})
},
error: error => {
logger.warn('Command error:', error)
delete subscriptions[requestId]
logger.onCmd('cmd.ERROR', id, Date.now() - startTime, error)
process.send({
type: 'cmd.ERROR',
requestId,
data: serializeError(error),
})
},
})
try {
subscriptions[requestId] = cmd.impl(data).subscribe({
next: data => {
logger.onCmd('cmd.NEXT', id, Date.now() - startTime, data)
process.send({
type: 'cmd.NEXT',
requestId,
data,
})
},
complete: () => {
delete subscriptions[requestId]
logger.onCmd('cmd.COMPLETE', id, Date.now() - startTime)
process.send({
type: 'cmd.COMPLETE',
requestId,
})
},
error: error => {
logger.warn('Command error:', error)
delete subscriptions[requestId]
logger.onCmd('cmd.ERROR', id, Date.now() - startTime, error)
process.send({
type: 'cmd.ERROR',
requestId,
data: serializeError(error),
})
},
})
} catch (error) {
logger.warn('Command error:', error)
delete subscriptions[requestId]
logger.onCmd('cmd.ERROR', id, Date.now() - startTime, error)
process.send({
type: 'cmd.ERROR',
requestId,
data: serializeError(error),
})
}
} else if (m.type === 'command-unsubscribe') {
const { requestId } = m
const sub = subscriptions[requestId]

3
src/logger/logger.js

@ -273,6 +273,7 @@ export default {
status,
error,
responseTime,
...rest
}: {
method: string,
url: string,
@ -285,7 +286,7 @@ export default {
0,
)}ms`
if (logNetwork) {
logger.log('info', log, { type: 'network-error', status, method })
logger.log('info', log, { type: 'network-error', status, method, ...rest })
}
captureBreadcrumb({
category: 'network',

7
static/i18n/en/app.json

@ -78,7 +78,8 @@
"menu": "Menu",
"accounts": "Accounts ({{count}})",
"manager": "Manager",
"exchange": "Buy/Trade"
"exchange": "Buy/Trade",
"developer": "Dev tools"
},
"account": {
"lastOperations": "Last operations",
@ -133,7 +134,7 @@
"title": "Current address",
"for": "Address for account <1><0>{{accountName}}</0></1>",
"messageIfUnverified": "Verify the address on your device for optimal security. Press the right button to confirm.",
"messageIfAccepted": "{{currencyName}} address confirmed on your device. Carefully verify when you copy and paste it.",
"messageIfAccepted": "{{currencyName}} address confirmed. Please verify the address if you copy/paste it or if you scan the QR code.",
"messageIfSkipped": "Your receive address has not been confirmed on your Ledger device. Please verify your {{currencyName}} address for optimal security."
},
"deviceConnect": {
@ -315,7 +316,7 @@
},
"verification": {
"title": "Verification",
"warning": "Carefully verify all transaction details now displayed on your device screen\n",
"warning": "Please verify all transaction details now displayed on your device\n",
"body": "Once verified, press the right button to confirm and sign the transaction"
},
"confirmation": {

10
static/i18n/en/errors.json

@ -5,7 +5,7 @@
},
"AccountNameRequired": {
"title": "An account name is required",
"description": "Please provide with an account name"
"description": "Please provide an account name"
},
"BtcUnmatchedApp": {
"title": "That's the wrong app",
@ -80,8 +80,8 @@
"description": "Your device was locked. Please unlock it."
},
"ManagerNotEnoughSpace": {
"title": "Sorry, insufficient device storage",
"description": "Uninstall some apps to increase available storage and try again."
"title": "Sorry, not enough storage left",
"description": "Please uninstall some apps to make space. This will not affect your crypto assets."
},
"ManagerUninstallBTCDep": {
"title": "Sorry, this app is required",
@ -139,6 +139,10 @@
"title": "Receive address rejected",
"description": "Please try again or contact Ledger Support"
},
"UpdateYourApp": {
"title": "App update required. Uninstall and reinstall the {{managerAppName}} app in the Manager",
"description": null
},
"WebsocketConnectionError": {
"title": "Sorry, try again (websocket error).",
"description": null

12
yarn.lock

@ -1536,9 +1536,9 @@
dependencies:
events "^2.0.0"
"@ledgerhq/ledger-core@2.0.0-rc.6":
version "2.0.0-rc.6"
resolved "https://registry.yarnpkg.com/@ledgerhq/ledger-core/-/ledger-core-2.0.0-rc.6.tgz#a08c84bd91c680cd731e1030ce081a9b568a2c47"
"@ledgerhq/ledger-core@2.0.0-rc.7":
version "2.0.0-rc.7"
resolved "https://registry.yarnpkg.com/@ledgerhq/ledger-core/-/ledger-core-2.0.0-rc.7.tgz#36a2573f01a1e19c51c6e39692e6b0f8be6c3a77"
dependencies:
"@ledgerhq/hw-app-btc" "^4.7.3"
"@ledgerhq/hw-transport-node-hid" "^4.7.6"
@ -1549,9 +1549,9 @@
npm "^5.7.1"
prebuild-install "^2.2.2"
"@ledgerhq/live-common@^3.4.0":
version "3.4.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-3.4.0.tgz#49a9f8865d2e3ea898cfba15d69bdaf32c949f80"
"@ledgerhq/live-common@^3.5.1":
version "3.5.1"
resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-3.5.1.tgz#dab3eb061f361999a9e04ef564808831faac61ea"
dependencies:
axios "^0.18.0"
bignumber.js "^7.2.1"

Loading…
Cancel
Save