Skip to content

Commit

Permalink
Feat/lw 9709 prompt password when unlocking account (#901)
Browse files Browse the repository at this point in the history
* fix(ui): password box LW-9709

The PasswordBox component layout in lace/ui wasn't responsive
and broke in different width scenarios. This commit fixes
the layout issues so that the password box can have any size.

* feat(core): enable account password prompt LW-9709

This is the basis of the core component which will later
be wired-up in Lace to prompt for the wallet password
when enabling an account.

* feat(extension): wire-up password prompt to enable account LW-9709

extracted the password prompting out of the wallet
manager into the UI and optionally pass it down
instead.

* feat(core): add ui for enabling account HW flow LW-9709

* feat(extension): integrate ui for enabling account HW in Lace LW-9709

* fix(extension): improve unit test for adding accounts LW-9709
  • Loading branch information
DominikGuzei authored Mar 12, 2024
1 parent 0749851 commit 16a0650
Show file tree
Hide file tree
Showing 26 changed files with 697 additions and 95 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ReactNode, useState, VFC } from 'react';
import React, { ReactNode, useCallback, useState, VFC } from 'react';
import { Menu, MenuProps } from 'antd';
import {
AddNewWalletLink,
Expand Down Expand Up @@ -49,6 +49,8 @@ export const DropdownMenuOverlay: VFC<Props> = ({
sendAnalyticsEvent(PostHogAction.UserWalletProfileNetworkClick);
};

const goBackToMainSection = useCallback(() => setCurrentSection(Sections.Main), []);

topSection = topSection ?? <UserInfo onOpenWalletAccounts={openWalletAccounts} />;

return (
Expand Down Expand Up @@ -77,10 +79,8 @@ export const DropdownMenuOverlay: VFC<Props> = ({
</Links>
</div>
)}
{currentSection === Sections.NetworkInfo && <NetworkInfo onBack={() => setCurrentSection(Sections.Main)} />}
{currentSection === Sections.WalletAccounts && (
<WalletAccounts onBack={() => setCurrentSection(Sections.Main)} isPopup={isPopup} />
)}
{currentSection === Sections.NetworkInfo && <NetworkInfo onBack={goBackToMainSection} />}
{currentSection === Sections.WalletAccounts && <WalletAccounts onBack={goBackToMainSection} isPopup={isPopup} />}
</Menu>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import { NavigationButton, toast } from '@lace/common';
import styles from './WalletAccounts.module.scss';
import { ProfileDropdown } from '@lace/ui';
import { AccountData } from '@lace/ui/dist/design-system/profile-dropdown/accounts/profile-dropdown-accounts-list.component';
import { DisableAccountConfirmation, EditAccountDrawer, useAccountDataModal } from '@lace/core';
import {
DisableAccountConfirmation,
EditAccountDrawer,
EnableAccountConfirmWithHW,
EnableAccountConfirmWithHWState,
EnableAccountPasswordPrompt,
useDialogWithData
} from '@lace/core';
import { useWalletStore } from '@src/stores';
import { useWalletManager } from '@hooks';
import { TOAST_DEFAULT_DURATION } from '@hooks/useActionExecution';
Expand All @@ -17,6 +24,17 @@ import { BrowserViewSections } from '@lib/scripts/types';
const defaultAccountName = (accountNumber: number) => `Account #${accountNumber}`;

const NUMBER_OF_ACCOUNTS_PER_WALLET = 24;
const HW_CONNECT_TIMEOUT_MS = 30_000;

type EnableAccountPasswordDialogData = {
accountIndex: number;
wasPasswordIncorrect?: boolean;
};

type EnableAccountHWSigningDialogData = {
accountIndex: number;
state: EnableAccountConfirmWithHWState;
};

export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack: () => void }): React.ReactElement => {
const { t } = useTranslation();
Expand All @@ -27,17 +45,21 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack:
}),
[t]
);
const editAccountDrawer = useAccountDataModal();
const backgroundServices = useBackgroundServiceAPIContext();
const disableAccountConfirmation = useAccountDataModal();
const { manageAccountsWallet: wallet, cardanoWallet, setIsDropdownMenuOpen } = useWalletStore();
const { walletRepository, addAccount, activateWallet } = useWalletManager();
const {
source: {
wallet: { walletId: activeWalletId },
account: activeAccount
}
} = cardanoWallet;
const { walletRepository, addAccount, activateWallet } = useWalletManager();

