Prevent from typing a derivation with ///password since it is ignored (#622)
* implementation and test * Update packages/extension-ui/src/Popup/Derive/SelectParent.tsx * Update packages/extension-ui/src/Popup/Derive/SelectParent.tsx * add path check and tests
This commit is contained in:
@@ -18,9 +18,10 @@ interface Props extends ThemeProps{
|
||||
onChange: (suri: string) => void;
|
||||
parentAddress: string;
|
||||
parentPassword: string;
|
||||
withSoftPath: boolean;
|
||||
}
|
||||
|
||||
function DerivationPath ({ className, defaultPath, isError, onChange }: Props): React.ReactElement<Props> {
|
||||
function DerivationPath ({ className, defaultPath, isError, onChange, withSoftPath }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [path, setPath] = useState<string>(defaultPath);
|
||||
const [isDisabled, setIsDisabled] = useState(true);
|
||||
@@ -46,7 +47,10 @@ function DerivationPath ({ className, defaultPath, isError, onChange }: Props):
|
||||
: t('Derivation Path')
|
||||
}
|
||||
onChange={_onChange}
|
||||
placeholder={t<string>('//hard/soft')}
|
||||
placeholder={withSoftPath
|
||||
? t<string>('//hard/soft')
|
||||
: t<string>('//hard')
|
||||
}
|
||||
value={path}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -33,19 +33,19 @@ const accounts = [
|
||||
{ address: '5D2TPhGEy2FhznvzaNYW9AkuMBbg3cyRemnPsBvBY4ZhkZXA', name: 'BB', parentAddress: '5GYmFzQCuC5u3tQNiMZNbFGakrz3Jq31NmMg4D2QAkSoQ2g5', type: 'sr25519' },
|
||||
{ address: '5GhGENSJBWQZ8d8mARKgqEkiAxiW3hHeznQDW2iG4XzNieb6', isExternal: true, name: 'C', type: 'sr25519' },
|
||||
{ address: '0xd5D81CD4236a43F48A983fc5B895975c511f634D', name: 'Ethereum', type: 'ethereum' },
|
||||
{ address: '5EeaoDj4VDk8V6yQngKBaCD5MpJUCHrhYjVhBjgMHXoYon1s', isExternal: false, name: 'D', type: 'sr25519' },
|
||||
{ address: '5HRKYp5anSNGtqC7cq9ftiaq4y8Mk7uHk7keaXUrQwZqDWLJ', name: 'DD', parentAddress: '5EeaoDj4VDk8V6yQngKBaCD5MpJUCHrhYjVhBjgMHXoYon1s', type: 'sr25519' }
|
||||
{ address: '5EeaoDj4VDk8V6yQngKBaCD5MpJUCHrhYjVhBjgMHXoYon1s', isExternal: false, name: 'D', type: 'ed25519' },
|
||||
{ address: '5HRKYp5anSNGtqC7cq9ftiaq4y8Mk7uHk7keaXUrQwZqDWLJ', name: 'DD', parentAddress: '5EeaoDj4VDk8V6yQngKBaCD5MpJUCHrhYjVhBjgMHXoYon1s', type: 'ed25519' }
|
||||
] as AccountJson[];
|
||||
|
||||
describe('Derive', () => {
|
||||
const mountComponent = async (locked = false): Promise<{
|
||||
const mountComponent = async (locked = false, account = 1): Promise<{
|
||||
wrapper: ReactWrapper;
|
||||
onActionStub: jest.Mock;
|
||||
}> => {
|
||||
const onActionStub = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
<MemoryRouter initialEntries={ [`/account/derive/${accounts[1].address}`] }>
|
||||
<MemoryRouter initialEntries={ [`/account/derive/${accounts[account].address}`] }>
|
||||
<ActionContext.Provider value={onActionStub}>
|
||||
<AccountContext.Provider value={{
|
||||
accounts: accounts,
|
||||
@@ -98,11 +98,12 @@ describe('Derive', () => {
|
||||
return { address: derivedAddress, suri: defaultDerivation } as ResponseDeriveValidate;
|
||||
});
|
||||
|
||||
it('Button is disabled and password field visible', () => {
|
||||
it('Button is disabled and password field visible, path field is hidden', () => {
|
||||
const button = wrapper.find('[data-button-action="create derived account"] button');
|
||||
|
||||
expect(button.exists()).toBe(true);
|
||||
expect(button.prop('disabled')).toBe(true);
|
||||
expect(wrapper.find('.pathInput').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('Password field is visible and not in error state', () => {
|
||||
@@ -154,6 +155,30 @@ describe('Derive', () => {
|
||||
expect(wrapper.find('.warning-message')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('Derivation path gets visible, is set and locked', async () => {
|
||||
await type(wrapper.find('input[type="password"]'), 'wrong_pass');
|
||||
|
||||
expect(wrapper.find('.pathInput.locked input').prop('disabled')).toBe(true);
|
||||
expect(wrapper.find('.pathInput.locked input').prop('value')).toBe('//1');
|
||||
});
|
||||
|
||||
it('Derivation path can be unlocked', async () => {
|
||||
await type(wrapper.find('input[type="password"]'), 'wrong_pass');
|
||||
wrapper.find('FontAwesomeIcon.lockIcon').simulate('click');
|
||||
await act(flushAllPromises);
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('.pathInput').exists()).toBe(true);
|
||||
expect(wrapper.find('.pathInput input').prop('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('Derivation path placeholder contains //hard/soft', async () => {
|
||||
await type(wrapper.find('input[type="password"]'), parentPassword);
|
||||
const pathInput = wrapper.find('[data-input-suri] input');
|
||||
|
||||
expect(pathInput.first().prop('placeholder')).toEqual('//hard/soft');
|
||||
});
|
||||
|
||||
it('An error is visible and the button is disabled when suri is incorrect', async () => {
|
||||
await type(wrapper.find('input[type="password"]'), parentPassword);
|
||||
await type(wrapper.find('[data-input-suri] input'), '//');
|
||||
@@ -165,7 +190,29 @@ describe('Derive', () => {
|
||||
|
||||
expect(button.prop('disabled')).toBe(true);
|
||||
expect(wrapper.find('.warning-message')).toHaveLength(1);
|
||||
expect(wrapper.find('.warning-message').first().text()).toEqual('Incorrect derivation path');
|
||||
expect(wrapper.find('.warning-message').first().text()).toEqual('Invalid derivation path');
|
||||
});
|
||||
|
||||
it('An error is visible and the button is disabled when suri contains `///`', async () => {
|
||||
await type(wrapper.find('input[type="password"]'), parentPassword);
|
||||
await type(wrapper.find('[data-input-suri] input'), '///');
|
||||
|
||||
const button = wrapper.find('[data-button-action="create derived account"] button');
|
||||
|
||||
expect(button.prop('disabled')).toBe(true);
|
||||
expect(wrapper.find('.warning-message')).toHaveLength(1);
|
||||
// eslint-disable-next-line quotes
|
||||
expect(wrapper.find('.warning-message').first().text()).toEqual("`///password` not supported for derivation");
|
||||
});
|
||||
|
||||
it('No error is shown when suri contains soft derivation `/` with sr25519', async () => {
|
||||
await type(wrapper.find('input[type="password"]'), parentPassword);
|
||||
await type(wrapper.find('[data-input-suri] input'), '//somehard/soft');
|
||||
|
||||
const button = wrapper.find('[data-button-action="create derived account"] button');
|
||||
|
||||
expect(button.prop('disabled')).toBe(false);
|
||||
expect(wrapper.find('.warning-message')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('The error disappears and "Create derived account" is enabled when typing a new suri', async () => {
|
||||
@@ -228,8 +275,6 @@ describe('Derive', () => {
|
||||
await act(flushAllPromises);
|
||||
wrapper.update();
|
||||
await enterName(newAccount.name).then(password(newAccount.password)).then(repeat(newAccount.password));
|
||||
await act(flushAllPromises);
|
||||
wrapper.update();
|
||||
wrapper.find('[data-button-action="add new root"] button').simulate('click');
|
||||
await act(flushAllPromises);
|
||||
wrapper.update();
|
||||
@@ -239,4 +284,33 @@ describe('Derive', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ed25519 Parent', () => {
|
||||
beforeEach(async () => {
|
||||
const mountedComponent = await mountComponent(false, 5);
|
||||
|
||||
wrapper = mountedComponent.wrapper;
|
||||
onActionStub = mountedComponent.onActionStub;
|
||||
await type(wrapper.find('input[type="password"]'), parentPassword);
|
||||
});
|
||||
|
||||
it('Derivation path placeholder only contains //hard', () => {
|
||||
const pathInput = wrapper.find('[data-input-suri] input');
|
||||
|
||||
expect(pathInput.first().prop('placeholder')).toEqual('//hard');
|
||||
});
|
||||
|
||||
it('An error is shown when suri contains soft derivation `/` with ed25519', async () => {
|
||||
const pathInput = wrapper.find('[data-input-suri] input');
|
||||
|
||||
await type(pathInput, '//somehard/soft');
|
||||
|
||||
const button = wrapper.find('[data-button-action="create derived account"] button');
|
||||
|
||||
expect(button.prop('disabled')).toBe(true);
|
||||
expect(wrapper.find('[data-input-suri]').first().prop('isError')).toBe(true);
|
||||
expect(wrapper.find('.warning-message')).toHaveLength(1);
|
||||
expect(wrapper.find('.warning-message').first().text()).toEqual('Soft derivation is only allowed for sr25519 accounts');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { canDerive } from '@polkadot/extension-base/utils';
|
||||
import { assert } from '@polkadot/util';
|
||||
|
||||
import { AccountContext, ActionContext, Address, ButtonArea, InputWithLabel, Label, NextStepButton, VerticalSpace, Warning } from '../../components';
|
||||
import useTranslation from '../../hooks/useTranslation';
|
||||
@@ -21,6 +20,9 @@ interface Props {
|
||||
onDerivationConfirmed: (derivation: { account: { address: string; suri: string }; parentPassword: string }) => void;
|
||||
}
|
||||
|
||||
// match any single slash
|
||||
const singleSlashRegex = /([^/]|^)\/([^/]|$)/;
|
||||
|
||||
export default function SelectParent ({ className, isLocked, onDerivationConfirmed, parentAddress, parentGenesis }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const onAction = useContext(ActionContext);
|
||||
@@ -30,7 +32,24 @@ export default function SelectParent ({ className, isLocked, onDerivationConfirm
|
||||
const [suriPath, setSuriPath] = useState<null | string>(defaultPath);
|
||||
const [parentPassword, setParentPassword] = useState<string>('');
|
||||
const [isProperParentPassword, setIsProperParentPassword] = useState(false);
|
||||
const [pathError, setPathError] = useState('');
|
||||
const passwordInputRef = useRef<HTMLDivElement>(null);
|
||||
const allowSoftDerivation = useMemo(() => {
|
||||
const parent = accounts.find(({ address }) => address === parentAddress);
|
||||
|
||||
return parent?.type === 'sr25519';
|
||||
}, [accounts, parentAddress]);
|
||||
|
||||
useEffect(() => {
|
||||
// forbid the use of password since Keyring ignores it
|
||||
if (suriPath?.includes('///')) {
|
||||
setPathError(t('`///password` not supported for derivation'));
|
||||
}
|
||||
|
||||
if (!allowSoftDerivation && suriPath && singleSlashRegex.test(suriPath)) {
|
||||
setPathError(t('Soft derivation is only allowed for sr25519 accounts'));
|
||||
}
|
||||
}, [allowSoftDerivation, suriPath, t]);
|
||||
|
||||
const allAddresses = useMemo(
|
||||
() => hierarchy
|
||||
@@ -48,6 +67,14 @@ export default function SelectParent ({ className, isLocked, onDerivationConfirm
|
||||
[]
|
||||
);
|
||||
|
||||
const _onSuriPathChange = useCallback(
|
||||
(path: string): void => {
|
||||
setSuriPath(path);
|
||||
setPathError('');
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const _onParentChange = useCallback(
|
||||
(address: string) => onAction(`/account/derive/${address}`),
|
||||
[onAction]
|
||||
@@ -64,11 +91,11 @@ export default function SelectParent ({ className, isLocked, onDerivationConfirm
|
||||
try {
|
||||
const account = await validateDerivationPath(parentAddress, suriPath, parentPassword);
|
||||
|
||||
assert(account, 'Unable to derive');
|
||||
onDerivationConfirmed({ account, parentPassword });
|
||||
} catch (error) {
|
||||
setIsBusy(false);
|
||||
setSuriPath(null);
|
||||
setPathError(t('Invalid derivation path'));
|
||||
console.error(error);
|
||||
}
|
||||
} else {
|
||||
setIsBusy(false);
|
||||
@@ -76,7 +103,7 @@ export default function SelectParent ({ className, isLocked, onDerivationConfirm
|
||||
}
|
||||
}
|
||||
},
|
||||
[parentAddress, parentPassword, onDerivationConfirmed, suriPath]
|
||||
[parentAddress, parentPassword, onDerivationConfirmed, suriPath, t]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -89,71 +116,70 @@ export default function SelectParent ({ className, isLocked, onDerivationConfirm
|
||||
return (
|
||||
<>
|
||||
<div className={className}>
|
||||
<div className=''>
|
||||
{isLocked
|
||||
? (
|
||||
<Address
|
||||
address={parentAddress}
|
||||
genesisHash={parentGenesis}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Label label={t<string>('Choose Parent Account:')}>
|
||||
<AddressDropdown
|
||||
allAddresses={allAddresses}
|
||||
onSelect={_onParentChange}
|
||||
selectedAddress={parentAddress}
|
||||
selectedGenesis={parentGenesis}
|
||||
/>
|
||||
</Label>
|
||||
)
|
||||
}
|
||||
<div ref={passwordInputRef}>
|
||||
<InputWithLabel
|
||||
data-input-password
|
||||
isError={!!parentPassword && !isProperParentPassword}
|
||||
isFocused
|
||||
label={t<string>('enter the password for the account you want to derive from')}
|
||||
onChange={_onParentPasswordEnter}
|
||||
type='password'
|
||||
value={parentPassword}
|
||||
{isLocked
|
||||
? (
|
||||
<Address
|
||||
address={parentAddress}
|
||||
genesisHash={parentGenesis}
|
||||
/>
|
||||
{!!parentPassword && !isProperParentPassword && (
|
||||
)
|
||||
: (
|
||||
<Label label={t<string>('Choose Parent Account:')}>
|
||||
<AddressDropdown
|
||||
allAddresses={allAddresses}
|
||||
onSelect={_onParentChange}
|
||||
selectedAddress={parentAddress}
|
||||
selectedGenesis={parentGenesis}
|
||||
/>
|
||||
</Label>
|
||||
)
|
||||
}
|
||||
<div ref={passwordInputRef}>
|
||||
<InputWithLabel
|
||||
data-input-password
|
||||
isError={!!parentPassword && !isProperParentPassword}
|
||||
isFocused
|
||||
label={t<string>('enter the password for the account you want to derive from')}
|
||||
onChange={_onParentPasswordEnter}
|
||||
type='password'
|
||||
value={parentPassword}
|
||||
/>
|
||||
{!!parentPassword && !isProperParentPassword && (
|
||||
<Warning
|
||||
isBelowInput
|
||||
isDanger
|
||||
>
|
||||
{t('Wrong password')}
|
||||
</Warning>
|
||||
)}
|
||||
</div>
|
||||
{isProperParentPassword && (
|
||||
<>
|
||||
<DerivationPath
|
||||
defaultPath={defaultPath}
|
||||
isError={!!pathError}
|
||||
onChange={_onSuriPathChange}
|
||||
parentAddress={parentAddress}
|
||||
parentPassword={parentPassword}
|
||||
withSoftPath={allowSoftDerivation}
|
||||
/>
|
||||
{(!!pathError) && (
|
||||
<Warning
|
||||
isBelowInput
|
||||
isDanger
|
||||
>
|
||||
{t('Wrong password')}
|
||||
{pathError}
|
||||
</Warning>
|
||||
)}
|
||||
</div>
|
||||
{isProperParentPassword && (
|
||||
<>
|
||||
<DerivationPath
|
||||
defaultPath={defaultPath}
|
||||
isError={!suriPath}
|
||||
onChange={setSuriPath}
|
||||
parentAddress={parentAddress}
|
||||
parentPassword={parentPassword}
|
||||
/>
|
||||
{!suriPath && (
|
||||
<Warning
|
||||
isBelowInput
|
||||
isDanger
|
||||
>
|
||||
{t('Incorrect derivation path')}
|
||||
</Warning>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<VerticalSpace/>
|
||||
<ButtonArea>
|
||||
<NextStepButton
|
||||
data-button-action='create derived account'
|
||||
isBusy={isBusy}
|
||||
isDisabled={!isProperParentPassword || !suriPath}
|
||||
isDisabled={!isProperParentPassword || !!pathError}
|
||||
onClick={_onSubmit}
|
||||
>
|
||||
{t<string>('Create a derived account')}
|
||||
|
||||
Reference in New Issue
Block a user