From 72aa39ef3dcfce680070e1239fa95d038171cfe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Wed, 10 Oct 2018 21:02:26 +0200 Subject: [PATCH] Migrate to new live-common major --- package.json | 3 +- src/actions/general.js | 2 +- src/bridge/EthereumJSBridge.js | 42 +-- src/bridge/LibcoreBridge.js | 10 +- src/bridge/RippleJSBridge.js | 33 ++- src/bridge/makeMockBridge.js | 2 +- src/commands/libcoreGetFees.js | 29 ++- src/commands/libcoreScanFromXPUB.js | 8 +- src/commands/libcoreSignAndBroadcast.js | 64 +++-- src/commands/libcoreSyncAccount.js | 16 +- .../AccountPage/AccountHeaderActions.js | 2 +- src/components/AccountPage/index.js | 2 +- .../DevToolsPage/AccountImporter.js | 14 +- src/components/EnsureDeviceApp.js | 11 +- src/components/QRCodeExporter.js | 19 +- .../modals/AccountSettingRenderBody.js | 2 +- src/components/modals/AddAccounts/index.js | 2 +- .../AddAccounts/steps/03-step-import.js | 2 +- src/components/modals/Debug.js | 20 +- .../Receive/steps/04-step-receive-funds.js | 4 +- src/config/cryptocurrencies.js | 9 - src/helpers/accountId.js | 22 -- src/helpers/accountModel.js | 145 +++++++++++ src/helpers/accountName.js | 20 -- src/helpers/accountOrdering.js | 38 --- src/helpers/bip32.js | 16 -- src/helpers/derivations.js | 38 --- src/helpers/isAccountEmpty.js | 10 - src/helpers/libcore.js | 239 +++++++++--------- src/reducers/accounts.js | 4 +- yarn.lock | 95 ++++++- 31 files changed, 521 insertions(+), 402 deletions(-) delete mode 100644 src/helpers/accountId.js create mode 100644 src/helpers/accountModel.js delete mode 100644 src/helpers/accountName.js delete mode 100644 src/helpers/accountOrdering.js delete mode 100644 src/helpers/bip32.js delete mode 100644 src/helpers/derivations.js delete mode 100644 src/helpers/isAccountEmpty.js diff --git a/package.json b/package.json index 582f457b..1c643571 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@ledgerhq/hw-transport": "^4.13.0", "@ledgerhq/hw-transport-node-hid": "4.22.0", "@ledgerhq/ledger-core": "2.0.0-rc.8", - "@ledgerhq/live-common": "3.8.0", + "@ledgerhq/live-common": "/Users/gre/dev/ledger-live-common", "animated": "^0.2.2", "async": "^2.6.1", "axios": "^0.18.0", @@ -69,6 +69,7 @@ "measure-scrollbar": "^1.1.0", "moment": "^2.22.2", "qrcode": "^1.2.0", + "qrloop": "^0.6.1", "qs": "^6.5.1", "raven": "^2.5.0", "raven-js": "^3.24.2", diff --git a/src/actions/general.js b/src/actions/general.js index 35607cfd..9a133e31 100644 --- a/src/actions/general.js +++ b/src/actions/general.js @@ -8,7 +8,7 @@ import { getOrderAccounts, } from 'reducers/settings' import { accountsSelector } from 'reducers/accounts' -import { sortAccounts } from 'helpers/accountOrdering' +import { sortAccounts } from '@ledgerhq/live-common/lib/helpers/account' const accountsBtcBalanceSelector = createSelector( accountsSelector, diff --git a/src/bridge/EthereumJSBridge.js b/src/bridge/EthereumJSBridge.js index 69be1675..f0f6ea05 100644 --- a/src/bridge/EthereumJSBridge.js +++ b/src/bridge/EthereumJSBridge.js @@ -8,14 +8,22 @@ import AdvancedOptions from 'components/AdvancedOptions/EthereumKind' import throttle from 'lodash/throttle' import flatMap from 'lodash/flatMap' import uniqBy from 'lodash/uniqBy' +import { + getDerivationModesForCurrency, + getDerivationScheme, + runDerivationScheme, + getMandatoryEmptyAccountSkip, +} from '@ledgerhq/live-common/lib/helpers/derivation' +import { + getAccountPlaceholderName, + getNewAccountPlaceholderName, +} from '@ledgerhq/live-common/lib/helpers/account' import type { Account, Operation } from '@ledgerhq/live-common/lib/types' import eip55 from 'eip55' import { apiForCurrency } from 'api/Ethereum' import type { Tx } from 'api/Ethereum' -import { getDerivations } from 'helpers/derivations' import getAddressCommand from 'commands/getAddress' import signTransactionCommand from 'commands/signTransaction' -import { getAccountPlaceholderName, getNewAccountPlaceholderName } from 'helpers/accountName' import { NotEnoughBalance, FeeNotLoaded, ETHAddressNonEIP } from 'config/errors' import type { EditProps, WalletBridge } from './types' @@ -211,7 +219,7 @@ const EthereumBridge: WalletBridge = { async function stepAddress( index, { address, path: freshAddressPath, publicKey }, - isStandard, + derivationMode, shouldSkipEmpty, ): { account?: Account, complete?: boolean } { const balance = await api.getAccountBalance(address) @@ -222,19 +230,21 @@ const EthereumBridge: WalletBridge = { if (finished) return { complete: true } const freshAddress = address - const accountId = `ethereumjs:${currency.id}:${address}:${publicKey}` + const accountId = `ethereumjs:2:${currency.id}:${address}:${publicKey}` if (txs.length === 0 && balance.isZero()) { // this is an empty account - if (isStandard) { + if (derivationMode === '') { + // is standard derivation if (newAccountCount === 0) { // first zero account will emit one account as opportunity to create a new account.. const account: $Exact = { id: accountId, - xpub: '', + seedIdentifier: freshAddress, freshAddress, freshAddressPath, - name: getNewAccountPlaceholderName(currency, index), + derivationMode, + name: getNewAccountPlaceholderName({ currency, index, derivationMode }), balance, blockHeight: currentBlock.height, index, @@ -258,10 +268,11 @@ const EthereumBridge: WalletBridge = { const account: $Exact = { id: accountId, - xpub: '', + seedIdentifier: freshAddress, freshAddress, freshAddressPath, - name: getAccountPlaceholderName(currency, index, !isStandard), + derivationMode, + name: getAccountPlaceholderName({ currency, index, derivationMode }), balance, blockHeight: currentBlock.height, index, @@ -288,21 +299,20 @@ const EthereumBridge: WalletBridge = { async function main() { try { - const derivations = getDerivations(currency) - const last = derivations[derivations.length - 1] - for (const derivation of derivations) { - const isStandard = last === derivation + const derivationModes = getDerivationModesForCurrency(currency) + for (const derivationMode of derivationModes) { let emptyCount = 0 - const mandatoryEmptyAccountSkip = derivation.mandatoryEmptyAccountSkip || 0 + const mandatoryEmptyAccountSkip = getMandatoryEmptyAccountSkip(derivationMode) + const derivationScheme = getDerivationScheme({ derivationMode, currency }) for (let index = 0; index < 255; index++) { - const freshAddressPath = derivation({ currency, x: index, segwit: false }) + const freshAddressPath = runDerivationScheme(derivationScheme, { account: index }) const res = await getAddressCommand .send({ currencyId: currency.id, devicePath: deviceId, path: freshAddressPath }) .toPromise() const r = await stepAddress( index, res, - isStandard, + derivationMode, emptyCount < mandatoryEmptyAccountSkip, ) logger.log( diff --git a/src/bridge/LibcoreBridge.js b/src/bridge/LibcoreBridge.js index fa86c0eb..4ad2bfc0 100644 --- a/src/bridge/LibcoreBridge.js +++ b/src/bridge/LibcoreBridge.js @@ -129,7 +129,9 @@ const LibcoreBridge: WalletBridge = { libcoreSyncAccount .send({ accountId: account.id, - freshAddressPath: account.freshAddressPath, + derivationMode: account.derivationMode, + xpub: account.xpub || '', + seedIdentifier: account.seedIdentifier, index: account.index, currencyId: account.currency.id, }) @@ -218,9 +220,9 @@ const LibcoreBridge: WalletBridge = { .send({ accountId: account.id, currencyId: account.currency.id, - xpub: account.xpub, - freshAddress: account.freshAddress, - freshAddressPath: account.freshAddressPath, + xpub: account.xpub || '', // FIXME only reason is to build the op id. we need to consider another id for making op id. + derivationMode: account.derivationMode, + seedIdentifier: account.seedIdentifier, index: account.index, transaction: serializeTransaction(transaction), deviceId, diff --git a/src/bridge/RippleJSBridge.js b/src/bridge/RippleJSBridge.js index 94337482..c24f1b04 100644 --- a/src/bridge/RippleJSBridge.js +++ b/src/bridge/RippleJSBridge.js @@ -7,7 +7,15 @@ import bs58check from 'ripple-bs58check' import { computeBinaryTransactionHash } from 'ripple-hashes' import throttle from 'lodash/throttle' import type { Account, Operation } from '@ledgerhq/live-common/lib/types' -import { getDerivations } from 'helpers/derivations' +import { + getDerivationModesForCurrency, + getDerivationScheme, + runDerivationScheme, +} from '@ledgerhq/live-common/lib/helpers/derivation' +import { + getAccountPlaceholderName, + getNewAccountPlaceholderName, +} from '@ledgerhq/live-common/lib/helpers/account' import getAddress from 'commands/getAddress' import signTransaction from 'commands/signTransaction' import { @@ -19,7 +27,6 @@ import { } from 'api/Ripple' import FeesRippleKind from 'components/FeesField/RippleKind' import AdvancedOptionsRippleKind from 'components/AdvancedOptions/RippleKind' -import { getAccountPlaceholderName, getNewAccountPlaceholderName } from 'helpers/accountName' import { NotEnoughBalance, FeeNotLoaded, @@ -289,17 +296,17 @@ const RippleJSBridge: WalletBridge = { const minLedgerVersion = Number(ledgers[0]) const maxLedgerVersion = Number(ledgers[1]) - const derivations = getDerivations(currency) - for (const derivation of derivations) { - const legacy = derivation !== derivations[derivations.length - 1] + const derivationModes = getDerivationModesForCurrency(currency) + for (const derivationMode of derivationModes) { + const derivationScheme = getDerivationScheme({ derivationMode, currency }) for (let index = 0; index < 255; index++) { - const freshAddressPath = derivation({ currency, x: index, segwit: false }) + const freshAddressPath = runDerivationScheme(derivationScheme, { account: index }) const { address, publicKey } = await await getAddress .send({ currencyId: currency.id, devicePath: deviceId, path: freshAddressPath }) .toPromise() if (finished) return - const accountId = `ripplejs:${currency.id}:${address}:${publicKey}` + const accountId = `ripplejs:2:${currency.id}:${address}:${publicKey}` let info try { @@ -316,11 +323,12 @@ const RippleJSBridge: WalletBridge = { if (!info) { // account does not exist in Ripple server // we are generating a new account locally - if (!legacy) { + if (derivationMode === '') { o.next({ id: accountId, - xpub: '', - name: getNewAccountPlaceholderName(currency, index), + seedIdentifier: freshAddress, + derivationMode, + name: getNewAccountPlaceholderName({ currency, index, derivationMode }), freshAddress, freshAddressPath, balance: BigNumber(0), @@ -353,8 +361,9 @@ const RippleJSBridge: WalletBridge = { const account: $Exact = { id: accountId, - xpub: '', - name: getAccountPlaceholderName(currency, index, legacy), + seedIdentifier: freshAddress, + derivationMode, + name: getAccountPlaceholderName({ currency, index, derivationMode }), freshAddress, freshAddressPath, balance, diff --git a/src/bridge/makeMockBridge.js b/src/bridge/makeMockBridge.js index d266c234..8c66bdb2 100644 --- a/src/bridge/makeMockBridge.js +++ b/src/bridge/makeMockBridge.js @@ -10,7 +10,7 @@ import { getOperationAmountNumber } from '@ledgerhq/live-common/lib/helpers/oper import Prando from 'prando' import { BigNumber } from 'bignumber.js' import type { Operation } from '@ledgerhq/live-common/lib/types' -import { validateNameEdition } from 'helpers/accountName' +import { validateNameEdition } from '@ledgerhq/live-common/lib/helpers/account' import { MOCK_DATA_SEED } from 'config/constants' import type { WalletBridge } from './types' diff --git a/src/commands/libcoreGetFees.js b/src/commands/libcoreGetFees.js index 4b3c8d5b..c57bb193 100644 --- a/src/commands/libcoreGetFees.js +++ b/src/commands/libcoreGetFees.js @@ -4,17 +4,16 @@ import { Observable } from 'rxjs' import { BigNumber } from 'bignumber.js' import withLibcore from 'helpers/withLibcore' import { createCommand, Command } from 'helpers/ipc' +import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' +import { getWalletName } from '@ledgerhq/live-common/lib/helpers/account' import type { Account } from '@ledgerhq/live-common/lib/types' -import * as accountIdHelper from 'helpers/accountId' 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 @@ -24,22 +23,20 @@ type BitcoinLikeTransaction = { } type Input = { - accountId: string, accountIndex: number, transaction: BitcoinLikeTransaction, currencyId: string, - isSegwit: boolean, - isUnsplit: boolean, + derivationMode: string, + seedIdentifier: string, } 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]), + derivationMode: a.derivationMode, + seedIdentifier: a.seedIdentifier, } } @@ -47,17 +44,21 @@ type Result = { totalFees: string } const cmd: Command = createCommand( 'libcoreGetFees', - ({ accountId, currencyId, isSegwit, isUnsplit, accountIndex, transaction }) => + ({ currencyId, derivationMode, seedIdentifier, accountIndex, transaction }) => Observable.create(o => { let unsubscribed = false const isCancelled = () => unsubscribed + const currency = getCryptoCurrencyById(currencyId) withLibcore(async core => { - const { walletName } = accountIdHelper.decode(accountId) + const walletName = getWalletName({ + currency, + derivationMode, + seedIdentifier, + }) const njsWallet = await getOrCreateWallet(core, walletName, { - currencyId, - isSegwit, - isUnsplit, + currency, + derivationMode, }) if (isCancelled()) return const njsAccount = await njsWallet.getAccount(accountIndex) diff --git a/src/commands/libcoreScanFromXPUB.js b/src/commands/libcoreScanFromXPUB.js index 1c595861..0444f501 100644 --- a/src/commands/libcoreScanFromXPUB.js +++ b/src/commands/libcoreScanFromXPUB.js @@ -10,18 +10,18 @@ import { scanAccountsFromXPUB } from 'helpers/libcore' type Input = { currencyId: string, xpub: string, - isSegwit: boolean, - isUnsplit: boolean, + derivationMode: string, + seedIdentifier: string, } type Result = AccountRaw const cmd: Command = createCommand( 'libcoreScanFromXPUB', - ({ currencyId, xpub, isSegwit, isUnsplit }) => + ({ currencyId, xpub, derivationMode, seedIdentifier }) => fromPromise( withLibcore(async core => - scanAccountsFromXPUB({ core, currencyId, xpub, isSegwit, isUnsplit }), + scanAccountsFromXPUB({ core, currencyId, xpub, derivationMode, seedIdentifier }), ), ), ) diff --git a/src/commands/libcoreSignAndBroadcast.js b/src/commands/libcoreSignAndBroadcast.js index ad81d05c..d6210ab2 100644 --- a/src/commands/libcoreSignAndBroadcast.js +++ b/src/commands/libcoreSignAndBroadcast.js @@ -2,22 +2,21 @@ import logger from 'logger' import { BigNumber } from 'bignumber.js' -import type { OperationRaw } from '@ledgerhq/live-common/lib/types' import Btc from '@ledgerhq/hw-app-btc' import { Observable } from 'rxjs' +import { isSegwitDerivationMode } from '@ledgerhq/live-common/lib/helpers/derivation' import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' -import { isSegwitPath, isUnsplitPath } from 'helpers/bip32' +import type { OperationRaw, CryptoCurrency } from '@ledgerhq/live-common/lib/types' +import { getWalletName } from '@ledgerhq/live-common/lib/helpers/account' import { libcoreAmountToBigNumber, bigNumberToLibcoreAmount, getOrCreateWallet, } from 'helpers/libcore' -import { splittedCurrencies } from 'config/cryptocurrencies' import withLibcore from 'helpers/withLibcore' import { createCommand, Command } from 'helpers/ipc' import { withDevice } from 'helpers/deviceAccess' -import * as accountIdHelper from 'helpers/accountId' type BitcoinLikeTransaction = { amount: string, @@ -28,9 +27,9 @@ type BitcoinLikeTransaction = { type Input = { accountId: string, currencyId: string, + derivationMode: string, + seedIdentifier: string, xpub: string, - freshAddress: string, - freshAddressPath: string, index: number, transaction: BitcoinLikeTransaction, deviceId: string, @@ -42,17 +41,18 @@ type Result = { type: 'signed' } | { type: 'broadcasted', operation: OperationRa const cmd: Command = createCommand( 'libcoreSignAndBroadcast', - ({ accountId, currencyId, xpub, freshAddress, freshAddressPath, index, transaction, deviceId }) => + ({ accountId, currencyId, derivationMode, seedIdentifier, xpub, index, transaction, deviceId }) => Observable.create(o => { let unsubscribed = false + const currency = getCryptoCurrencyById(currencyId) const isCancelled = () => unsubscribed withLibcore(core => doSignAndBroadcast({ accountId, - currencyId, + currency, + derivationMode, + seedIdentifier, xpub, - freshAddress, - freshAddressPath, index, transaction, deviceId, @@ -78,28 +78,26 @@ const cmd: Command = createCommand( async function signTransaction({ hwApp, - currencyId, + currency, transaction, + derivationMode, sigHashType, - supportsSegwit, - isSegwit, hasTimestamp, }: { hwApp: Btc, - currencyId: string, + currency: CryptoCurrency, transaction: *, + derivationMode: string, sigHashType: number, - supportsSegwit: boolean, - isSegwit: boolean, hasTimestamp: boolean, }) { const additionals = [] let expiryHeight - if (currencyId === 'bitcoin_cash' || currencyId === 'bitcoin_gold') additionals.push('bip143') - if (currencyId === 'zcash') expiryHeight = Buffer.from([0x00, 0x00, 0x00, 0x00]) + if (currency.id === 'bitcoin_cash' || currency.id === 'bitcoin_gold') additionals.push('bip143') + if (currency.id === 'zcash') expiryHeight = Buffer.from([0x00, 0x00, 0x00, 0x00]) const rawInputs = transaction.getInputs() - const hasExtraData = currencyId === 'zcash' + const hasExtraData = currency.id === 'zcash' const inputs = await Promise.all( rawInputs.map(async input => { @@ -107,7 +105,7 @@ async function signTransaction({ const hexPreviousTransaction = Buffer.from(rawPreviousTransaction).toString('hex') const previousTransaction = hwApp.splitTransaction( hexPreviousTransaction, - supportsSegwit, + currency.supportsSegwit, hasTimestamp, hasExtraData, ) @@ -156,7 +154,7 @@ async function signTransaction({ outputScriptHex, lockTime, sigHashType, - isSegwit, + isSegwitDerivationMode(derivationMode), initialTimestamp, additionals, expiryHeight, @@ -167,9 +165,10 @@ async function signTransaction({ export async function doSignAndBroadcast({ accountId, - currencyId, + derivationMode, + seedIdentifier, + currency, xpub, - freshAddressPath, index, transaction, deviceId, @@ -179,10 +178,10 @@ export async function doSignAndBroadcast({ onOperationBroadcasted, }: { accountId: string, - currencyId: string, + derivationMode: string, + seedIdentifier: string, + currency: CryptoCurrency, xpub: string, - freshAddress: string, - freshAddressPath: string, index: number, transaction: BitcoinLikeTransaction, deviceId: string, @@ -191,11 +190,9 @@ export async function doSignAndBroadcast({ onSigned: () => void, onOperationBroadcasted: (optimisticOp: $Exact) => void, }): Promise { - const { walletName } = accountIdHelper.decode(accountId) + const walletName = getWalletName({ currency, seedIdentifier, derivationMode }) - const isSegwit = isSegwitPath(freshAddressPath) - const isUnsplit = isUnsplitPath(freshAddressPath, splittedCurrencies[currencyId]) - const njsWallet = await getOrCreateWallet(core, walletName, { currencyId, isSegwit, isUnsplit }) + const njsWallet = await getOrCreateWallet(core, walletName, { currency, derivationMode }) if (isCancelled()) return const njsAccount = await njsWallet.getAccount(index) if (isCancelled()) return @@ -221,17 +218,14 @@ export async function doSignAndBroadcast({ const hasTimestamp = !!njsWalletCurrency.bitcoinLikeNetworkParameters.UsesTimestampedTransaction // TODO: const timestampDelay = njsWalletCurrency.bitcoinLikeNetworkParameters.TimestampDelay - const currency = getCryptoCurrencyById(currencyId) - const signedTransaction = await withDevice(deviceId)(async transport => signTransaction({ hwApp: new Btc(transport), - currencyId, + currency, transaction: builded, sigHashType: parseInt(sigHashType, 16), - supportsSegwit: !!currency.supportsSegwit, - isSegwit: isSegwitPath(freshAddressPath), hasTimestamp, + derivationMode, }), ) diff --git a/src/commands/libcoreSyncAccount.js b/src/commands/libcoreSyncAccount.js index 21104d20..525fe0c1 100644 --- a/src/commands/libcoreSyncAccount.js +++ b/src/commands/libcoreSyncAccount.js @@ -1,6 +1,7 @@ // @flow import type { AccountRaw } from '@ledgerhq/live-common/lib/types' +import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' import { fromPromise } from 'rxjs/observable/fromPromise' import { createCommand, Command } from 'helpers/ipc' @@ -9,15 +10,24 @@ import withLibcore from 'helpers/withLibcore' type Input = { accountId: string, - freshAddressPath: string, currencyId: string, + xpub: string, + derivationMode: string, + seedIdentifier: string, index: number, } type Result = { rawAccount: AccountRaw, requiresCacheFlush: boolean } -const cmd: Command = createCommand('libcoreSyncAccount', accountInfos => - fromPromise(withLibcore(core => syncAccount({ ...accountInfos, core }))), +const cmd: Command = createCommand( + 'libcoreSyncAccount', + ({ currencyId, ...accountInfos }) => + fromPromise( + withLibcore(core => { + const currency = getCryptoCurrencyById(currencyId) + return syncAccount({ ...accountInfos, currency, core }) + }), + ), ) export default cmd diff --git a/src/components/AccountPage/AccountHeaderActions.js b/src/components/AccountPage/AccountHeaderActions.js index a9a8d747..f5e70459 100644 --- a/src/components/AccountPage/AccountHeaderActions.js +++ b/src/components/AccountPage/AccountHeaderActions.js @@ -7,7 +7,7 @@ import { translate } from 'react-i18next' import styled from 'styled-components' import type { Account } from '@ledgerhq/live-common/lib/types' import Tooltip from 'components/base/Tooltip' -import isAccountEmpty from 'helpers/isAccountEmpty' +import { isAccountEmpty } from '@ledgerhq/live-common/lib/helpers/account' import { MODAL_SEND, MODAL_RECEIVE, MODAL_SETTINGS_ACCOUNT } from 'config/constants' diff --git a/src/components/AccountPage/index.js b/src/components/AccountPage/index.js index 15dce6ab..4404c8fa 100644 --- a/src/components/AccountPage/index.js +++ b/src/components/AccountPage/index.js @@ -8,7 +8,7 @@ import { Redirect } from 'react-router' import type { Currency, Account } from '@ledgerhq/live-common/lib/types' import type { T } from 'types/common' import { accountSelector } from 'reducers/accounts' -import isAccountEmpty from 'helpers/isAccountEmpty' +import { isAccountEmpty } from '@ledgerhq/live-common/lib/helpers/account' import { counterValueCurrencySelector, localeSelector, diff --git a/src/components/DevToolsPage/AccountImporter.js b/src/components/DevToolsPage/AccountImporter.js index 88afd7c9..9bccb67f 100644 --- a/src/components/DevToolsPage/AccountImporter.js +++ b/src/components/DevToolsPage/AccountImporter.js @@ -21,7 +21,6 @@ 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' @@ -80,12 +79,19 @@ class AccountImporter extends PureComponent { try { const { currency, xpub, isSegwit, isUnsplit } = this.state invariant(currency, 'no currency') + const derivationMode = isSegwit + ? isUnsplit + ? 'segwit_unsplit' + : 'segwit' + : isUnsplit + ? 'unsplit' + : '' const rawAccount = await scanFromXPUB .send({ + seedIdentifier: 'dev_tool', currencyId: currency.id, xpub, - isSegwit, - isUnsplit, + derivationMode, }) .toPromise() const account = decodeAccount(rawAccount) @@ -107,7 +113,7 @@ class AccountImporter extends PureComponent { render() { const { currency, xpub, isSegwit, isUnsplit, status, account, error } = this.state - const supportsSplit = !!currency && !!splittedCurrencies[currency.id] + const supportsSplit = !!currency && !!currency.forkedFrom return ( {status === 'idle' ? ( diff --git a/src/components/EnsureDeviceApp.js b/src/components/EnsureDeviceApp.js index 4ad9fd4b..af71d6cd 100644 --- a/src/components/EnsureDeviceApp.js +++ b/src/components/EnsureDeviceApp.js @@ -10,8 +10,11 @@ import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react' import logger from 'logger' import getAddress from 'commands/getAddress' import { createCancelablePolling } from 'helpers/promise' -import { standardDerivation } from 'helpers/derivations' -import { isSegwitPath } from 'helpers/bip32' +import { + isSegwitDerivationMode, + getDerivationScheme, + runDerivationScheme, +} from '@ledgerhq/live-common/lib/helpers/derivation' import DeviceInteraction from 'components/DeviceInteraction' import Text from 'components/base/Text' @@ -122,8 +125,8 @@ async function getAddressFromAccountOrCurrency(device, account, currency) { currencyId: currency.id, path: account ? account.freshAddressPath - : standardDerivation({ currency, segwit: false, x: 0 }), - segwit: account ? isSegwitPath(account.freshAddressPath) : false, + : runDerivationScheme(getDerivationScheme({ currency, derivationMode: '' })), + segwit: account ? isSegwitDerivationMode(account.derivationMode) : false, }) .toPromise() return address diff --git a/src/components/QRCodeExporter.js b/src/components/QRCodeExporter.js index 43fca7f9..d81375d2 100644 --- a/src/components/QRCodeExporter.js +++ b/src/components/QRCodeExporter.js @@ -7,20 +7,23 @@ import { connect } from 'react-redux' import { accountsSelector } from 'reducers/accounts' import { exportSettingsSelector } from 'reducers/settings' -import { makeChunks } from '@ledgerhq/live-common/lib/bridgestream/exporter' +import { encode } from '@ledgerhq/live-common/lib/bridgestream' +import { dataToFrames } from 'qrloop/exporter' import QRCode from './base/QRCode' const mapStateToProps = createSelector( accountsSelector, exportSettingsSelector, (accounts, settings) => ({ - chunks: makeChunks({ - accounts, - settings, - exporterName: 'desktop', - exporterVersion: __APP_VERSION__, - chunkSize: 120, - }), + chunks: dataToFrames( + encode({ + accounts, + settings, + exporterName: 'desktop', + exporterVersion: __APP_VERSION__, + chunkSize: 120, + }), + ), }), ) diff --git a/src/components/modals/AccountSettingRenderBody.js b/src/components/modals/AccountSettingRenderBody.js index 66a84184..90c21665 100644 --- a/src/components/modals/AccountSettingRenderBody.js +++ b/src/components/modals/AccountSettingRenderBody.js @@ -10,7 +10,7 @@ import { translate } from 'react-i18next' import type { Account, Unit, Currency } from '@ledgerhq/live-common/lib/types' import type { T } from 'types/common' import { MODAL_SETTINGS_ACCOUNT, MAX_ACCOUNT_NAME_SIZE } from 'config/constants' -import { validateNameEdition } from 'helpers/accountName' +import { validateNameEdition } from '@ledgerhq/live-common/lib/helpers/account' import { updateAccount, removeAccount } from 'actions/accounts' import { setDataModal } from 'reducers/modals' diff --git a/src/components/modals/AddAccounts/index.js b/src/components/modals/AddAccounts/index.js index 20a77f39..85dd42ca 100644 --- a/src/components/modals/AddAccounts/index.js +++ b/src/components/modals/AddAccounts/index.js @@ -23,7 +23,7 @@ import { closeModal } from 'reducers/modals' import Modal from 'components/base/Modal' import Stepper from 'components/base/Stepper' -import { validateNameEdition } from 'helpers/accountName' +import { validateNameEdition } from '@ledgerhq/live-common/lib/helpers/account' import StepChooseCurrency, { StepChooseCurrencyFooter } from './steps/01-step-choose-currency' import StepConnectDevice, { StepConnectDeviceFooter } from './steps/02-step-connect-device' diff --git a/src/components/modals/AddAccounts/steps/03-step-import.js b/src/components/modals/AddAccounts/steps/03-step-import.js index 81188533..115664c3 100644 --- a/src/components/modals/AddAccounts/steps/03-step-import.js +++ b/src/components/modals/AddAccounts/steps/03-step-import.js @@ -10,7 +10,7 @@ import uniq from 'lodash/uniq' import { urls } from 'config/urls' import ExternalLinkButton from 'components/base/ExternalLinkButton' import RetryButton from 'components/base/RetryButton' -import isAccountEmpty from 'helpers/isAccountEmpty' +import { isAccountEmpty } from '@ledgerhq/live-common/lib/helpers/account' import { getBridgeForCurrency } from 'bridge' diff --git a/src/components/modals/Debug.js b/src/components/modals/Debug.js index c87bbf9f..e092c1b2 100644 --- a/src/components/modals/Debug.js +++ b/src/components/modals/Debug.js @@ -4,13 +4,15 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import { createStructuredSelector } from 'reselect' import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' -import last from 'lodash/last' +import { + getDerivationScheme, + runDerivationScheme, +} from '@ledgerhq/live-common/lib/helpers/derivation' import Modal, { ModalBody, ModalTitle, ModalContent } from 'components/base/Modal' import { getCurrentDevice } from 'reducers/devices' import Button from 'components/base/Button' import Box from 'components/base/Box' import Input from 'components/base/Input' -import { getDerivations } from 'helpers/derivations' import getAddress from 'commands/getAddress' import testInterval from 'commands/testInterval' import testCrash from 'commands/testCrash' @@ -44,11 +46,14 @@ class Debug extends Component<*, *> { onClickStressDevice = (device: *) => async () => { try { const currency = getCryptoCurrencyById('bitcoin') - const derivation = last(getDerivations(currency)) + const derivationScheme = getDerivationScheme({ + currency, + derivationMode: 'segwit', + }) for (let x = 0; x < 20; x++) { const { address, path } = await getAddress .send({ - path: derivation({ currency, segwit: true, x }), + path: runDerivationScheme(derivationScheme, { account: x }), currencyId: currency.id, devicePath: device.path, }) @@ -94,9 +99,12 @@ class Debug extends Component<*, *> { .then(o => o.stringVersion), ) const currency = getCryptoCurrencyById('bitcoin') - const derivation = last(getDerivations(currency)) + const derivationScheme = getDerivationScheme({ + currency, + derivationMode: 'segwit', + }) const obj = { - path: derivation({ currency, segwit: true, x: 0 }), + path: runDerivationScheme(derivationScheme), currencyId: currency.id, devicePath: device.path, } diff --git a/src/components/modals/Receive/steps/04-step-receive-funds.js b/src/components/modals/Receive/steps/04-step-receive-funds.js index 5faff059..63502cec 100644 --- a/src/components/modals/Receive/steps/04-step-receive-funds.js +++ b/src/components/modals/Receive/steps/04-step-receive-funds.js @@ -5,7 +5,7 @@ import React, { PureComponent } from 'react' import TrackPage from 'analytics/TrackPage' import getAddress from 'commands/getAddress' -import { isSegwitPath } from 'helpers/bip32' +import { isSegwitDerivationMode } from '@ledgerhq/live-common/lib/helpers/derivation' import Box from 'components/base/Box' import CurrentAddressForAccount from 'components/CurrentAddressForAccount' import { DisconnectedDevice, WrongDeviceForAccount } from 'config/errors' @@ -29,7 +29,7 @@ export default class StepReceiveFunds extends PureComponent { currencyId: account.currency.id, devicePath: device.path, path: account.freshAddressPath, - segwit: isSegwitPath(account.freshAddressPath), + segwit: isSegwitDerivationMode(account.derivationMode), verify: true, } const { address } = await getAddress.send(params).toPromise() diff --git a/src/config/cryptocurrencies.js b/src/config/cryptocurrencies.js index 21f56322..2667ddb7 100644 --- a/src/config/cryptocurrencies.js +++ b/src/config/cryptocurrencies.js @@ -35,12 +35,3 @@ 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, - }, -} diff --git a/src/helpers/accountId.js b/src/helpers/accountId.js deleted file mode 100644 index 543a8223..00000000 --- a/src/helpers/accountId.js +++ /dev/null @@ -1,22 +0,0 @@ -// @flow - -import invariant from 'invariant' - -type Params = { - type: string, - version: string, - xpub: string, - walletName: string, -} - -export function encode({ type, version, xpub, walletName }: Params) { - return `${type}:${version}:${xpub}:${walletName}` -} - -export function decode(accountId: string): Params { - invariant(typeof accountId === 'string', 'accountId is not a string') - const splitted = accountId.split(':') - invariant(splitted.length === 4, 'invalid size for accountId') - const [type, version, xpub, walletName] = splitted - return { type, version, xpub, walletName } -} diff --git a/src/helpers/accountModel.js b/src/helpers/accountModel.js new file mode 100644 index 00000000..622a8bc2 --- /dev/null +++ b/src/helpers/accountModel.js @@ -0,0 +1,145 @@ +/** + * @module models/account + * @flow + */ +import { BigNumber } from 'bignumber.js' +import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' +import { createDataModel } from '@ledgerhq/live-common/lib/DataModel' +import type { DataModel } from '@ledgerhq/live-common/lib/DataModel' +import type { Account, AccountRaw, Operation } from '@ledgerhq/live-common/lib/types' + +/** + * @memberof models/account + */ +export const opRetentionStategy = (maxDaysOld: number, keepFirst: number) => ( + op: Operation, + index: number, +): boolean => index < keepFirst || Date.now() - op.date < 1000 * 60 * 60 * 24 * maxDaysOld + +const opRetentionFilter = opRetentionStategy(366, 100) + +const accountModel: DataModel = createDataModel({ + migrations: [ + // 2018-10-10: change of the account id format to include the derivationMode and seedIdentifier in Account + raw => { + raw = { ...raw } + const { currencyId, freshAddressPath } = raw + const [type, originalVersion, xpubOrAddress, walletName] = raw.id.split(':') + let version = originalVersion + let derivationMode + let seedIdentifier + switch (type) { + case 'libcore': { + const i = walletName.indexOf('__') + currencyId.length + 1 + derivationMode = walletName.slice(i + 2) + seedIdentifier = walletName.slice(0, i) + break + } + + case 'ethereumjs': { + // reverse the derivation that was used to infer what was the derivationMode + if (freshAddressPath.match(/^44'\/60'\/0'\/[0-9]+$/)) { + derivationMode = 'ethM' + } else if ( + currencyId === 'ethereum_classic' && + freshAddressPath.match(/^44'\/60'\/160720'\/0'\/[0-9]+$/) + ) { + derivationMode = 'etcM' + } else { + derivationMode = '' + } + delete raw.xpub + seedIdentifier = xpubOrAddress + version = '2' // replace version because no need to have the currencyId like used to do. + break + } + + case 'ripplejs': { + // reverse the derivation that was used to infer what was the derivationMode + if (freshAddressPath.match(/^44'\/144'\/0'\/[0-9]+'$/)) { + derivationMode = 'rip' + } else { + derivationMode = '' + } + delete raw.xpub + seedIdentifier = xpubOrAddress + version = '2' // replace version because no need to have the currencyId like used to do. + break + } + + default: + // this case should never happen + throw new Error(`unknown Account type=${type}`) + } + + const id = `${type}:${version}:${currencyId}:${xpubOrAddress}:${derivationMode}` + console.log({ old: raw, id, derivationMode, seedIdentifier }) + return { + ...raw, + id, + derivationMode, + seedIdentifier, + } + }, + // ^- Each time a modification is brought to the model, add here a migration function here + ], + + decode: (rawAccount: AccountRaw): Account => { + const { + currencyId, + unitMagnitude, + operations, + pendingOperations, + lastSyncDate, + balance, + ...acc + } = rawAccount + const currency = getCryptoCurrencyById(currencyId) + const unit = currency.units.find(u => u.magnitude === unitMagnitude) || currency.units[0] + const convertOperation = ({ date, value, fee, ...op }) => ({ + ...op, + accountId: acc.id, + date: new Date(date), + value: BigNumber(value), + fee: BigNumber(fee), + }) + return { + ...acc, + balance: BigNumber(balance), + operations: operations.map(convertOperation), + pendingOperations: pendingOperations.map(convertOperation), + unit, + currency, + lastSyncDate: new Date(lastSyncDate), + } + }, + + encode: ({ + currency, + operations, + pendingOperations, + unit, + lastSyncDate, + balance, + ...acc + }: Account): AccountRaw => { + const convertOperation = ({ date, value, fee, ...op }) => ({ + ...op, + date: date.toISOString(), + value: value.toString(), + fee: fee.toString(), + }) + + return { + ...acc, + operations: operations.filter(opRetentionFilter).map(convertOperation), + pendingOperations: pendingOperations.map(convertOperation), + currencyId: currency.id, + unitMagnitude: unit.magnitude, + lastSyncDate: lastSyncDate.toISOString(), + balance: balance.toString(), + } + }, +}) + +export default accountModel diff --git a/src/helpers/accountName.js b/src/helpers/accountName.js deleted file mode 100644 index ce9d2d36..00000000 --- a/src/helpers/accountName.js +++ /dev/null @@ -1,20 +0,0 @@ -// @flow -import type { Account, CryptoCurrency } from '@ledgerhq/live-common/lib/types' -import { MAX_ACCOUNT_NAME_SIZE } from 'config/constants' - -export const getAccountPlaceholderName = ( - c: CryptoCurrency, - index: number, - isLegacy: boolean = false, - isUnsplit: boolean = false, -) => `${c.name} ${index + 1}${isLegacy ? ' (legacy)' : ''}${isUnsplit ? ' (unsplit)' : ''}` - -export const getNewAccountPlaceholderName = getAccountPlaceholderName // same naming -// export const getNewAccountPlaceholderName = (_c: CryptoCurrency, _index: number) => `New Account` - -export const validateNameEdition = (account: Account, name: ?string): string => - ( - (name || account.name || '').replace(/\s+/g, ' ').trim() || - account.name || - getAccountPlaceholderName(account.currency, account.index) - ).slice(0, MAX_ACCOUNT_NAME_SIZE) diff --git a/src/helpers/accountOrdering.js b/src/helpers/accountOrdering.js deleted file mode 100644 index 32cd30df..00000000 --- a/src/helpers/accountOrdering.js +++ /dev/null @@ -1,38 +0,0 @@ -// @flow - -import type { BigNumber } from 'bignumber.js' -import type { Account } from '@ledgerhq/live-common/lib/types' - -type Param = { - accounts: Account[], - accountsBtcBalance: BigNumber[], - orderAccounts: string, -} - -type SortMethod = 'name' | 'balance' - -const sortMethod: { [_: SortMethod]: (Param) => string[] } = { - balance: ({ accounts, accountsBtcBalance }) => - accounts - .map((a, i) => [a.id, accountsBtcBalance[i]]) - .sort((a, b) => a[1].minus(b[1]).toNumber()) - .map(o => o[0]), - - name: ({ accounts }) => - accounts - .slice(0) - .sort((a, b) => a.name.localeCompare(b.name)) - .map(a => a.id), -} - -export function sortAccounts(param: Param) { - const [order, sort] = param.orderAccounts.split('|') - if (order === 'name' || order === 'balance') { - const ids = sortMethod[order](param) - if (sort === 'asc') { - ids.reverse() - } - return ids - } - return null -} diff --git a/src/helpers/bip32.js b/src/helpers/bip32.js deleted file mode 100644 index decfdc44..00000000 --- a/src/helpers/bip32.js +++ /dev/null @@ -1,16 +0,0 @@ -// @flow - -type SplitConfig = { - coinType: number, -} - -export const isSegwitPath = (path: string): boolean => path.startsWith("49'") - -export const isUnsplitPath = (path: string, splitConfig: SplitConfig) => { - try { - const coinType = parseInt(path.split('/')[1], 10) - return coinType === splitConfig.coinType - } catch (e) { - return false - } -} diff --git a/src/helpers/derivations.js b/src/helpers/derivations.js deleted file mode 100644 index 21ea8b30..00000000 --- a/src/helpers/derivations.js +++ /dev/null @@ -1,38 +0,0 @@ -// @flow -import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types' - -type Derivation = { - ({ - currency: CryptoCurrency, - segwit: boolean, - x: number, - }): string, - - mandatoryEmptyAccountSkip?: number, -} - -const ethLegacyMEW: Derivation = ({ x }) => `44'/60'/0'/${x}` -ethLegacyMEW.mandatoryEmptyAccountSkip = 10 - -const etcLegacyMEW: Derivation = ({ x }) => `44'/60'/160720'/0'/${x}` -etcLegacyMEW.mandatoryEmptyAccountSkip = 10 - -const rippleLegacy: Derivation = ({ x }) => `44'/144'/0'/${x}'` - -const legacyDerivations = { - ethereum: [ethLegacyMEW], - ethereum_classic: [ethLegacyMEW, etcLegacyMEW], - ripple: [rippleLegacy], -} - -export const standardDerivation: Derivation = ({ currency, segwit, x }) => { - const purpose = segwit ? 49 : 44 - const { coinType } = currency - return `${purpose}'/${coinType}'/${x}'/0/0` -} - -// return an array of ways to derivate, by convention the latest is the standard one. -export const getDerivations = (currency: CryptoCurrency): Derivation[] => [ - ...(legacyDerivations[currency.id] || []), - standardDerivation, -] diff --git a/src/helpers/isAccountEmpty.js b/src/helpers/isAccountEmpty.js deleted file mode 100644 index 6f2522b9..00000000 --- a/src/helpers/isAccountEmpty.js +++ /dev/null @@ -1,10 +0,0 @@ -// @flow -import type { Account } from '@ledgerhq/live-common/lib/types' - -// TODO move this back in live-common - -// An account is empty if there is no operations AND balance is zero. -// balance can be non-zero in edgecases, for instance: -// - Ethereum contract only funds (api limitations) -// - Ripple node that don't show all ledgers and if you have very old txs -export default (a: Account): boolean => a.operations.length === 0 && a.balance.isZero() diff --git a/src/helpers/libcore.js b/src/helpers/libcore.js index a81f2353..3b76b4b1 100644 --- a/src/helpers/libcore.js +++ b/src/helpers/libcore.js @@ -6,18 +6,30 @@ import logger from 'logger' import { BigNumber } from 'bignumber.js' import Btc from '@ledgerhq/hw-app-btc' import { withDevice } from 'helpers/deviceAccess' +import { + getDerivationScheme, + isSegwitDerivationMode, + isUnsplitDerivationMode, +} from '@ledgerhq/live-common/lib/helpers/derivation' import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' +import { + encodeAccountId, + getNewAccountPlaceholderName, + getAccountPlaceholderName, + getWalletName, +} from '@ledgerhq/live-common/lib/helpers/account' import { SHOW_LEGACY_NEW_ACCOUNT, SYNC_TIMEOUT } from 'config/constants' -import type { AccountRaw, OperationRaw, OperationType } from '@ledgerhq/live-common/lib/types' +import type { + CryptoCurrency, + AccountRaw, + OperationRaw, + OperationType, +} from '@ledgerhq/live-common/lib/types' import type { NJSAccount, NJSOperation } from '@ledgerhq/ledger-core/src/ledgercore_doc' -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' export function isValidAddress(core: *, currency: *, address: string): boolean { @@ -39,7 +51,7 @@ export async function scanAccountsOnDevice(props: Props): Promise const commonParams = { core, - currencyId, + currency, onAccountScanned, devicePath, isUnsubscribed, @@ -47,40 +59,38 @@ export async function scanAccountsOnDevice(props: Props): Promise let allAccounts = [] + // TODO use getDerivationModesForCurrency + // introduce an internal shouldShowNewAccount({currency, derivationMode}) + const nonSegwitAccounts = await scanAccountsOnDeviceBySegwit({ ...commonParams, showNewAccount: !!SHOW_LEGACY_NEW_ACCOUNT || !currency.supportsSegwit, - isSegwit: false, - isUnsplit: false, + derivationMode: '', }) allAccounts = allAccounts.concat(nonSegwitAccounts) if (currency.supportsSegwit) { const segwitAccounts = await scanAccountsOnDeviceBySegwit({ ...commonParams, + derivationMode: 'segwit', showNewAccount: true, - isSegwit: true, - isUnsplit: false, }) allAccounts = allAccounts.concat(segwitAccounts) } - // TODO: put that info inside currency itself - if (currencyId in splittedCurrencies) { + if (currency.forkedFrom) { const splittedAccounts = await scanAccountsOnDeviceBySegwit({ ...commonParams, - isSegwit: false, + derivationMode: 'unsplit', showNewAccount: false, - isUnsplit: true, }) allAccounts = allAccounts.concat(splittedAccounts) if (currency.supportsSegwit) { const segwitAccounts = await scanAccountsOnDeviceBySegwit({ ...commonParams, + derivationMode: 'segwit_unsplit', showNewAccount: false, - isUnsplit: true, - isSegwit: true, }) allAccounts = allAccounts.concat(segwitAccounts) } @@ -89,56 +99,42 @@ export async function scanAccountsOnDevice(props: Props): Promise return allAccounts } -function encodeWalletName({ - publicKey, - currencyId, - isSegwit, - isUnsplit, -}: { - publicKey: string, - currencyId: string, - isSegwit: boolean, - isUnsplit: boolean, -}) { - const splitConfig = isUnsplit ? splittedCurrencies[currencyId] || null : null - return `${publicKey}__${currencyId}${isSegwit ? '_segwit' : ''}${splitConfig ? '_unsplit' : ''}` -} - async function scanAccountsOnDeviceBySegwit({ core, devicePath, - currencyId, + currency, onAccountScanned, isUnsubscribed, - isSegwit, - isUnsplit, + derivationMode, showNewAccount, }: { core: *, devicePath: string, - currencyId: string, + currency: CryptoCurrency, onAccountScanned: AccountRaw => void, isUnsubscribed: () => boolean, - isSegwit: boolean, // FIXME all segwit to change to 'purpose' + derivationMode: string, showNewAccount: boolean, - isUnsplit: boolean, }): Promise { - const customOpts = - isUnsplit && splittedCurrencies[currencyId] ? splittedCurrencies[currencyId] : null - const { coinType } = customOpts ? customOpts.coinType : getCryptoCurrencyById(currencyId) - + const isSegwit = isSegwitDerivationMode(derivationMode) + const unsplitFork = isUnsplitDerivationMode(derivationMode) ? currency.forkedFrom : null + const { coinType } = unsplitFork ? getCryptoCurrencyById(unsplitFork) : currency const path = `${isSegwit ? '49' : '44'}'/${coinType}'` - const { publicKey } = await withDevice(devicePath)(async transport => + const { publicKey: seedIdentifier } = await withDevice(devicePath)(async transport => new Btc(transport).getWalletPublicKey(path, false, isSegwit), ) if (isUnsubscribed()) return [] - const walletName = encodeWalletName({ publicKey, currencyId, isSegwit, isUnsplit }) + const walletName = getWalletName({ + seedIdentifier, + currency, + derivationMode, + }) // retrieve or create the wallet - const wallet = await getOrCreateWallet(core, walletName, { currencyId, isSegwit, isUnsplit }) + const wallet = await getOrCreateWallet(core, walletName, { currency, derivationMode }) const accountsCount = await wallet.getAccountCount() // recursively scan all accounts on device on the given app @@ -148,13 +144,13 @@ async function scanAccountsOnDeviceBySegwit({ wallet, walletName, devicePath, - currencyId, + currency, accountsCount, accountIndex: 0, accounts: [], onAccountScanned, - isSegwit, - isUnsplit, + seedIdentifier, + derivationMode, showNewAccount, isUnsubscribed, }) @@ -227,13 +223,13 @@ async function scanNextAccount(props: { walletName: string, core: *, devicePath: string, - currencyId: string, + currency: CryptoCurrency, + seedIdentifier: string, + derivationMode: string, accountsCount: number, accountIndex: number, accounts: AccountRaw[], onAccountScanned: AccountRaw => void, - isSegwit: boolean, - isUnsplit: boolean, showNewAccount: boolean, isUnsubscribed: () => boolean, }): Promise { @@ -242,13 +238,13 @@ async function scanNextAccount(props: { wallet, walletName, devicePath, - currencyId, + currency, accountsCount, accountIndex, accounts, onAccountScanned, - isSegwit, - isUnsplit, + derivationMode, + seedIdentifier, showNewAccount, isUnsubscribed, } = props @@ -275,12 +271,12 @@ async function scanNextAccount(props: { const account = await buildAccountRaw({ njsAccount, - isSegwit, - isUnsplit, + seedIdentifier, + derivationMode, accountIndex, wallet, walletName, - currencyId, + currency, core, ops, }) @@ -316,13 +312,11 @@ export async function getOrCreateWallet( core: *, walletName: string, { - currencyId, - isSegwit, - isUnsplit, + currency, + derivationMode, }: { - currencyId: string, - isSegwit: boolean, - isUnsplit: boolean, + currency: CryptoCurrency, + derivationMode: string, }, ): NJSWallet { const pool = core.getPoolInstance() @@ -330,24 +324,21 @@ export async function getOrCreateWallet( 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 ? splittedCurrencies[currencyId] || null : null - const coinType = splitConfig ? splitConfig.coinType : '' - const walletConfig = isSegwit + const currencyCore = await timeoutTagged('getCurrency', 5000, pool.getCurrency(currency.id)) + const derivationScheme = getDerivationScheme({ currency, derivationMode }) + const walletConfig = isSegwitDerivationMode(derivationMode) ? { KEYCHAIN_ENGINE: 'BIP49_P2SH', - KEYCHAIN_DERIVATION_SCHEME: `49'/${coinType}'/'//
`, + KEYCHAIN_DERIVATION_SCHEME: derivationScheme, + } + : { + KEYCHAIN_DERIVATION_SCHEME: derivationScheme, } - : splitConfig - ? { - KEYCHAIN_DERIVATION_SCHEME: `44'/${coinType}'/'//
`, - } - : undefined const njsWalletConfig = createWalletConfig(core, walletConfig) const wallet = await timeoutTagged( 'createWallet', 10000, - core.getPoolInstance().createWallet(walletName, currency, njsWalletConfig), + core.getPoolInstance().createWallet(walletName, currencyCore, njsWalletConfig), ) return wallet } @@ -355,21 +346,20 @@ export async function getOrCreateWallet( async function buildAccountRaw({ njsAccount, - isSegwit, - isUnsplit, + seedIdentifier, + derivationMode, wallet, - walletName, - currencyId, + currency, core, accountIndex, ops, }: { njsAccount: NJSAccount, - isSegwit: boolean, - isUnsplit: boolean, wallet: NJSWallet, + seedIdentifier: string, walletName: string, - currencyId: string, + currency: CryptoCurrency, + derivationMode: string, accountIndex: number, core: *, ops: NJSOperation[], @@ -377,7 +367,6 @@ async function buildAccountRaw({ const njsBalance = await timeoutTagged('getBalance', 10000, njsAccount.getBalance()) const balance = njsBalance.toLong() - const jsCurrency = getCryptoCurrencyById(currencyId) const { derivations } = await timeoutTagged( 'getAccountCreationInfo', 10000, @@ -416,29 +405,29 @@ async function buildAccountRaw({ ops.sort((a, b) => b.getDate() - a.getDate()) const operations = ops.map(op => buildOperationRaw({ core, op, xpub })) - const currency = getCryptoCurrencyById(currencyId) const name = operations.length === 0 - ? getNewAccountPlaceholderName(currency, accountIndex) - : getAccountPlaceholderName( + ? getNewAccountPlaceholderName({ currency, index: accountIndex, derivationMode }) + : getAccountPlaceholderName({ currency, - accountIndex, - (currency.supportsSegwit && !isSegwit) || false, - isUnsplit, - ) + index: accountIndex, + derivationMode, + }) const rawAccount: AccountRaw = { - id: accountIdHelper.encode({ + id: encodeAccountId({ type: 'libcore', version: '1', - xpub, - walletName, + currencyId: currency.id, + xpubOrAddress: xpub, + derivationMode, }), + seedIdentifier, + derivationMode, xpub, path: walletPath, name, - isSegwit, freshAddress, freshAddressPath, balance, @@ -447,8 +436,8 @@ async function buildAccountRaw({ index: accountIndex, operations, pendingOperations: [], - currencyId, - unitMagnitude: jsCurrency.units[0].magnitude, + currencyId: currency.id, + unitMagnitude: currency.units[0].magnitude, lastSyncDate: new Date().toISOString(), } @@ -501,23 +490,26 @@ function buildOperationRaw({ } export async function syncAccount({ - accountId, - freshAddressPath, - currencyId, - index, core, + xpub, + derivationMode, + seedIdentifier, + currency, + index, }: { core: *, - accountId: string, - freshAddressPath: string, - currencyId: string, + xpub: string, + derivationMode: string, + seedIdentifier: string, + currency: CryptoCurrency, index: number, }) { - const decodedAccountId = accountIdHelper.decode(accountId) - const { walletName } = decodedAccountId - const isSegwit = isSegwitPath(freshAddressPath) - const isUnsplit = isUnsplitPath(freshAddressPath, splittedCurrencies[currencyId]) - const njsWallet = await getOrCreateWallet(core, walletName, { currencyId, isSegwit, isUnsplit }) + const walletName = getWalletName({ + seedIdentifier, + derivationMode, + currency, + }) + const njsWallet = await getOrCreateWallet(core, walletName, { currency, derivationMode }) let njsAccount let requiresCacheFlush = false @@ -531,7 +523,7 @@ export async function syncAccount({ 10000, njsWallet.getExtendedKeyAccountCreationInfo(index), ) - extendedInfos.extendedKeys.push(decodedAccountId.xpub) + extendedInfos.extendedKeys.push(xpub) njsAccount = await timeoutTagged( 'newAWEKI', 10000, @@ -548,12 +540,12 @@ export async function syncAccount({ const syncedRawAccount = await buildAccountRaw({ njsAccount, - isSegwit, - isUnsplit, + derivationMode, + seedIdentifier, accountIndex: index, wallet: njsWallet, walletName, - currencyId, + currency, core, ops, }) @@ -577,29 +569,30 @@ export async function scanAccountsFromXPUB({ core, currencyId, xpub, - isSegwit, - isUnsplit, + derivationMode, + seedIdentifier, }: { core: *, currencyId: string, xpub: string, - isSegwit: boolean, - isUnsplit: boolean, + derivationMode: string, + seedIdentifier: string, }) { const currency = getCryptoCurrencyById(currencyId) - const walletName = encodeWalletName({ - publicKey: `debug_${xpub}`, - currencyId, - isSegwit, - isUnsplit, + const walletName = getWalletName({ + currency, + seedIdentifier: 'debug', + derivationMode, }) - const wallet = await getOrCreateWallet(core, walletName, { currencyId, isSegwit, isUnsplit }) + const wallet = await getOrCreateWallet(core, walletName, { currency, derivationMode }) await wallet.eraseDataSince(new Date(0)) const index = 0 + const isSegwit = isSegwitDerivationMode(derivationMode) + const extendedInfos = { index, owners: ['main'], @@ -616,12 +609,12 @@ export async function scanAccountsFromXPUB({ const ops = await query.complete().execute() const rawAccount = await buildAccountRaw({ njsAccount: account, - isSegwit, - isUnsplit, + derivationMode, + seedIdentifier, accountIndex: index, wallet, walletName, - currencyId, + currency, core, ops, }) diff --git a/src/reducers/accounts.js b/src/reducers/accounts.js index dad9d98f..82075e9e 100644 --- a/src/reducers/accounts.js +++ b/src/reducers/accounts.js @@ -2,7 +2,7 @@ import { createSelector } from 'reselect' import { handleActions } from 'redux-actions' -import { createAccountModel } from '@ledgerhq/live-common/lib/models/account' +import accountModel from 'helpers/accountModel' import logger from 'logger' import type { Account, AccountRaw } from '@ledgerhq/live-common/lib/types' import { OUTDATED_CONSIDERED_DELAY, DEBUG_SYNC } from 'config/constants' @@ -10,8 +10,6 @@ import { OUTDATED_CONSIDERED_DELAY, DEBUG_SYNC } from 'config/constants' export type AccountsState = Account[] const state: AccountsState = [] -const accountModel = createAccountModel() - const handlers: Object = { SET_ACCOUNTS: ( state: AccountsState, diff --git a/yarn.lock b/yarn.lock index 8589c638..fc5db475 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1747,10 +1747,23 @@ bindings "^1.3.0" nan "^2.6.2" -"@ledgerhq/live-common@3.8.0": +"@ledgerhq/live-common@/Users/gre/dev/ledger-live-common": + version "3.8.0" + dependencies: + axios "^0.18.0" + bignumber.js "^7.2.1" + invariant "^2.2.2" + lodash "^4.17.4" + node-lzw "^0.3.1" + numeral "^2.0.6" + prando "^3.0.1" + react "^16.4.0" + react-redux "^5.0.7" + redux "^4.0.0" + reselect "^3.0.1" + +"@ledgerhq/live-common@file:../ledger-live-common": version "3.8.0" - resolved "https://registry.yarnpkg.com/@ledgerhq/live-common/-/live-common-3.8.0.tgz#8a956f4995c6006d7788a98959d0b0cab92307f6" - integrity sha512-ZfhNP9Zt6LGVJhwIdGqeqUZdele6cWEiGDsuXl5NGxS5BGuFZFp7MfrQ7IQWIaePQ5/A1FMy4SoOkgdPskqtvw== dependencies: axios "^0.18.0" bignumber.js "^7.2.1" @@ -1792,6 +1805,21 @@ node-fetch "^2.1.1" url-template "^2.0.8" +"@octokit/rest@^15.2.6": + version "15.13.1" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-15.13.1.tgz#68b12ff0e470ad70c90e99bbb69f16bcdf62edb4" + integrity sha512-r6aRAZaaUZkTqtI4seaSamvgqmYswXpxclIqUrwtFtOuRAnE7l0aeWU252vQ/mxd1wKZWMq1oFChzk0/qzcYcg== + dependencies: + before-after-hook "^1.1.0" + btoa-lite "^1.0.0" + debug "^3.1.0" + http-proxy-agent "^2.1.0" + https-proxy-agent "^2.2.0" + lodash "^4.17.4" + node-fetch "^2.1.1" + universal-user-agent "^2.0.0" + url-template "^2.0.8" + "@posthtml/esm@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@posthtml/esm/-/esm-1.0.0.tgz#09bcb28a02438dcee22ad1970ca1d85a000ae0cf" @@ -7879,6 +7907,27 @@ flow-typed@^2.4.0: which "^1.3.0" yargs "^4.2.0" +flow-typed@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/flow-typed/-/flow-typed-2.5.1.tgz#0ff565cc94d2af8c557744ba364b6f14726a6b9f" + integrity sha1-D/VlzJTSr4xVd0S6NktvFHJqa58= + dependencies: + "@octokit/rest" "^15.2.6" + babel-polyfill "^6.26.0" + colors "^1.1.2" + fs-extra "^5.0.0" + glob "^7.1.2" + got "^7.1.0" + md5 "^2.1.0" + mkdirp "^0.5.1" + rimraf "^2.6.2" + semver "^5.5.0" + table "^4.0.2" + through "^2.3.8" + unzipper "^0.8.11" + which "^1.3.0" + yargs "^4.2.0" + flush-write-stream@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd" @@ -10689,6 +10738,11 @@ lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.3: pseudomap "^1.0.2" yallist "^2.1.2" +macos-release@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-1.1.0.tgz#831945e29365b470aa8724b0ab36c8f8959d10fb" + integrity sha512-mmLbumEYMi5nXReB9js3WGsB8UE6cDBWyIO62Z4DNx6GbRhDxHNjA1MlzSpJ2S2KM1wyiPRA0d19uHWYYvMHjA== + make-dir@^1.0.0, make-dir@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" @@ -11728,6 +11782,14 @@ os-locale@^2.0.0: lcid "^1.0.0" mem "^1.1.0" +os-name@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/os-name/-/os-name-2.0.1.tgz#b9a386361c17ae3a21736ef0599405c9a8c5dc5e" + integrity sha1-uaOGNhwXrjohc27wWZQFyajF3F4= + dependencies: + macos-release "^1.0.0" + win-release "^1.0.0" + os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -12745,6 +12807,14 @@ qrcode@^1.2.0: pngjs "^3.3.0" yargs "^8.0.2" +qrloop@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/qrloop/-/qrloop-0.6.1.tgz#167f8868f03018c7625b0e887049830e9253beae" + integrity sha512-1wJwoKpukmkfqwzyb9lMMEE9BDWXlKv63J/gdWMIeUfzgSR3QrZ3VKTtjN9Xvyo1DWwJd/G3FuMtJaH9cf+XUw== + dependencies: + flow-typed "^2.5.1" + md5 "^2.2.1" + qs@6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" @@ -14171,6 +14241,11 @@ semver-diff@^2.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA== +semver@^5.0.1: + version "5.5.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" + integrity sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw== + semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" @@ -15640,6 +15715,13 @@ unist-util-visit@^1.1.0, unist-util-visit@^1.3.0: dependencies: unist-util-is "^2.1.1" +universal-user-agent@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-2.0.1.tgz#18e591ca52b1cb804f6b9cbc4c336cf8191f80e1" + integrity sha512-vz+heWVydO0iyYAa65VHD7WZkYzhl7BeNVy4i54p4TF8OMiLSXdbuQe4hm+fmWAsL+rVibaQHXfhvkw3c1Ws2w== + dependencies: + os-name "^2.0.1" + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -16401,6 +16483,13 @@ wif@^2.0.1: dependencies: bs58check "<3.0.0" +win-release@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/win-release/-/win-release-1.1.1.tgz#5fa55e02be7ca934edfc12665632e849b72e5209" + integrity sha1-X6VeAr58qTTt/BJmVjLoSbcuUgk= + dependencies: + semver "^5.0.1" + window-or-global@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/window-or-global/-/window-or-global-1.0.1.tgz#dbe45ba2a291aabc56d62cf66c45b7fa322946de"