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}