Skip to content

Commit

Permalink
Merge pull request #5595 from LiskHQ/5593-add-private-key
Browse files Browse the repository at this point in the history
Add private key import feature
  • Loading branch information
shuse2 authored Jul 19, 2024
2 parents 69d367b + 92d8e10 commit 3bd92b1
Show file tree
Hide file tree
Showing 22 changed files with 1,093 additions and 2 deletions.
5 changes: 5 additions & 0 deletions src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,13 @@
"Enter network name, e.g Testnet": "Enter network name, e.g Testnet",
"Enter password": "Enter password",
"Enter password confirmation": "Enter password confirmation",
"Enter private key": "Enter private key",
"Enter public key": "Enter public key",
"Enter service URL, e.g. https://testnet-service.lisk.com": "Enter service URL, e.g. https://testnet-service.lisk.com",
"Enter stake amount": "Enter stake amount",
"Enter websocket service URL, e.g. wss://testnet-service.lisk.com": "Enter websocket service URL, e.g. wss://testnet-service.lisk.com",
"Enter your account password": "Enter your account password",
"Enter your private key to manage your account.": "Enter your private key to manage your account.",
"Enter your secret recovery phrase to manage your account.": "Enter your secret recovery phrase to manage your account.",
"Error": "Error",
"Error loading application data": "Error loading application data",
Expand Down Expand Up @@ -296,6 +298,7 @@
"If you just made the transaction, it will take up to a few minutes to be included in the blockchain. Please open this page later.": "If you just made the transaction, it will take up to a few minutes to be included in the blockchain. Please open this page later.",
"Import account": "Import account",
"Import account from hardware wallet": "Import account from hardware wallet",
"Import private key": "Import private key",
"In order to use this feature you need to sign in to your Lisk account.": "In order to use this feature you need to sign in to your Lisk account.",
"Index": "Index",
"Information": "Information",
Expand Down Expand Up @@ -484,6 +487,8 @@
"Priority": "Priority",
"Privacy": "Privacy",
"Privacy policy": "Privacy policy",
"Private key": "Private key",
"Private key should be represented as a hexadecimal string with length of 64 characters.": "Private key should be represented as a hexadecimal string with length of 64 characters.",
"Provide Feedback": "Provide Feedback",
"Provide a correct amount of {{token}}": "Provide a correct amount of {{token}}",
"Provide a correct wallet address or the name of a bookmarked account": "Provide a correct wallet address or the name of a bookmarked account",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@import '../../../../../setup/react/app/mixins.css';

.addAccount {
align-items: center;
display: flex;
flex-grow: 1;
margin: 0;
min-height: 100%;
justify-content: center;
text-align: center;
width: 100%;
}

