Browse Source

Merge pull request #1610 from meriadec/devtools-improvement

Ability to import multiple xpubs at once
gre-patch-1
Meriadec Pillet 6 years ago
committed by GitHub
parent
commit
586f060d3b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 302
      src/components/DevToolsPage/AccountImporter.js
  2. 4
      src/helpers/libcore.js

302
src/components/DevToolsPage/AccountImporter.js

@ -1,5 +1,7 @@
// @flow // @flow
/* eslint-disable react/no-multi-comp */
import React, { PureComponent, Fragment } from 'react' import React, { PureComponent, Fragment } from 'react'
import invariant from 'invariant' import invariant from 'invariant'
import { connect } from 'react-redux' import { connect } from 'react-redux'
@ -9,7 +11,8 @@ import type { Currency, Account } from '@ledgerhq/live-common/lib/types'
import { decodeAccount } from 'reducers/accounts' import { decodeAccount } from 'reducers/accounts'
import { addAccount } from 'actions/accounts' import { addAccount } from 'actions/accounts'
import FormattedVal from 'components/base/FormattedVal' import FakeLink from 'components/base/FakeLink'
import Ellipsis from 'components/base/Ellipsis'
import Switch from 'components/base/Switch' import Switch from 'components/base/Switch'
import Spinner from 'components/base/Spinner' import Spinner from 'components/base/Spinner'
import Box, { Card } from 'components/base/Box' import Box, { Card } from 'components/base/Box'
@ -32,26 +35,40 @@ type Props = {
addAccount: Account => void, addAccount: Account => void,
} }
const INITIAL_STATE = { type ImportableAccountType = {
status: 'idle', name: string,
currency: null, currency: Currency,
xpub: '', derivationMode: string,
account: null, xpub: string,
isSegwit: true,
isUnsplit: false,
error: null,
} }
type State = { type State = {
status: string, status: string,
importableAccounts: ImportableAccountType[],
currency: ?Currency, currency: ?Currency,
xpub: string, xpub: string,
account: ?Account, name: string,
isSegwit: boolean, isSegwit: boolean,
isUnsplit: boolean, isUnsplit: boolean,
error: ?Error, error: ?Error,
} }
const INITIAL_STATE = {
status: 'idle',
currency: null,
xpub: '',
name: 'dev',
isSegwit: true,
isUnsplit: false,
error: null,
importableAccounts: [],
}
class AccountImporter extends PureComponent<Props, State> { class AccountImporter extends PureComponent<Props, State> {
state = INITIAL_STATE state = INITIAL_STATE
@ -67,142 +84,199 @@ class AccountImporter extends PureComponent<Props, State> {
onChangeXPUB = xpub => this.setState({ xpub }) onChangeXPUB = xpub => this.setState({ xpub })
onChangeSegwit = isSegwit => this.setState({ isSegwit }) onChangeSegwit = isSegwit => this.setState({ isSegwit })
onChangeUnsplit = isUnsplit => this.setState({ isUnsplit }) onChangeUnsplit = isUnsplit => this.setState({ isUnsplit })
onChangeName = name => this.setState({ name })
isValid = () => { isValid = () => {
const { currency, xpub } = this.state const { currency, xpub, status } = this.state
return !!currency && !!xpub return !!currency && !!xpub && status !== 'scanning'
} }
scan = async () => { scan = async () => {
if (!this.isValid()) return
this.setState({ status: 'scanning' }) this.setState({ status: 'scanning' })
const { importableAccounts } = this.state
try { try {
const { currency, xpub, isSegwit, isUnsplit } = this.state for (let i = 0; i < importableAccounts.length; i++) {
invariant(currency, 'no currency') const a = importableAccounts[i]
const derivationMode = isSegwit const scanPayload = {
? isUnsplit seedIdentifier: `dev_${a.xpub}`,
? 'segwit_unsplit' currencyId: a.currency.id,
: 'segwit' xpub: a.xpub,
: isUnsplit derivationMode: a.derivationMode,
? 'unsplit' }
: '' const rawAccount = await scanFromXPUB.send(scanPayload).toPromise()
const rawAccount = await scanFromXPUB const account = decodeAccount(rawAccount)
.send({ await this.import({
seedIdentifier: 'dev_tool', ...account,
currencyId: currency.id, name: a.name,
xpub,
derivationMode,
}) })
.toPromise() this.removeImportableAccount(a)
const account = decodeAccount(rawAccount) }
this.setState({ status: 'finish', account }) this.reset()
} catch (error) { } catch (error) {
this.setState({ status: 'error', error }) this.setState({ status: 'error', error })
} }
} }
import = async () => { addToScan = () => {
const { account } = this.state const { xpub, currency, isSegwit, isUnsplit, name } = this.state
const derivationMode = isSegwit
? isUnsplit
? 'segwit_unsplit'
: 'segwit'
: isUnsplit
? 'unsplit'
: ''
const importableAccount = { xpub, currency, derivationMode, name }
this.setState(({ importableAccounts }) => ({
importableAccounts: [...importableAccounts, importableAccount],
currency: null,
xpub: '',
name: 'dev',
isSegwit: true,
isUnsplit: false,
}))
}
removeImportableAccount = importableAccount => {
this.setState(({ importableAccounts }) => ({
importableAccounts: importableAccounts.filter(i => i.xpub !== importableAccount.xpub),
}))
}
import = async account => {
invariant(account, 'no account') invariant(account, 'no account')
await idleCallback() await idleCallback()
this.props.addAccount(account) this.props.addAccount(account)
this.reset()
} }
reset = () => this.setState(INITIAL_STATE) reset = () => this.setState(INITIAL_STATE)
render() { render() {
const { currency, xpub, isSegwit, isUnsplit, status, account, error } = this.state const {
currency,
xpub,
name,
isSegwit,
isUnsplit,
status,
error,
importableAccounts,
} = this.state
const supportsSplit = !!currency && !!currency.forkedFrom const supportsSplit = !!currency && !!currency.forkedFrom
return ( return (
<Card title="Import from xpub" flow={3}> <Fragment>
{status === 'idle' ? ( <Card title="Import from xpub" flow={3}>
<Fragment> {status === 'idle' || status === 'scanning' ? (
<Box flow={1}> <Fragment>
<Label>{'currency'}</Label> <Box flow={1}>
<SelectCurrency autoFocus value={currency} onChange={this.onChangeCurrency} /> <Label>{'currency'}</Label>
</Box> <SelectCurrency autoFocus value={currency} onChange={this.onChangeCurrency} />
{currency && (currency.supportsSegwit || supportsSplit) ? ( </Box>
<Box horizontal justify="flex-end" align="center" flow={3}> {currency && (currency.supportsSegwit || supportsSplit) ? (
{supportsSplit && ( <Box horizontal justify="flex-end" align="center" flow={3}>
<Box horizontal align="center" flow={1}> {supportsSplit && (
<Box ff="Museo Sans|Bold" fontSize={4}> <Box horizontal align="center" flow={1}>
{'unsplit'} <Box ff="Museo Sans|Bold" fontSize={4}>
{'unsplit'}
</Box>
<Switch isChecked={isUnsplit} onChange={this.onChangeUnsplit} />
</Box> </Box>
<Switch isChecked={isUnsplit} onChange={this.onChangeUnsplit} /> )}
</Box> {currency.supportsSegwit && (
)} <Box horizontal align="center" flow={1}>
{currency.supportsSegwit && ( <Box ff="Museo Sans|Bold" fontSize={4}>
<Box horizontal align="center" flow={1}> {'segwit'}
<Box ff="Museo Sans|Bold" fontSize={4}> </Box>
{'segwit'} <Switch isChecked={isSegwit} onChange={this.onChangeSegwit} />
</Box> </Box>
<Switch isChecked={isSegwit} onChange={this.onChangeSegwit} /> )}
</Box>
)}
</Box>
) : null}
<Box flow={1}>
<Label>{'xpub'}</Label>
<Input
placeholder="xpub"
value={xpub}
onChange={this.onChangeXPUB}
onEnter={this.scan}
/>
</Box>
<Box align="flex-end">
<Button primary small disabled={!this.isValid()} onClick={this.scan}>
{'scan'}
</Button>
</Box>
</Fragment>
) : status === 'scanning' ? (
<Box align="center" justify="center" p={5}>
<Spinner size={16} />
</Box>
) : status === 'finish' ? (
account ? (
<Box p={8} align="center" justify="center" flow={5} horizontal>
<Box horizontal flow={4} color="graphite" align="center">
{currency && <CurrencyCircleIcon size={64} currency={currency} />}
<Box>
<Box ff="Museo Sans|Bold">{account.name}</Box>
<FormattedVal
fontSize={2}
alwaysShowSign={false}
color="graphite"
unit={account.unit}
showCode
val={account.balance || 0}
/>
<Box fontSize={2}>{`${account.operations.length} operation(s)`}</Box>
</Box> </Box>
) : null}
<Box flow={1}>
<Label>{'xpub'}</Label>
<Input
placeholder="xpub"
value={xpub}
onChange={this.onChangeXPUB}
onEnter={this.addToScan}
/>
</Box> </Box>
<Box flow={1}>
<Button outline small disabled={!account} onClick={this.import}> <Label>{'name'}</Label>
{'import'} <Input
</Button> placeholder="name"
</Box> value={name}
) : ( onChange={this.onChangeName}
onEnter={this.addToScan}
/>
</Box>
<Box align="flex-end">
<Button primary small disabled={!this.isValid()} onClick={this.addToScan}>
{'add to scan'}
</Button>
</Box>
</Fragment>
) : status === 'error' ? (
<Box align="center" justify="center" p={5} flow={4}> <Box align="center" justify="center" p={5} flow={4}>
<Box>{'No accounts found or wrong xpub'}</Box> <Box>
<TranslatedError error={error} />
</Box>
<Button primary onClick={this.reset} small autoFocus> <Button primary onClick={this.reset} small autoFocus>
{'Reset'} {'Reset'}
</Button> </Button>
</Box> </Box>
) ) : null}
) : status === 'error' ? ( </Card>
<Box align="center" justify="center" p={5} flow={4}> {!!importableAccounts.length && (
<Box> <Card flow={2}>
<TranslatedError error={error} /> {importableAccounts.map((acc, i) => (
</Box> <ImportableAccount
<Button primary onClick={this.reset} small autoFocus> key={acc.xpub}
{'Reset'} importableAccount={acc}
</Button> onRemove={this.removeImportableAccount}
</Box> isLoading={status === 'scanning' && i === 0}
) : null} >
</Card> {acc.xpub}
</ImportableAccount>
))}
{status !== 'scanning' && (
<Box mt={4} align="flex-start">
<Button primary onClick={this.scan}>
{'Launch scan'}
</Button>
</Box>
)}
</Card>
)}
</Fragment>
)
}
}
class ImportableAccount extends PureComponent<{
importableAccount: ImportableAccountType,
onRemove: ImportableAccountType => void,
isLoading: boolean,
}> {
remove = () => {
this.props.onRemove(this.props.importableAccount)
}
render() {
const { importableAccount, isLoading } = this.props
return (
<Box horizontal flow={2} align="center">
{isLoading && <Spinner size={16} color="rgba(0, 0, 0, 0.3)" />}
<CurrencyCircleIcon currency={importableAccount.currency} size={24} />
<Box grow ff="Rubik" fontSize={3}>
<Ellipsis>{`[${importableAccount.name}] ${importableAccount.derivationMode ||
'default'} ${importableAccount.xpub}`}</Ellipsis>
</Box>
{!isLoading && (
<FakeLink onClick={this.remove} fontSize={3}>
{'Remove'}
</FakeLink>
)}
</Box>
) )
} }
} }

4
src/helpers/libcore.js

@ -557,14 +557,12 @@ export async function scanAccountsFromXPUB({
const currency = getCryptoCurrencyById(currencyId) const currency = getCryptoCurrencyById(currencyId)
const walletName = getWalletName({ const walletName = getWalletName({
currency, currency,
seedIdentifier: 'debug', seedIdentifier,
derivationMode, derivationMode,
}) })
const wallet = await getOrCreateWallet(core, walletName, { currency, derivationMode }) const wallet = await getOrCreateWallet(core, walletName, { currency, derivationMode })
await wallet.eraseDataSince(new Date(0))
const index = 0 const index = 0
const isSegwit = isSegwitDerivationMode(derivationMode) const isSegwit = isSegwitDerivationMode(derivationMode)

Loading…
Cancel
Save