const editAccountDrawer = useDialogWithData<ProfileDropdown.AccountData | undefined>();
const disableAccountConfirmation = useDialogWithData<ProfileDropdown.AccountData | undefined>();
const enableAccountPasswordDialog = useDialogWithData<EnableAccountPasswordDialogData | undefined>();
const enableAccountHWSigningDialog = useDialogWithData<EnableAccountHWSigningDialogData | undefined>();

const disableUnlock = useMemo(
() =>
isPopup &&
Expand Down Expand Up @@ -72,20 +94,27 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack:
[wallet, activeAccount?.accountIndex, activeWalletId, disableUnlock]
);

const closeDropdownAndShowAccountActivated = useCallback(
(accountName: string) => {
setIsDropdownMenuOpen(false);
toast.notify({
duration: TOAST_DEFAULT_DURATION,
text: t('multiWallet.activated.account', { accountName })
});
},
[setIsDropdownMenuOpen, t]
);

const activateAccount = useCallback(
async (accountIndex: number) => {
await activateWallet({
walletId: wallet.walletId,
accountIndex
});
setIsDropdownMenuOpen(false);
const accountName = accountsData.find((acc) => acc.accountNumber === accountIndex)?.label;
toast.notify({
duration: TOAST_DEFAULT_DURATION,
text: t('multiWallet.activated.account', { accountName })
});
closeDropdownAndShowAccountActivated(accountName);
},
[wallet.walletId, activateWallet, accountsData, setIsDropdownMenuOpen, t]
[wallet.walletId, activateWallet, accountsData, closeDropdownAndShowAccountActivated]
);

const editAccount = useCallback(
Expand All @@ -100,27 +129,77 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack:
[disableAccountConfirmation, accountsData]
);

const showHWErrorState = useCallback(() => {
enableAccountHWSigningDialog.setData({
...enableAccountHWSigningDialog.data,
state: 'error'
});
}, [enableAccountHWSigningDialog]);

const unlockHWAccount = useCallback(
async (accountIndex: number) => {
const name = defaultAccountName(accountIndex);
try {
const timeout = setTimeout(showHWErrorState, HW_CONNECT_TIMEOUT_MS);
await addAccount({
wallet,
accountIndex,
metadata: { name }
});
clearTimeout(timeout);
enableAccountHWSigningDialog.hide();
closeDropdownAndShowAccountActivated(name);
} catch {
showHWErrorState();
}
},
[addAccount, wallet, enableAccountHWSigningDialog, closeDropdownAndShowAccountActivated, showHWErrorState]
);

const unlockAccount = useCallback(
async (accountIndex: number) => {
switch (wallet.type) {
case WalletType.InMemory:
enableAccountPasswordDialog.setData({ accountIndex });
enableAccountPasswordDialog.open();
break;
case WalletType.Ledger:
case WalletType.Trezor:
enableAccountHWSigningDialog.setData({
accountIndex,
state: 'signing'
});
enableAccountHWSigningDialog.open();
await unlockHWAccount(accountIndex);
}
},
[wallet.type, enableAccountPasswordDialog, enableAccountHWSigningDialog, unlockHWAccount]
);

const unlockInMemoryWalletAccountWithPassword = useCallback(
async (passphrase: Uint8Array) => {
const { accountIndex } = enableAccountPasswordDialog.data;
const name = defaultAccountName(accountIndex);
await addAccount({
wallet,
accountIndex,
metadata: { name }
});
setIsDropdownMenuOpen(false);
toast.notify({
duration: TOAST_DEFAULT_DURATION,
text: t('multiWallet.activated.account', { accountName: name })
});
try {
await addAccount({
wallet,
accountIndex,
passphrase,
metadata: { name: defaultAccountName(accountIndex) }
});
enableAccountPasswordDialog.hide();
closeDropdownAndShowAccountActivated(name);
} catch {
enableAccountPasswordDialog.setData({ ...enableAccountPasswordDialog.data, wasPasswordIncorrect: true });
}
},
[wallet, addAccount, t, setIsDropdownMenuOpen]
[wallet, addAccount, enableAccountPasswordDialog, closeDropdownAndShowAccountActivated]
);

