diff --git a/package.json b/package.json
index 52dc402b..2b3bc8b5 100644
--- a/package.json
+++ b/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",
diff --git a/src/api/network.js b/src/api/network.js
index eac84f7f..0da18166 100644
--- a/src/api/network.js
+++ b/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 = (p: Promise, { url, method, startTime }): Promise =>
+const userFriendlyError = (p: Promise, { url, method, startTime, ...rest }): Promise =>
p.catch(error => {
let errorToThrow
if (error.response) {
@@ -47,6 +47,7 @@ const userFriendlyError = (p: Promise, { 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)
diff --git a/src/bridge/LibcoreBridge.js b/src/bridge/LibcoreBridge.js
index 642788d1..abfa5eae 100644
--- a/src/bridge/LibcoreBridge.js
+++ b/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 = {
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 = {
}
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) {
diff --git a/src/commands/index.js b/src/commands/index.js
index 8b1ff369..117a8b3f 100644
--- a/src/commands/index.js
+++ b/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> = [
libcoreGetFees,
libcoreGetVersion,
libcoreScanAccounts,
+ libcoreScanFromXPUB,
libcoreSignAndBroadcast,
libcoreSyncAccount,
libcoreValidAddress,
diff --git a/src/commands/libcoreGetFees.js b/src/commands/libcoreGetFees.js
index ab96ff30..4b3c8d5b 100644
--- a/src/commands/libcoreGetFees.js
+++ b/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 = 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
diff --git a/src/commands/libcoreScanFromXPUB.js b/src/commands/libcoreScanFromXPUB.js
new file mode 100644
index 00000000..1c595861
--- /dev/null
+++ b/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 = createCommand(
+ 'libcoreScanFromXPUB',
+ ({ currencyId, xpub, isSegwit, isUnsplit }) =>
+ fromPromise(
+ withLibcore(async core =>
+ scanAccountsFromXPUB({ core, currencyId, xpub, isSegwit, isUnsplit }),
+ ),
+ ),
+)
+
+export default cmd
diff --git a/src/commands/libcoreSignAndBroadcast.js b/src/commands/libcoreSignAndBroadcast.js
index e479c9f3..ad81d05c 100644
--- a/src/commands/libcoreSignAndBroadcast.js
+++ b/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) => void,
}): Promise {
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(),
})
diff --git a/src/commands/libcoreSyncAccount.js b/src/commands/libcoreSyncAccount.js
index bbc94644..21104d20 100644
--- a/src/commands/libcoreSyncAccount.js
+++ b/src/commands/libcoreSyncAccount.js
@@ -14,7 +14,7 @@ type Input = {
index: number,
}
-type Result = AccountRaw
+type Result = { rawAccount: AccountRaw, requiresCacheFlush: boolean }
const cmd: Command = createCommand('libcoreSyncAccount', accountInfos =>
fromPromise(withLibcore(core => syncAccount({ ...accountInfos, core }))),
diff --git a/src/components/AdvancedOptions/RippleKind.js b/src/components/AdvancedOptions/RippleKind.js
index b87cff5f..74b0b5a4 100644
--- a/src/components/AdvancedOptions/RippleKind.js
+++ b/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) => (
-
-
-
-
-
-
- {
- const tag = parseInt(str, 10)
- if (!isNaN(tag) && isFinite(tag)) onChangeTag(tag)
- else onChangeTag(undefined)
- }}
- />
-
-
-
-))
+const uint32maxPlus1 = BigNumber(2).pow(32)
+
+class RippleKind extends Component {
+ 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 (
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+export default translate()(RippleKind)
diff --git a/src/components/CurrentAddress/index.js b/src/components/CurrentAddress/index.js
index 488bf105..bf979fbc 100644
--- a/src/components/CurrentAddress/index.js
+++ b/src/components/CurrentAddress/index.js
@@ -143,7 +143,26 @@ class CurrentAddress extends PureComponent {
copyFeedback: false,
}
- _isUnmounted = false
+ componentWillUnmount() {
+ if (this._timeout) clearTimeout(this._timeout)
+ }
+
+ renderCopy = copy => {
+ const { t } = this.props
+ return (
+ }
+ 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 {
onClick={onVerify}
/>
) : null}
- (
- }
- label={t('app:common.copyAddress')}
- onClick={() => {
- this.setState({ copyFeedback: true })
- setTimeout(() => {
- if (this._isUnmounted) return
- this.setState({ copyFeedback: false })
- }, 1e3)
- copy()
- }}
- />
- )}
- />
+
)
diff --git a/src/components/DevToolsPage/AccountImporter.js b/src/components/DevToolsPage/AccountImporter.js
new file mode 100644
index 00000000..88afd7c9
--- /dev/null
+++ b/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 {
+ 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 (
+
+ {status === 'idle' ? (
+
+
+
+
+
+ {currency && (currency.supportsSegwit || supportsSplit) ? (
+
+ {supportsSplit && (
+
+
+ {'unsplit'}
+
+
+
+ )}
+ {currency.supportsSegwit && (
+
+
+ {'segwit'}
+
+
+
+ )}
+
+ ) : null}
+
+
+
+
+
+
+
+
+ ) : status === 'scanning' ? (
+
+
+
+ ) : status === 'finish' ? (
+ account ? (
+
+
+ {currency && }
+
+ {account.name}
+
+ {`${account.operations.length} operation(s)`}
+
+
+
+
+
+ ) : (
+
+ {'No accounts found or wrong xpub'}
+
+
+ )
+ ) : status === 'error' ? (
+
+
+
+
+
+
+ ) : null}
+
+ )
+ }
+}
+
+export default connect(
+ null,
+ mapDispatchToProps,
+)(AccountImporter)
diff --git a/src/components/DevToolsPage/index.js b/src/components/DevToolsPage/index.js
new file mode 100644
index 00000000..bc849837
--- /dev/null
+++ b/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 () => (
+
+
+
+)
diff --git a/src/components/EnsureDeviceApp.js b/src/components/EnsureDeviceApp.js
index 6bb0e89c..c453af7d 100644
--- a/src/components/EnsureDeviceApp.js
+++ b/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 =
@@ -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
},
},
)
diff --git a/src/components/MainSideBar/index.js b/src/components/MainSideBar/index.js
index 1c67e87f..e1cb9051 100644
--- a/src/components/MainSideBar/index.js
+++ b/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 = () => (
+
+ {'DEV'}
+
+)
+
class MainSideBar extends PureComponent {
push = (to: string) => {
const { push } = this.props
@@ -78,10 +98,11 @@ class MainSideBar extends PureComponent {
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 {
onClick={this.handleClickExchange}
isActive={pathname === '/exchange'}
/>
+ {developerMode && (
+
+ )}
!oldAppsInstallDisabled.includes(c.name)
+
const LoadingApp = () => (
@@ -285,7 +288,7 @@ class AppsList extends PureComponent {
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)}
/>
))}
diff --git a/src/components/ManagerPage/ManagerApp.js b/src/components/ManagerPage/ManagerApp.js
index 9f4b4494..eb50b073 100644
--- a/src/components/ManagerPage/ManagerApp.js
+++ b/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}
-
+ {onInstall ? (
+
+ ) : null}