Skip to content

Commit

Permalink
Ledger derive compat option (#4529)
Browse files Browse the repository at this point in the history
* Ledger derive compat option

* Hook in Ledger path

* Derive operational

* Bump util

* Use hdLedger from util-crypto

* Optional approach via fill
  • Loading branch information
jacogr authored Feb 5, 2021
1 parent 4fd5b8b commit cab7d05
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 98 deletions.
232 changes: 137 additions & 95 deletions packages/page-accounts/src/modals/Create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -40,7 +42,7 @@ interface AddressState {
derivePath: string;
deriveValidation? : DeriveValidationOutput
isSeedValid: boolean;
pairType: KeypairType;
pairType: PairType;
seed: string;
seedType: SeedType;
}
Expand All @@ -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 = {};
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);

Expand All @@ -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);
Expand All @@ -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;
}
}
Expand All @@ -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;
Expand All @@ -185,6 +196,7 @@ function createAccount (suri: string, pairType: KeypairType, { genesisHash, name
function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, type: propsType }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api, isDevelopment, isEthereum } = useApi();
const { isLedgerEnabled } = useLedger();
const [{ address, derivePath, deriveValidation, isSeedValid, pairType, seed, seedType }, setAddress] = useState<AddressState>(() => generateSeed(propsSeed, '', propsSeed ? 'raw' : 'bip', isEthereum ? 'ethereum' : propsType));
const [isMnemonicSaved, setIsMnemonicSaved] = useState<boolean>(false);
const [step, setStep] = useState(1);
Expand All @@ -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<string, string> = useMemo(() => ({

const errorIndex = useRef<Record<string, string>>({
PASSWORD_IGNORED: t<string>('Password are ignored for hex seed'),
SOFT_NOT_ALLOWED: t<string>('Soft derivation paths are not allowed on ed25519'),
WARNING_SLASH_PASSWORD: t<string>('Your password contains at least one "/" character. Disregard this warning if it is intended.')
}), [t]);
});

