From cab7d05f364a4144386d206046228a480e6a2e0e Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Fri, 5 Feb 2021 10:18:02 +0100 Subject: [PATCH] Ledger derive compat option (#4529) * Ledger derive compat option * Hook in Ledger path * Derive operational * Bump util * Use hdLedger from util-crypto * Optional approach via fill --- packages/page-accounts/src/modals/Create.tsx | 232 +++++++++++------- .../src/modals/CreateSuriLedger.tsx | 86 +++++++ packages/page-accounts/src/modals/Ledger.tsx | 7 +- 3 files changed, 227 insertions(+), 98 deletions(-) create mode 100644 packages/page-accounts/src/modals/CreateSuriLedger.tsx diff --git a/packages/page-accounts/src/modals/Create.tsx b/packages/page-accounts/src/modals/Create.tsx index 8d4ac0323d30..52c96bc8e2f0 100644 --- a/packages/page-accounts/src/modals/Create.tsx +++ b/packages/page-accounts/src/modals/Create.tsx @@ -4,33 +4,35 @@ import type { ActionStatus } from '@polkadot/react-components/Status/types'; import type { ThemeProps } from '@polkadot/react-components/types'; import type { CreateResult } from '@polkadot/ui-keyring/types'; -import type { KeypairType } from '@polkadot/util-crypto/types'; import type { ModalProps } from '../types'; import FileSaver from 'file-saver'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import styled from 'styled-components'; import { DEV_PHRASE } from '@polkadot/keyring/defaults'; import { getEnvironment } from '@polkadot/react-api/util'; import { AddressRow, Button, Checkbox, CopyButton, Dropdown, Expander, Input, InputAddress, MarkError, MarkWarning, Modal, TextArea } from '@polkadot/react-components'; -import { useApi } from '@polkadot/react-hooks'; +import { useApi, useLedger } from '@polkadot/react-hooks'; import { keyring } from '@polkadot/ui-keyring'; import { settings } from '@polkadot/ui-settings'; import { isHex, u8aToHex } from '@polkadot/util'; -import { keyExtractSuri, mnemonicGenerate, mnemonicValidate, randomAsU8a } from '@polkadot/util-crypto'; +import { hdLedger, keyExtractSuri, mnemonicGenerate, mnemonicValidate, randomAsU8a } from '@polkadot/util-crypto'; import { useTranslation } from '../translate'; import CreateConfirmation from './CreateConfirmation'; +import CreateSuriLedger from './CreateSuriLedger'; import ExternalWarning from './ExternalWarning'; import PasswordInput from './PasswordInput'; +type PairType = 'ecdsa' | 'ed25519' | 'ed25519-ledger' | 'ethereum' | 'sr25519'; + interface Props extends ModalProps { className?: string; onClose: () => void; onStatusChange: (status: ActionStatus) => void; seed?: string; - type?: KeypairType; + type?: PairType; } type SeedType = 'bip' | 'raw' | 'dev'; @@ -40,7 +42,7 @@ interface AddressState { derivePath: string; deriveValidation? : DeriveValidationOutput isSeedValid: boolean; - pairType: KeypairType; + pairType: PairType; seed: string; seedType: SeedType; } @@ -59,7 +61,13 @@ interface DeriveValidationOutput { const DEFAULT_PAIR_TYPE = 'sr25519'; const STEPS_COUNT = 3; -function deriveValidate (seed: string, seedType: SeedType, derivePath: string, pairType: KeypairType): DeriveValidationOutput { +function getSuri (seed: string, derivePath: string, pairType: PairType): string { + return pairType === 'ed25519-ledger' + ? u8aToHex(hdLedger(seed, derivePath).secretKey.slice(0, 32)) + : `${seed}${derivePath}`; +} + +function deriveValidate (seed: string, seedType: SeedType, derivePath: string, pairType: PairType): DeriveValidationOutput { try { const { password, path } = keyExtractSuri(`${seed}${derivePath}`); let result: DeriveValidationOutput = {}; @@ -93,9 +101,9 @@ function rawValidate (seed: string): boolean { return ((seed.length > 0) && (seed.length <= 32)) || isHexSeed(seed); } -function addressFromSeed (phrase: string, derivePath: string, pairType: KeypairType): string { +function addressFromSeed (seed: string, derivePath: string, pairType: PairType): string { return keyring - .createFromUri(`${phrase.trim()}${derivePath}`, {}, pairType) + .createFromUri(getSuri(seed, derivePath, pairType), {}, pairType === 'ed25519-ledger' ? 'ed25519' : pairType) .address; } @@ -110,7 +118,7 @@ function newSeed (seed: string | undefined | null, seedType: SeedType): string { } } -function generateSeed (_seed: string | undefined | null, derivePath: string, seedType: SeedType, pairType: KeypairType = DEFAULT_PAIR_TYPE): AddressState { +function generateSeed (_seed: string | undefined | null, derivePath: string, seedType: SeedType, pairType: PairType = DEFAULT_PAIR_TYPE): AddressState { const seed = newSeed(_seed, seedType); const address = addressFromSeed(seed, derivePath, pairType); @@ -125,8 +133,9 @@ function generateSeed (_seed: string | undefined | null, derivePath: string, see }; } -function updateAddress (seed: string, derivePath: string, seedType: SeedType, pairType: KeypairType): AddressState { +function updateAddress (seed: string, derivePath: string, seedType: SeedType, pairType: PairType): AddressState { const deriveValidation = deriveValidate(seed, seedType, derivePath, pairType); + let isSeedValid = seedType === 'raw' ? rawValidate(seed) : mnemonicValidate(seed); @@ -136,6 +145,8 @@ function updateAddress (seed: string, derivePath: string, seedType: SeedType, pa try { address = addressFromSeed(seed, derivePath, pairType); } catch (error) { + console.error(error); + isSeedValid = false; } } @@ -157,12 +168,12 @@ export function downloadAccount ({ json, pair }: CreateResult): void { FileSaver.saveAs(blob, `${pair.address}.json`); } -function createAccount (suri: string, pairType: KeypairType, { genesisHash, name, tags = [] }: CreateOptions, password: string, success: string): ActionStatus { +function createAccount (seed: string, derivePath: string, pairType: PairType, { genesisHash, name, tags = [] }: CreateOptions, password: string, success: string): ActionStatus { // we will fill in all the details below const status = { action: 'create' } as ActionStatus; try { - const result = keyring.addUri(suri, password, { genesisHash, isHardware: false, name, tags }, pairType); + const result = keyring.addUri(getSuri(seed, derivePath, pairType), password, { genesisHash, isHardware: false, name, tags }, pairType === 'ed25519-ledger' ? 'ed25519' : pairType); const { address } = result.pair; status.account = address; @@ -185,6 +196,7 @@ function createAccount (suri: string, pairType: KeypairType, { genesisHash, name function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, type: propsType }: Props): React.ReactElement { const { t } = useTranslation(); const { api, isDevelopment, isEthereum } = useApi(); + const { isLedgerEnabled } = useLedger(); const [{ address, derivePath, deriveValidation, isSeedValid, pairType, seed, seedType }, setAddress] = useState(() => generateSeed(propsSeed, '', propsSeed ? 'raw' : 'bip', isEthereum ? 'ethereum' : propsType)); const [isMnemonicSaved, setIsMnemonicSaved] = useState(false); const [step, setStep] = useState(1); @@ -194,34 +206,43 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ const isFirstStepValid = !!address && isMnemonicSaved && !deriveValidation?.error && isSeedValid; const isSecondStepValid = isNameValid && isPasswordValid; const isValid = isFirstStepValid && isSecondStepValid; - const errorIndex: Record = useMemo(() => ({ + + const errorIndex = useRef>({ PASSWORD_IGNORED: t('Password are ignored for hex seed'), SOFT_NOT_ALLOWED: t('Soft derivation paths are not allowed on ed25519'), WARNING_SLASH_PASSWORD: t('Your password contains at least one "/" character. Disregard this warning if it is intended.') - }), [t]); + }); - const seedOpt = useMemo(() => ( + const seedOpt = useRef(( isDevelopment ? [{ text: t('Development'), value: 'dev' }] : [] ).concat( { text: t('Mnemonic'), value: 'bip' }, - isEthereum ? { text: t('Private Key'), value: 'raw' } : { text: t('Raw seed'), value: 'raw' } - ), [isEthereum, isDevelopment, t]); - - const _onChangeDerive = useCallback( - (newDerivePath: string) => setAddress(updateAddress(seed, newDerivePath, seedType, pairType)), + isEthereum + ? { text: t('Private Key'), value: 'raw' } + : { text: t('Raw seed'), value: 'raw' } + )); + + const _onChangePath = useCallback( + (newDerivePath: string) => setAddress( + updateAddress(seed, newDerivePath, seedType, pairType) + ), [pairType, seed, seedType] ); const _onChangeSeed = useCallback( - (newSeed: string) => setAddress(updateAddress(newSeed, derivePath, seedType, pairType)), + (newSeed: string) => setAddress( + updateAddress(newSeed, derivePath, seedType, pairType) + ), [derivePath, pairType, seedType] ); const _onChangePairType = useCallback( - (newPairType: KeypairType) => setAddress(updateAddress(seed, derivePath, seedType, newPairType)), - [derivePath, seed, seedType] + (newPairType: PairType) => setAddress( + updateAddress(seed, '', seedType, newPairType) + ), + [seed, seedType] ); const _selectSeedType = useCallback( @@ -264,8 +285,8 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ setIsBusy(true); setTimeout((): void => { - const options = { genesisHash: isDevelopment ? undefined : api.genesisHash.toString(), name: name.trim() }; - const status = createAccount(`${seed}${derivePath}`, pairType, options, password, t('created account')); + const options = { genesisHash: isDevelopment ? undefined : api.genesisHash.toString(), isHardware: false, name: name.trim() }; + const status = createAccount(seed, derivePath, pairType, options, password, t('created account')); onStatusChange(status); setIsBusy(false); @@ -320,7 +341,7 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ defaultValue={seedType} isButton onChange={_selectSeedType} - options={seedOpt} + options={seedOpt.current} /> ('Determines what cryptography will be used to create this account. Note that to validate on Polkadot, the session account must use "ed25519".')} label={t('keypair crypto type')} onChange={_onChangePairType} - options={isEthereum ? settings.availableCryptosEth : settings.availableCryptos} + options={ + isEthereum + ? settings.availableCryptosEth + : isLedgerEnabled + ? settings.availableCryptosLedger + : settings.availableCryptos + } tabIndex={-1} /> @@ -353,37 +380,45 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ

{t('If you are moving accounts between applications, ensure that you use the correct type.')}

- - - ('You can set a custom derivation path for this account using the following syntax "///". The "/" and "//" may be repeated and mixed`. An optional "///" can be used with a mnemonic seed, and may only be specified once.')} - isError={!!deriveValidation?.error} - label={t('secret derivation path')} - onChange={_onChangeDerive} - onEnter={_onCommit} - placeholder={ - seedType === 'raw' - ? pairType === 'sr25519' - ? t('//hard/soft') - : t('//hard') - : pairType === 'sr25519' - ? t('//hard/soft///password') - : t('//hard///password') - } - tabIndex={-1} - value={derivePath} + {pairType === 'ed25519-ledger' + ? ( + - {deriveValidation?.error && ( - - )} - {deriveValidation?.warning && ( - - )} - - -

{t('The derivation path allows you to create different accounts from the same base mnemonic.')}

-
-
+ ) + : ( + + + ('You can set a custom derivation path for this account using the following syntax "///". The "/" and "//" may be repeated and mixed`. An optional "///" can be used with a mnemonic seed, and may only be specified once.')} + isError={!!deriveValidation?.error} + label={t('secret derivation path')} + onChange={_onChangePath} + placeholder={ + seedType === 'raw' + ? pairType === 'sr25519' + ? t('//hard/soft') + : t('//hard') + : pairType === 'sr25519' + ? t('//hard/soft///password') + : t('//hard///password') + } + tabIndex={-1} + value={derivePath} + /> + {deriveValidation?.error && ( + + )} + {deriveValidation?.warning && ( + + )} + + +

{t('The derivation path allows you to create different accounts from the same base mnemonic.')}

+
+
+ )} @@ -426,51 +461,58 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ } - {step === 3 && address && - } + {step === 3 && address && ( + + )} {step === 1 && +