{t('Continue')}
@@ -82,4 +108,30 @@ const AddAccountForm = ({ settings, onAddAccount }) => {
);
};
+const AddAccountFormWithDerivationPath = (props) => {
+ const [derivationPath, setDerivationPath] = useState(defaultDerivationPath);
+ const derivationPathErrorMessage = useMemo(
+ () => getDerivationPathErrorMessage(derivationPath),
+ [derivationPath]
+ );
+
+ const onDerivationPathChange = (value) => {
+ setDerivationPath(value);
+ };
+
+ return (
+
+
+
+ );
+};
+
export default AddAccountForm;
diff --git a/src/modules/account/components/AddAccountForm/AddAccountForm.test.js b/src/modules/account/components/AddAccountForm/AddAccountForm.test.js
index c90d053ba9..6cb3781255 100644
--- a/src/modules/account/components/AddAccountForm/AddAccountForm.test.js
+++ b/src/modules/account/components/AddAccountForm/AddAccountForm.test.js
@@ -20,7 +20,7 @@ beforeEach(() => {
});
});
-describe('Generals', () => {
+describe('AddAccountForm', () => {
it('should render successfully', () => {
expect(screen.getByText('Add account')).toBeTruthy();
expect(
@@ -85,19 +85,52 @@ describe('Generals', () => {
expect(screen.queryByText('Select Network')).toBeTruthy();
});
- it('should render the custom derivation path field with no default value', () => {
- jest.clearAllMocks();
- accountFormInstance = renderWithStore(AddAccountForm, props, {
- settings: { enableCustomDerivationPath: true },
+ it('should have disabled button if derivation path has an error', () => {
+ props.settings.enableCustomDerivationPath = true;
+ accountFormInstance.rerender(
);
+
+ const input = screen.getByLabelText('Custom derivation path');
+ fireEvent.change(input, { target: { value: 'incorrectPath' } });
+
+ const passphraseInput1 = screen.getByTestId('recovery-1');
+ const pasteEvent = createEvent.paste(passphraseInput1, {
+ clipboardData: {
+ getData: () =>
+ 'below record evolve eye youth post control consider spice swamp hidden easily',
+ },
});
+ fireEvent(passphraseInput1, pasteEvent);
- expect(accountFormInstance.getByDisplayValue(defaultDerivationPath)).toBeTruthy();
+ expect(screen.getByText('Continue')).toHaveAttribute('disabled');
});
- it('should render the custom derivation path field with default value', () => {
- accountFormInstance = renderWithStore(AddAccountForm, props, {
- settings: { enableCustomDerivationPath: true, customDerivationPath: `m/0/2'` },
+ it('should trigger add account if derivation path and passphrase is correct', () => {
+ props.settings.enableCustomDerivationPath = true;
+ accountFormInstance.rerender(
);
+
+ const input = screen.getByLabelText('Custom derivation path');
+ const correctDerivationPath = "m/44'/134'/0'";
+ fireEvent.change(input, { target: { value: correctDerivationPath } });
+
+ const passphrase = 'below record evolve eye youth post control consider spice swamp hidden easily';
+ const passphraseInput1 = screen.getByTestId('recovery-1');
+ const pasteEvent = createEvent.paste(passphraseInput1, {
+ clipboardData: {
+ getData: () =>
+ passphrase,
+ },
});
- expect(screen.getByDisplayValue(`m/0/2'`)).toBeTruthy();
+ fireEvent(passphraseInput1, pasteEvent);
+
+ fireEvent.click(screen.getByText('Continue'));
+
+ expect(props.onAddAccount).toHaveBeenCalledWith({ value: passphrase, isValid: true }, correctDerivationPath);
+ });
+
+ it('should render the custom derivation path field with no default value', () => {
+ props.settings.enableCustomDerivationPath = true;
+ accountFormInstance.rerender(
);
+
+ expect(accountFormInstance.getByDisplayValue(defaultDerivationPath)).toBeTruthy();
});
});
diff --git a/src/modules/account/hooks/useEncryptAccount.js b/src/modules/account/hooks/useEncryptAccount.js
index f57062ff44..cf1548811c 100644
--- a/src/modules/account/hooks/useEncryptAccount.js
+++ b/src/modules/account/hooks/useEncryptAccount.js
@@ -3,8 +3,8 @@ import { selectSettings } from 'src/redux/selectors';
import { encryptAccount as encryptAccountUtils } from '@account/utils';
// eslint-disable-next-line
-export function useEncryptAccount() {
- const { enableCustomDerivationPath, customDerivationPath } = useSelector(selectSettings);
+export function useEncryptAccount(customDerivationPath) {
+ const { enableCustomDerivationPath } = useSelector(selectSettings);
const encryptAccount = ({ recoveryPhrase, password, name }) => encryptAccountUtils({
recoveryPhrase,
password,
diff --git a/src/modules/auth/components/CustomDerivationPath/index.js b/src/modules/auth/components/CustomDerivationPath/index.js
index 24730abd68..2c549ee11f 100644
--- a/src/modules/auth/components/CustomDerivationPath/index.js
+++ b/src/modules/auth/components/CustomDerivationPath/index.js
@@ -1,32 +1,25 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
-import { useDispatch, useSelector } from 'react-redux';
-import { settingsUpdated } from 'src/redux/actions';
-import { selectSettings } from 'src/redux/selectors';
import { Input } from 'src/theme';
-import { defaultDerivationPath } from 'src/utils/explicitBipKeyDerivation';
-import styles from '../Signin/login.css';
-const CustomDerivationPath = () => {
- const { enableCustomDerivationPath, customDerivationPath } = useSelector(selectSettings);
- const dispatch = useDispatch();
+const CustomDerivationPath = ({ onChange, value, errorMessage }) => {
const { t } = useTranslation();
const onPathInputChange = (e) => {
- dispatch(settingsUpdated({ customDerivationPath: e.target.value }));
+ onChange(e.target.value);
};
- if (!enableCustomDerivationPath) return null;
-
return (
);
diff --git a/src/modules/auth/components/CustomDerivationPath/index.test.js b/src/modules/auth/components/CustomDerivationPath/index.test.js
new file mode 100644
index 0000000000..a232002450
--- /dev/null
+++ b/src/modules/auth/components/CustomDerivationPath/index.test.js
@@ -0,0 +1,23 @@
+import { render, fireEvent } from '@testing-library/react';
+import React from 'react';
+
+import CustomDerivationPath from './index';
+
+describe('CustomDerivationPath', () => {
+ const props = {
+ onChange: jest.fn(),
+ value: '',
+ };
+
+ it('Should render without breaking', () => {
+ render(
);
+ });
+
+ it('Should call onChange when input changes', () => {
+ const { getByLabelText } = render(
);
+ const input = getByLabelText('Custom derivation path');
+ fireEvent.change(input, { target: { value: 'hello' } });
+
+ expect(props.onChange).toHaveBeenCalledWith('hello');
+ });
+});
diff --git a/src/modules/auth/components/SetPasswordForm/SetPasswordForm.js b/src/modules/auth/components/SetPasswordForm/SetPasswordForm.js
index 65044519fb..84b4a26bf4 100644
--- a/src/modules/auth/components/SetPasswordForm/SetPasswordForm.js
+++ b/src/modules/auth/components/SetPasswordForm/SetPasswordForm.js
@@ -26,9 +26,9 @@ const setPasswordFormSchema = yup.object({
hasAgreed: yup.boolean().required(),
}).required();
-function SetPasswordForm({ onSubmit, recoveryPhrase }) {
+function SetPasswordForm({ onSubmit, recoveryPhrase, customDerivationPath }) {
const { t } = useTranslation();
- const { encryptAccount } = useEncryptAccount();
+ const { encryptAccount } = useEncryptAccount(customDerivationPath);
const {
register,
handleSubmit,
diff --git a/src/modules/auth/components/Signin/login.css b/src/modules/auth/components/Signin/login.css
index f74561e276..b75cd2fc39 100644
--- a/src/modules/auth/components/Signin/login.css
+++ b/src/modules/auth/components/Signin/login.css
@@ -123,11 +123,6 @@
}
}
-.derivationPathInput {
- height: 40px !important;
- margin-top: var(--vertical-padding-m);
-}
-
.link {
@mixin contentLarge bold;
diff --git a/src/modules/auth/components/Signin/login.test.js b/src/modules/auth/components/Signin/login.test.js
index 86cee77121..672303fd3b 100644
--- a/src/modules/auth/components/Signin/login.test.js
+++ b/src/modules/auth/components/Signin/login.test.js
@@ -1,11 +1,9 @@
import React from 'react';
import i18next from 'i18next';
import { mount } from 'enzyme';
-import { useDispatch } from 'react-redux';
import { mountWithRouterAndStore } from 'src/utils/testHelpers';
import routes from 'src/routes/routes';
import { defaultDerivationPath } from 'src/utils/explicitBipKeyDerivation';
-import { settingsUpdated } from 'src/redux/actions';
import accounts from '@tests/constants/wallets';
import Login from './login';
@@ -158,33 +156,5 @@ describe('Login', () => {
wrapper.find('.custom-derivation-path-input').exists(),
).toBeFalsy();
});
-
- it('Should display custom derivation path and dispatch settings updated action', () => {
- const mockDispatch = jest.fn();
- useDispatch.mockReturnValue(mockDispatch);
- wrapper = mountWithRouterAndStore(
- Login,
- props,
- {},
- {
- settings: {
- enableCustomDerivationPath: true,
- customDerivationPath: defaultDerivationPath,
- },
- },
- );
-
- expect(
- wrapper.find('.custom-derivation-path-input').at(1).props().value,
- ).toBe(defaultDerivationPath);
- wrapper
- .find('.custom-derivation-path-input')
- .at(1)
- .simulate('change', { target: { value: "m/44'/134'/1'" } });
- wrapper.update();
- expect(mockDispatch).toHaveBeenCalledWith(
- settingsUpdated({ customDerivationPath: "m/44'/134'/1'" }),
- );
- });
});
});
diff --git a/src/modules/wallet/components/PassphraseInput/PassphraseInput.js b/src/modules/wallet/components/PassphraseInput/PassphraseInput.js
index 8e86be9817..1ca1c99335 100644
--- a/src/modules/wallet/components/PassphraseInput/PassphraseInput.js
+++ b/src/modules/wallet/components/PassphraseInput/PassphraseInput.js
@@ -17,7 +17,7 @@ class passphraseInput extends React.Component {
this.state = {
showPassphrase: false,
partialPassphraseError: [],
- values: [],
+ values: props.values || [],
focus: 0,
validationError: '',
passphraseIsInvalid: false,
diff --git a/src/modules/wallet/utils/account.js b/src/modules/wallet/utils/account.js
index 5d5cf61ab6..79b28523bd 100644
--- a/src/modules/wallet/utils/account.js
+++ b/src/modules/wallet/utils/account.js
@@ -1,6 +1,7 @@
/* eslint-disable max-lines, max-len */
import { passphrase as LiskPassphrase, cryptography } from '@liskhq/lisk-client';
import { regex } from 'src/const/regex';
+import i18next from 'i18next';
/**
* Extracts Lisk PrivateKey/PublicKey pair from a given valid Mnemonic passphrase
@@ -303,3 +304,48 @@ export const validate2ndPass = async (account, passphrase, error) => {
}
return messages;
};
+
+/**
+ * Validate a derivation path
+ * TODO: Replace this function when @liskhq (lisk-sdk) exposes the one they use: Issue #7877 https://github.com/LiskHQ/lisk-sdk/issues/7877
+ * @param {string} derivationPath
+ * @returns {string} - Error message
+ */
+export const getDerivationPathErrorMessage = (derivationPath) => {
+ if ((!derivationPath || !derivationPath.startsWith('m')) || !derivationPath.includes('/')) {
+ return i18next.t('Invalid path format');
+ }
+
+ try {
+ const MAX_UINT32 = 4294967295;
+ const HARDENED_OFFSET = 0x80000000;
+
+ derivationPath
+ .split('/')
+ // slice first element which is `m`
+ .slice(1)
+ .map((segment) => {
+ if (!/^[0-9']+$/g.test(segment)) {
+ throw new Error('Invalid path format');
+ }
+
+ // if segment includes apostrophe add HARDENED_OFFSET
+ if (segment.includes(`'`)) {
+ if (parseInt(segment.slice(0, -1), 10) > MAX_UINT32 / 2) {
+ throw new Error('Invalid path format');
+ }
+ return parseInt(segment, 10) + HARDENED_OFFSET;
+ }
+
+ if (parseInt(segment, 10) > MAX_UINT32) {
+ throw new Error('Invalid path format');
+ }
+
+ return parseInt(segment, 10);
+ });
+ } catch (error) {
+ return i18next.t(error.message);
+ }
+
+ return undefined;
+};
diff --git a/src/theme/Input/input.css b/src/theme/Input/input.css
index 19bd7ba3f0..e8da5017b7 100644
--- a/src/theme/Input/input.css
+++ b/src/theme/Input/input.css
@@ -64,7 +64,7 @@
}
& .status {
- top: calc(var(--height-m) / 2 + 4px);
+ top: calc(var(--height-m) / 2);
right: var(--horizontal-padding-m);
}
diff --git a/src/theme/buttons/css/base.css b/src/theme/buttons/css/base.css
index 2117d92a46..ba2a9a7ddf 100644
--- a/src/theme/buttons/css/base.css
+++ b/src/theme/buttons/css/base.css
@@ -65,6 +65,7 @@
&:disabled {
opacity: 0.58;
+ cursor: default;
}
}