.wrapper {
align-items: center;
display: flex;
flex-direction: column;
padding: 20px 24px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* eslint-disable max-statements */
import React, { useRef, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import grid from 'flexboxgrid/dist/flexboxgrid.css';
import MultiStep from 'src/modules/common/components/MultiStep';
import SetPasswordSuccess from 'src/modules/auth/components/SetPasswordSuccess';
import PrivateKeyForm from '@auth/components/PrivateKeyForm';
import routes from 'src/routes/routes';
import { useCurrentAccount, useAccounts } from '@account/hooks';
import ImportPrivateKeyForm from '../ImportPrivateKeyForm';
import styles from './AddAccountByPrivateKey.css';

const AddAccountByPrivateKey = () => {
const history = useHistory();
const { search } = useLocation();
const multiStepRef = useRef(null);
const [privateKey, setPrivateKey] = useState(null);
const [currentAccount, setCurrentAccount] = useCurrentAccount();
const { setAccount } = useAccounts();

const queryParams = new URLSearchParams(search);
const referrer = queryParams.get('referrer');

const onAddAccount = (privateKeyData) => {
setPrivateKey(privateKeyData);
multiStepRef?.current?.next();
};

/* istanbul ignore next */
const onSetPassword = (account) => {
setCurrentAccount(account, null, false);
setAccount(account);
multiStepRef?.current?.next();
};

/* istanbul ignore next */
const onPasswordSetComplete = () => {
history.push(referrer || routes.wallet.path);
};

return (
<div className={`${styles.addAccount} ${grid.row}`}>
<MultiStep navStyles={{ multiStepWrapper: styles.wrapper }} ref={multiStepRef}>
<ImportPrivateKeyForm onAddAccount={onAddAccount} />
<PrivateKeyForm
privateKey={privateKey}
onSubmit={onSetPassword}
/>
<SetPasswordSuccess encryptedPhrase={currentAccount} onClose={onPasswordSetComplete} />
</MultiStep>
</div>
);
};

export default AddAccountByPrivateKey;
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { createEvent, fireEvent, screen, waitFor } from '@testing-library/react';
import mockSavedAccounts from '@tests/fixtures/accounts';
import { mockOnMessage } from '@setup/config/setupJest';
import * as reactRedux from 'react-redux';
import { renderWithCustomRouter } from 'src/utils/testHelpers';
import AddAccountByPrivateKey from './AddAccountByPrivateKey';

const privateKey =
'e005805e731d324ec6f083f7ec31967e60cda674cd09f51c323fce63a933e0dadd2df9b2b007bd8a2387f4e652517d6e094cdb54edf0c67b06d4786f5ecf964d';
const accountPassword = 'Password1$';
const userName = 'user1';
const mockSetAccount = jest.fn();

jest.mock('react-i18next');
jest.mock('@account/hooks', () => ({
useAccounts: jest.fn(() => ({
accounts: mockSavedAccounts,
setAccount: jest.fn(),
})),
useCurrentAccount: jest.fn(() => [mockSavedAccounts[0], mockSetAccount]),
useEncryptAccount: jest.fn().mockReturnValue({
encryptAccount: jest.fn().mockResolvedValue({
privateKey,
}),
}),
}));

reactRedux.useSelector = jest.fn().mockReturnValue(mockSavedAccounts[0]);

const props = {
history: { push: jest.fn() },
login: jest.fn(),
};

beforeEach(() => {
renderWithCustomRouter(AddAccountByPrivateKey, props);
});

describe('Add account by private key flow', () => {
it('Should successfully go though the flow', async () => {
expect(screen.getByText('Add your account')).toBeTruthy();
expect(
screen.getByText('Enter your private key to manage your account.')
).toBeTruthy();
expect(screen.getByText('Continue to set password')).toBeTruthy();
expect(screen.getByText('Go back')).toBeTruthy();

const inputField = screen.getByTestId('recovery-1');
const pasteEvent = createEvent.paste(inputField, {
clipboardData: {
getData: () =>
'e005805e731d324ec6f083f7ec31967e60cda674cd09f51c323fce63a933e0dadd2df9b2b007bd8a2387f4e652517d6e094cdb54edf0c67b06d4786f5ecf964d',
},
});

fireEvent(inputField, pasteEvent);
fireEvent.click(screen.getByText('Continue to set password'));

const password = screen.getByTestId('password');
const cPassword = screen.getByTestId('cPassword');
const accountName = screen.getByTestId('accountName');
const hasAgreed = screen.getByTestId('hasAgreed');

fireEvent.change(password, { target: { value: accountPassword } });
fireEvent.change(cPassword, { target: { value: accountPassword } });
fireEvent.change(accountName, { target: { value: userName } });
fireEvent.click(hasAgreed);
fireEvent.click(screen.getByText('Save Account'));

await waitFor(() => {
expect(mockOnMessage).toHaveBeenCalledWith({
accountName: 'user1',
cPassword: 'Password1$',
customDerivationPath: "m/44'/134'/0'",
enableAccessToLegacyAccounts: undefined,
hasAgreed: true,
password: 'Password1$',
privateKey: {
isValid: true,
value: 'e005805e731d324ec6f083f7ec31967e60cda674cd09f51c323fce63a933e0dadd2df9b2b007bd8a2387f4e652517d6e094cdb54edf0c67b06d4786f5ecf964d',
},
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/* istanbul ignore file */
import AddAccountByPrivateKey from './AddAccountByPrivateKey';

export default AddAccountByPrivateKey;
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ const addAccountOptions = (t) => [
iconName: 'accountUpload',
pathname: routes.addAccountByFile.path,
},
{
text: t('Import private key'),
iconName: 'secretPassphrase',
pathname: routes.addAccountByPrivateKey.path,
},
];

const AddAccountOptionButton = ({ iconName, text, onClick }) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe('Add Account Choice', () => {
expect(screen.getByText('Don’t have a Lisk account yet?')).toBeTruthy();
expect(screen.getByText('Restore from backup')).toBeTruthy();
expect(screen.getByText('Secret recovery phrase')).toBeTruthy();
expect(screen.getByText('Import private key')).toBeTruthy();
});

it('should redirect to /account/add/secret-recovery', async () => {
Expand All @@ -43,4 +44,9 @@ describe('Add Account Choice', () => {
fireEvent.click(screen.getByText('Restore from backup'));
expect(props.history.push).toBeCalled();
});

it('should redirect to /account/add/add-private-key', async () => {
fireEvent.click(screen.getByText('Import private key'));
expect(props.history.push).toBeCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const BackupRecoveryPhraseFlow = () => {
const [passphrase, setPassphrase] = useState('');

const onEnterPasswordSuccess = ({ recoveryPhrase }) => {
setPassphrase(recoveryPhrase);
setPassphrase(recoveryPhrase || '');
multiStepRef.current.next();
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
@import '../../../../../setup/react/app/mixins.css';

:root {
--login-title-font-size: 24px;
}

.wrapper {
width: 100%;
}

.addAccount {
align-items: center;
display: flex;
flex-grow: 1;
margin: 0;
min-height: 100%;
justify-content: center;
text-align: center;
width: 100%;
flex-direction: column;
}

.titleHolder {
text-align: center;

& > .stepsLabel {
@mixin contentSmallest;

color: var(--color-slate-gray);
display: block;
margin-bottom: 10px;
text-transform: uppercase;
}

& > h1 {
@mixin headingLarge;

display: flex;
justify-content: center;
margin: 0 0 16px;
font-size: var(--font-size-h3);
line-height: var(--font-size-h3);
}

& > p {
@mixin contentLargest;

color: var(--color-slate-gray);
margin: 0;
letter-spacing: 1px;
}
}

.buttonsHolder {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
margin: 30px auto 0;
max-width: 430px;
width: 100%;

& a {
width: 100%;
}
}

.inputFields {
& > fieldset {
align-items: flex-start;
display: flex;
flex-direction: column;
width: 100%;
border: none;
text-align: left;

& > label {
margin-bottom: 7px;
color: var(--color-content);
font-size: var(--font-size-small-secondary);

& .notice {
color: var(--color-primary);
}
}
}

& > * {
max-width: 665px;
margin: 30px auto 0px;
}
}

.link {
@mixin contentLarge bold;

color: var(--color-link);
cursor: pointer;
display: inline-block;
margin-bottom: 20px;
text-align: center;
text-decoration: none;

&:hover {
text-decoration: underline;
}
}

form {
& .button,
& .backLink {
@mixin contentLargest bold;

width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}

& .backLink {
margin-top: 24px;
}
}

.labelWrapper {
display: flex;

& .fieldGroup {
font-size: 16px;
color: var(--color-colecti-dark-blue);

&:last-child {
margin-left: 10px;
}

&.checkboxField {
align-items: flex-start;
display: flex;

& > div:last-child {
flex-basis: calc(100% - 25px);
margin-left: auto;
}

& .checkmark {
margin-top: 3px;
}

& .checkbox {
margin-right: 10px;
}
}
}

& .discreetMode {
& span {
font-size: 16px;
color: var(--color-colecti-dark-blue);
}
}
}
Loading

0 comments on commit 3bd92b1

Please sign in to comment.