const seedOpt = useMemo(() => (
const seedOpt = useRef((
isDevelopment
? [{ text: t<string>('Development'), value: 'dev' }]
: []
).concat(
{ text: t<string>('Mnemonic'), value: 'bip' },
isEthereum ? { text: t<string>('Private Key'), value: 'raw' } : { text: t<string>('Raw seed'), value: 'raw' }
), [isEthereum, isDevelopment, t]);

const _onChangeDerive = useCallback(
(newDerivePath: string) => setAddress(updateAddress(seed, newDerivePath, seedType, pairType)),
isEthereum
? { text: t<string>('Private Key'), value: 'raw' }
: { text: t<string>('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(
Expand Down Expand Up @@ -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<string>('created account'));
const options = { genesisHash: isDevelopment ? undefined : api.genesisHash.toString(), isHardware: false, name: name.trim() };
const status = createAccount(seed, derivePath, pairType, options, password, t<string>('created account'));

onStatusChange(status);
setIsBusy(false);
Expand Down Expand Up @@ -320,7 +341,7 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
defaultValue={seedType}
isButton
onChange={_selectSeedType}
options={seedOpt}
options={seedOpt.current}
/>
<CopyButton
className='copyMoved'
Expand All @@ -345,45 +366,59 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
help={t<string>('Determines what cryptography will be used to create this account. Note that to validate on Polkadot, the session account must use "ed25519".')}
label={t<string>('keypair crypto type')}
onChange={_onChangePairType}
options={isEthereum ? settings.availableCryptosEth : settings.availableCryptos}
options={
isEthereum
? settings.availableCryptosEth
: isLedgerEnabled
? settings.availableCryptosLedger
: settings.availableCryptos
}
tabIndex={-1}
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('If you are moving accounts between applications, ensure that you use the correct type.')}</p>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<Input
help={t<string>('You can set a custom derivation path for this account using the following syntax "/<soft-key>//<hard-key>". The "/<soft-key>" and "//<hard-key>" may be repeated and mixed`. An optional "///<password>" can be used with a mnemonic seed, and may only be specified once.')}
isError={!!deriveValidation?.error}
label={t<string>('secret derivation path')}
onChange={_onChangeDerive}
onEnter={_onCommit}
placeholder={
seedType === 'raw'
? pairType === 'sr25519'
? t<string>('//hard/soft')
: t<string>('//hard')
: pairType === 'sr25519'
? t<string>('//hard/soft///password')
: t<string>('//hard///password')
}
tabIndex={-1}
value={derivePath}
{pairType === 'ed25519-ledger'
? (
<CreateSuriLedger
onChange={_onChangePath}
seedType={seedType}
/>
{deriveValidation?.error && (
<MarkError content={errorIndex[deriveValidation.error] || deriveValidation.error} />
)}
{deriveValidation?.warning && (
<MarkWarning content={errorIndex[deriveValidation.warning]} />
)}
</Modal.Column>
<Modal.Column>
<p>{t<string>('The derivation path allows you to create different accounts from the same base mnemonic.')}</p>
</Modal.Column>
</Modal.Columns>
)
: (
<Modal.Columns>
<Modal.Column>
<Input
help={t<string>('You can set a custom derivation path for this account using the following syntax "/<soft-key>//<hard-key>". The "/<soft-key>" and "//<hard-key>" may be repeated and mixed`. An optional "///<password>" can be used with a mnemonic seed, and may only be specified once.')}
isError={!!deriveValidation?.error}
label={t<string>('secret derivation path')}
onChange={_onChangePath}
placeholder={
seedType === 'raw'
? pairType === 'sr25519'
? t<string>('//hard/soft')
: t<string>('//hard')
: pairType === 'sr25519'
? t<string>('//hard/soft///password')
: t<string>('//hard///password')
}
tabIndex={-1}
value={derivePath}
/>
{deriveValidation?.error && (
<MarkError content={errorIndex.current[deriveValidation.error] || deriveValidation.error} />
)}
{deriveValidation?.warning && (
<MarkWarning content={errorIndex.current[deriveValidation.warning]} />
)}
</Modal.Column>
<Modal.Column>
<p>{t<string>('The derivation path allows you to create different accounts from the same base mnemonic.')}</p>
</Modal.Column>
</Modal.Columns>
)}
</Expander>
<Modal.Columns>
<Modal.Column>
Expand Down Expand Up @@ -426,51 +461,58 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
</Modal.Column>
</Modal.Columns>
</>}
{step === 3 && address && <CreateConfirmation
derivePath={derivePath}
isBusy={isBusy}
pairType={pairType}
seed={seed}
/>
}
{step === 3 && address && (
<CreateConfirmation
derivePath={derivePath}
isBusy={isBusy}
pairType={
pairType === 'ed25519-ledger'
? 'ed25519'
: pairType
}
seed={seed}
/>
)}
</Modal.Content>
<Modal.Actions onCancel={onClose}>
{step === 1 &&
<Button
icon='step-forward'
isDisabled={!isFirstStepValid}
label={t<string>('Next')}
onClick={_nextStep}
/>
}
{step === 2 && (
<>
<Button
icon='step-backward'
label={t<string>('Prev')}
onClick={_previousStep}
/>
<Button
icon='step-forward'
isDisabled={!isFirstStepValid}
isDisabled={!isSecondStepValid}
label={t<string>('Next')}
onClick={_nextStep}
/>
}
{step === 2 &&
<><Button
icon='step-backward'
label={t<string>('Prev')}
onClick={_previousStep}
/>
<Button
icon='step-forward'
isDisabled={!isSecondStepValid}
label={t<string>('Next')}
onClick={_nextStep}
/>
</>}
{step === 3 &&
<>
<Button
icon='step-backward'
label={t<string>('Prev')}
onClick={_previousStep}
/>
<Button
icon='plus'
isBusy={isBusy}
label={t<string>('Save')}
onClick={_onCommit}
/>
</>
}
</>
)}
{step === 3 && (
<>
<Button
icon='step-backward'
label={t<string>('Prev')}
onClick={_previousStep}
/>
<Button
icon='plus'
isBusy={isBusy}
label={t<string>('Save')}
onClick={_onCommit}
/>
</>
)}
</Modal.Actions>
</Modal>
);
Expand Down
Loading

0 comments on commit cab7d05

Please sign in to comment.