Browse Source

Add DevTools page and ability to import accounts via xpub

master
meriadec 6 years ago
parent
commit
65bb796d45
No known key found for this signature in database GPG Key ID: 1D2FC2305E2CB399
  1. 2
      src/commands/index.js
  2. 29
      src/commands/libcoreScanFromXPUB.js
  3. 207
      src/components/DevToolsPage/AccountImporter.js
  4. 11
      src/components/DevToolsPage/index.js
  5. 32
      src/components/MainSideBar/index.js
  6. 2
      src/components/base/Input/index.js
  7. 2
      src/components/layout/Default.js
  8. 9
      src/config/cryptocurrencies.js
  9. 77
      src/helpers/libcore.js
  10. 3
      static/i18n/en/app.json

2
src/commands/index.js

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

29
src/commands/libcoreScanFromXPUB.js

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

207
src/components/DevToolsPage/AccountImporter.js

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

11
src/components/DevToolsPage/index.js

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

32
src/components/MainSideBar/index.js

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

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

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

2
src/components/layout/Default.js

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

9
src/config/cryptocurrencies.js

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

77
src/helpers/libcore.js

@ -15,20 +15,11 @@ import type { NJSAccount, NJSOperation } from '@ledgerhq/ledger-core/src/ledgerc
import { isSegwitPath, isUnsplitPath } from 'helpers/bip32'
import * as accountIdHelper from 'helpers/accountId'
import { NoAddressesFound } from 'config/errors'
import { splittedCurrencies } from 'config/cryptocurrencies'
import { deserializeError } from './errors'
import { getAccountPlaceholderName, getNewAccountPlaceholderName } from './accountName'
import { timeoutTagged } from './promise'
// TODO: put that info inside currency itself
const SPLITTED_CURRENCIES = {
bitcoin_cash: {
coinType: 0,
},
bitcoin_gold: {
coinType: 0,
},
}
// each time there is a breaking change that needs use to clear cache on both libcore and js side,
// we should bump a nonce in this map
// tech notes:
@ -84,7 +75,7 @@ export async function scanAccountsOnDevice(props: Props): Promise<AccountRaw[]>
}
// TODO: put that info inside currency itself
if (currencyId in SPLITTED_CURRENCIES) {
if (currencyId in splittedCurrencies) {
const splittedAccounts = await scanAccountsOnDeviceBySegwit({
...commonParams,
isSegwit: false,
@ -118,7 +109,7 @@ function encodeWalletName({
isSegwit: boolean,
isUnsplit: boolean,
}) {
const splitConfig = isUnsplit ? SPLITTED_CURRENCIES[currencyId] || null : null
const splitConfig = isUnsplit ? splittedCurrencies[currencyId] || null : null
return `${publicKey}__${currencyId}${isSegwit ? '_segwit' : ''}${splitConfig ? '_unsplit' : ''}`
}
@ -142,7 +133,7 @@ async function scanAccountsOnDeviceBySegwit({
isUnsplit: boolean,
}): Promise<AccountRaw[]> {
const customOpts =
isUnsplit && SPLITTED_CURRENCIES[currencyId] ? SPLITTED_CURRENCIES[currencyId] : null
isUnsplit && splittedCurrencies[currencyId] ? splittedCurrencies[currencyId] : null
const { coinType } = customOpts ? customOpts.coinType : getCryptoCurrencyById(currencyId)
const path = `${isSegwit ? '49' : '44'}'/${coinType}'`
@ -345,7 +336,7 @@ async function getOrCreateWallet(
return wallet
} catch (err) {
const currency = await timeoutTagged('getCurrency', 5000, pool.getCurrency(currencyId))
const splitConfig = isUnsplit ? SPLITTED_CURRENCIES[currencyId] || null : null
const splitConfig = isUnsplit ? splittedCurrencies[currencyId] || null : null
const coinType = splitConfig ? splitConfig.coinType : '<coin_type>'
const walletConfig = isSegwit
? {
@ -530,7 +521,7 @@ export async function syncAccount({
const decodedAccountId = accountIdHelper.decode(accountId)
const { walletName } = decodedAccountId
const isSegwit = isSegwitPath(freshAddressPath)
const isUnsplit = isUnsplitPath(freshAddressPath, SPLITTED_CURRENCIES[currencyId])
const isUnsplit = isUnsplitPath(freshAddressPath, splittedCurrencies[currencyId])
const njsWallet = await getOrCreateWallet(core, walletName, currencyId, isSegwit, isUnsplit)
let njsAccount
@ -584,3 +575,59 @@ export function libcoreAmountToBigNumber(njsAmount: *): BigNumber {
export function bigNumberToLibcoreAmount(core: *, njsWalletCurrency: *, bigNumber: BigNumber) {
return new core.NJSAmount(njsWalletCurrency, 0).fromHex(njsWalletCurrency, bigNumber.toString(16))
}
export async function scanAccountsFromXPUB({
core,
currencyId,
xpub,
isSegwit,
isUnsplit,
}: {
core: *,
currencyId: string,
xpub: string,
isSegwit: boolean,
isUnsplit: boolean,
}) {
const currency = getCryptoCurrencyById(currencyId)
const walletName = encodeWalletName({
publicKey: `debug_${xpub}`,
currencyId,
isSegwit,
isUnsplit,
})
const wallet = await getOrCreateWallet(core, walletName, currencyId, isSegwit, isUnsplit)
await wallet.eraseDataSince(new Date(0))
const index = 0
const extendedInfos = {
index,
owners: ['main'],
derivations: [
`${isSegwit ? '49' : '44'}'/${currency.coinType}'`,
`${isSegwit ? '49' : '44'}'/${currency.coinType}'/0`,
],
extendedKeys: [xpub],
}
const account = await wallet.newAccountWithExtendedKeyInfo(extendedInfos)
await coreSyncAccount(core, account)
const query = account.queryOperations()
const ops = await query.complete().execute()
const rawAccount = await buildAccountRaw({
njsAccount: account,
isSegwit,
isUnsplit,
accountIndex: index,
wallet,
walletName,
currencyId,
core,
ops,
})
return rawAccount
}

3
static/i18n/en/app.json

@ -78,7 +78,8 @@
"menu": "Menu",
"accounts": "Accounts ({{count}})",
"manager": "Manager",
"exchange": "Buy/Trade"
"exchange": "Buy/Trade",
"developer": "Dev tools"
},
"account": {
"lastOperations": "Last operations",

Loading…
Cancel
Save