const lockAccount = useCallback(async () => {
await walletRepository.removeAccount({
walletId: wallet.walletId,
accountIndex: disableAccountConfirmation.accountData.accountNumber
accountIndex: disableAccountConfirmation.data.accountNumber
});

disableAccountConfirmation.hide();
Expand All @@ -130,7 +209,7 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack:
async (newAccountName: string) => {
await walletRepository.updateAccountMetadata({
walletId: wallet.walletId,
accountIndex: editAccountDrawer.accountData.accountNumber,
accountIndex: editAccountDrawer.data.accountNumber,
metadata: { name: newAccountName }
});
editAccountDrawer.hide();
Expand Down Expand Up @@ -166,12 +245,61 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack:
/>
</div>
</div>
{/* Conditionally render the password prompt to make sure
the password is not stored in the component state */}
{enableAccountPasswordDialog.isOpen && (
<EnableAccountPasswordPrompt
open
isPopup={isPopup}
wasPasswordIncorrect={enableAccountPasswordDialog.data?.wasPasswordIncorrect}
onCancel={enableAccountPasswordDialog.hide}
onConfirm={unlockInMemoryWalletAccountWithPassword}
translations={{
title: t('account.enable.title'),
headline: t('account.enable.inMemory.headline'),
description: t('account.enable.inMemory.description'),
passwordPlaceholder: t('account.enable.inMemory.passwordPlaceholder'),
wrongPassword: t('account.enable.inMemory.wrongPassword'),
cancel: t('account.enable.inMemory.cancel'),
confirm: t('account.enable.inMemory.confirm')
}}
/>
)}
{enableAccountHWSigningDialog.isOpen && (
<EnableAccountConfirmWithHW
open
isPopup={isPopup}
onCancel={enableAccountHWSigningDialog.hide}
onRetry={() => {
enableAccountHWSigningDialog.setData({
...enableAccountHWSigningDialog.data,
state: 'signing'
});
unlockHWAccount(enableAccountHWSigningDialog.data?.accountIndex);
}}
state={enableAccountHWSigningDialog.data?.state}
translations={{
title: t('account.enable.title'),
headline: t('account.enable.hw.headline'),
errorHeadline: t('account.enable.hw.errorHeadline'),
description: t('account.enable.hw.description'),
errorDescription: t('account.enable.hw.errorDescription'),
errorHelpLink: t('account.enable.hw.errorHelpLink'),
buttons: {
cancel: t('account.enable.hw.buttons.cancel'),
waiting: t('account.enable.hw.buttons.waiting', { device: wallet.type }),
signing: t('account.enable.hw.buttons.signing'),
error: t('account.enable.hw.buttons.tryAgain')
}
}}
/>
)}
<EditAccountDrawer
onSave={renameAccount}
visible={editAccountDrawer.isOpen}
hide={editAccountDrawer.hide}
name={editAccountDrawer.accountData?.label}
index={editAccountDrawer.accountData?.accountNumber}
name={editAccountDrawer.data?.label}
index={editAccountDrawer.data?.accountNumber}
isPopup={isPopup}
translations={{
title: t('account.edit.title'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const mockEmip3encrypt = jest.fn();
const mockConnectDevice = jest.fn();
const mockRestoreWalletFromKeyAgent = jest.fn();
const mockSwitchKeyAgents = jest.fn();
const mockLedgerCheckDeviceConnection = jest.fn();
const mockLedgerGetXpub = jest.fn();
const mockTrezorGetXpub = jest.fn();
const mockInitializeTrezorTransport = jest.fn();
Expand Down Expand Up @@ -66,6 +67,7 @@ jest.mock('@lace/cardano', () => {
Ledger: {
LedgerKeyAgent: {
createWithDevice: mockLedgerCreateWithDevice,
checkDeviceConnection: mockLedgerCheckDeviceConnection,
getXpub: mockLedgerGetXpub
}
},
Expand Down Expand Up @@ -685,8 +687,8 @@ describe('Testing useWalletManager hook', () => {
'8403cf9d8267a7169381dd476f4fda48e1926fec8942ec51892e428e152fbed4835711cccb7efcae379627f477abb46c883f6b0c221f3aea40f9d931d2e8fdc69f85f16eb91ca380fc2e1edc2543e4dd71c1866208ea6c6960bca99f974e25776067e9a242b0e4066b96bd4d89ca99db5bd77bb65573b9cbeef85222ceed6d5a4dc516213ace986f03b183365505119b9a0abdc4375bfdf2363d7433'
}
},
passphrase: Buffer.from('passphrase1'),
prepare: () => {
global.prompt = jest.fn(() => 'passphrase1');
mockEmip3decrypt.mockImplementationOnce(
jest.requireActual('@lace/cardano').Wallet.KeyManagement.emip3decrypt
);
Expand All @@ -708,13 +710,14 @@ describe('Testing useWalletManager hook', () => {

it.each(walletTypes)(
'derives extended account public key for $type wallet and adds new account into the repository',
async ({ type, walletProps, prepare }) => {
async ({ type, walletProps, prepare, passphrase }) => {
prepare();
const walletId = 'bip32-wallet-id';
const addAccountProps = {
wallet: { walletId, type, ...walletProps } as AnyBip32Wallet<Wallet.WalletMetadata, Wallet.AccountMetadata>,
accountIndex: 0,
metadata: { name: 'new account' }
metadata: { name: 'new account' },
passphrase
};

const { addAccount } = render();
Expand Down
10 changes: 6 additions & 4 deletions apps/browser-extension-wallet/src/hooks/useWalletManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type WalletManagerAddAccountProps = {
wallet: AnyBip32Wallet<Wallet.WalletMetadata, Wallet.AccountMetadata>;
metadata: Wallet.AccountMetadata;
accountIndex: number;
passphrase?: Uint8Array;
};

type ActivateWalletProps = Omit<WalletManagerActivateProps, 'chainId'>;
Expand Down Expand Up @@ -101,6 +102,7 @@ const getHwExtendedAccountPublicKey = async (
) => {
switch (walletType) {
case WalletType.Ledger:
await Wallet.Ledger.LedgerKeyAgent.checkDeviceConnection(Wallet.KeyManagement.CommunicationType.Web);
return Wallet.Ledger.LedgerKeyAgent.getXpub({
communicationType: Wallet.KeyManagement.CommunicationType.Web,
deviceConnection: typeof deviceConnection !== 'boolean' ? deviceConnection : undefined,
Expand All @@ -120,13 +122,13 @@ const getHwExtendedAccountPublicKey = async (

const getExtendedAccountPublicKey = async (
wallet: AnyBip32Wallet<Wallet.WalletMetadata, Wallet.AccountMetadata>,
accountIndex: number
accountIndex: number,
passphrase?: Uint8Array
) => {
// eslint-disable-next-line sonarjs/no-small-switch
switch (wallet.type) {
case WalletType.InMemory: {
// eslint-disable-next-line no-alert
const passphrase = Buffer.from(prompt('Please enter your passphrase'));
const rootPrivateKeyBytes = await Wallet.KeyManagement.emip3decrypt(
Buffer.from(wallet.encryptedSecrets.rootPrivateKeyBytes, 'hex'),
passphrase
Expand Down Expand Up @@ -705,8 +707,8 @@ export const useWalletManager = (): UseWalletManager => {
);

const addAccount = useCallback(
async ({ wallet, accountIndex, metadata }: WalletManagerAddAccountProps): Promise<void> => {
const extendedAccountPublicKey = await getExtendedAccountPublicKey(wallet, accountIndex);
async ({ wallet, accountIndex, metadata, passphrase }: WalletManagerAddAccountProps): Promise<void> => {
const extendedAccountPublicKey = await getExtendedAccountPublicKey(wallet, accountIndex, passphrase);
await walletRepository.addAccount({
accountIndex,
extendedAccountPublicKey,
Expand Down
Loading

0 comments on commit 16a0650

Please sign in to comment.