From 9448474ec3d123e53545f776e2a70d5247cfbe2a Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Mon, 29 Jan 2024 15:08:26 +0200 Subject: [PATCH 01/27] fix(ui): use correct derivation path for displaying accounts --- .../accounts/profile-dropdown-account-item.component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.component.tsx b/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.component.tsx index 700814f48..e971501ad 100644 --- a/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.component.tsx +++ b/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.component.tsx @@ -56,7 +56,7 @@ export const AccountItem = ({ className={cx.derivationPath} data-testid="wallet-account-item-path" > - m/1842`/1841/{accountNumber} + m/1852'/1815'/{accountNumber}' From d1e5effb465d988944f014236f02c057c78e360b Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Wed, 31 Jan 2024 11:39:41 +0200 Subject: [PATCH 02/27] feat(ui): disable account delete btn for active or the only account --- ...rofile-dropdown-account-item.component.tsx | 3 ++ .../profile-dropdown-account-item.stories.tsx | 2 + ...ofile-dropdown-accounts-list.component.tsx | 38 +++++++++++-------- ...profile-dropdown-accounts-list.stories.tsx | 5 +++ 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.component.tsx b/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.component.tsx index e971501ad..0739ce6d8 100644 --- a/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.component.tsx +++ b/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.component.tsx @@ -15,6 +15,7 @@ export interface Props { label: string; unlockLabel: string; isUnlocked: boolean; + isDeletable: boolean; onEditClick?: (accountNumber: number) => void; onDeleteClick?: (accountNumber: number) => void; onUnlockClick?: (accountNumber: number) => void; @@ -25,6 +26,7 @@ export const AccountItem = ({ label, unlockLabel, isUnlocked, + isDeletable, onEditClick, onDeleteClick, onUnlockClick, @@ -74,6 +76,7 @@ export const AccountItem = ({ icon={} size="extraSmall" data-testid="wallet-account-item-delete-btn" + disabled={!isDeletable} onClick={(): void => { onDeleteClick?.(accountNumber); }} diff --git a/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.stories.tsx b/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.stories.tsx index cc6bca874..950ef918f 100644 --- a/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.stories.tsx +++ b/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.stories.tsx @@ -36,6 +36,7 @@ export const Overview = (): JSX.Element => ( @@ -46,6 +47,7 @@ export const Overview = (): JSX.Element => ( diff --git a/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-accounts-list.component.tsx b/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-accounts-list.component.tsx index d4b102e80..0ab61655f 100644 --- a/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-accounts-list.component.tsx +++ b/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-accounts-list.component.tsx @@ -8,6 +8,7 @@ export interface AccountData { label: string; accountNumber: number; isUnlocked: boolean; + isActive: boolean; } export interface Props { @@ -24,19 +25,24 @@ export const AccountsList = ({ onAccountUnlockClick, onAccountEditClick, onAccountDeleteClick, -}: Readonly): JSX.Element => ( - - {accounts.map(a => ( - - ))} - -); +}: Readonly): JSX.Element => { + const hasMultipleUnlockedAccounts = + accounts.filter(a => a.isUnlocked).length > 1; + return ( + + {accounts.map(a => ( + + ))} + + ); +}; diff --git a/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-accounts-list.stories.tsx b/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-accounts-list.stories.tsx index 9b23bad1e..17e51d6e3 100644 --- a/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-accounts-list.stories.tsx +++ b/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-accounts-list.stories.tsx @@ -35,26 +35,31 @@ export const Overview = (): JSX.Element => ( accountNumber: 1, label: 'Account #1', isUnlocked: true, + isActive: false, }, { accountNumber: 2, label: 'Account #2', isUnlocked: true, + isActive: true, }, { accountNumber: 3, label: 'Account #3', isUnlocked: false, + isActive: false, }, { accountNumber: 4, label: 'Account with a long name', isUnlocked: false, + isActive: false, }, { accountNumber: 10, label: 'Account #10', isUnlocked: false, + isActive: false, }, ]} /> From 9688246dd0f5805847ad6cb7dd7fdfa24f15f191 Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Thu, 1 Feb 2024 10:34:59 +0200 Subject: [PATCH 03/27] feat(ui): add onActivateClick handler for profile dropdown --- ...rofile-dropdown-account-item.component.tsx | 56 +++++++++++-------- ...ofile-dropdown-accounts-list.component.tsx | 7 +++ 2 files changed, 40 insertions(+), 23 deletions(-) diff --git a/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.component.tsx b/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.component.tsx index 0739ce6d8..7fb115285 100644 --- a/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.component.tsx +++ b/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.component.tsx @@ -16,6 +16,7 @@ export interface Props { unlockLabel: string; isUnlocked: boolean; isDeletable: boolean; + onActivateClick?: (accountNumber: number) => void; onEditClick?: (accountNumber: number) => void; onDeleteClick?: (accountNumber: number) => void; onUnlockClick?: (accountNumber: number) => void; @@ -27,6 +28,7 @@ export const AccountItem = ({ unlockLabel, isUnlocked, isDeletable, + onActivateClick, onEditClick, onDeleteClick, onUnlockClick, @@ -38,30 +40,38 @@ export const AccountItem = ({ className={cx.root} data-testid="wallet-account-item" > - - - - - {label} - - - m/1852'/1815'/{accountNumber}' - +
{ + onActivateClick?.(accountNumber); + }} + > + + + + + {label} + + + m/1852'/1815'/{accountNumber}' + + - +
{isUnlocked ? ( void; onAccountUnlockClick?: (accountNumber: number) => void; onAccountEditClick?: (accountNumber: number) => void; onAccountDeleteClick?: (accountNumber: number) => void; @@ -22,6 +23,7 @@ export interface Props { export const AccountsList = ({ accounts, unlockLabel, + onAccountActivateClick, onAccountUnlockClick, onAccountEditClick, onAccountDeleteClick, @@ -38,6 +40,11 @@ export const AccountsList = ({ label={a.label} isDeletable={!a.isActive && hasMultipleUnlockedAccounts} unlockLabel={unlockLabel} + onActivateClick={(accountNumber: number): void => { + if (!a.isActive && a.isUnlocked) { + onAccountActivateClick?.(accountNumber); + } + }} onUnlockClick={onAccountUnlockClick} onEditClick={onAccountEditClick} onDeleteClick={onAccountDeleteClick} From 64e7742d636550645325dcc472473cde10d2ba45 Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Wed, 14 Feb 2024 09:52:43 +0200 Subject: [PATCH 04/27] fix(ui): do not fire onClick in profile dropdown when clicking on arrow --- .../profile-dropdown-wallet-option.component.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/design-system/profile-dropdown/profile-dropdown-wallet-option.component.tsx b/packages/ui/src/design-system/profile-dropdown/profile-dropdown-wallet-option.component.tsx index b971fb53d..b258cff45 100644 --- a/packages/ui/src/design-system/profile-dropdown/profile-dropdown-wallet-option.component.tsx +++ b/packages/ui/src/design-system/profile-dropdown/profile-dropdown-wallet-option.component.tsx @@ -73,7 +73,10 @@ export const WalletOption = ({ justifyContent="center" > { + onOpenAccountsMenu?.(); + event.stopPropagation(); + }} icon={} size="extraSmall" as="div" From 44ca3adcbc7d32776da585cc781ef52fa24aaa5c Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Wed, 14 Feb 2024 11:44:12 +0200 Subject: [PATCH 05/27] fix(core): start from account #0 in select account step --- .../ui/components/WalletSetup/WalletSetupSelectAccountsStep.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/ui/components/WalletSetup/WalletSetupSelectAccountsStep.tsx b/packages/core/src/ui/components/WalletSetup/WalletSetupSelectAccountsStep.tsx index a4437e8bb..db56cc87c 100644 --- a/packages/core/src/ui/components/WalletSetup/WalletSetupSelectAccountsStep.tsx +++ b/packages/core/src/ui/components/WalletSetup/WalletSetupSelectAccountsStep.tsx @@ -51,7 +51,7 @@ export const WalletSetupSelectAccountsStep = ({ >

- {t('core.walletSetupSelectAccountsStep.account')} {index + 1} {index === 0 && '- Default'} + {t('core.walletSetupSelectAccountsStep.account')} {index} {index === 0 && '- Default'}

))} From 04816edf0b60a22f20dbb977e06c90f60cc05551 Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Wed, 14 Feb 2024 16:57:55 +0200 Subject: [PATCH 06/27] fix(extension): clear background page once finished with create wallet flow if create wallet flow finishes without explicitly calling setBackgroundPage() then it would break the routing, as background page would be defined forever --- .../src/views/browser-view/routes/index.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/browser-extension-wallet/src/views/browser-view/routes/index.tsx b/apps/browser-extension-wallet/src/views/browser-view/routes/index.tsx index 56eb96a62..b02751f2f 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/routes/index.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/routes/index.tsx @@ -106,11 +106,15 @@ export const BrowserViewRoutes = ({ routesMap = defaultRoutes }: { routesMap?: R const location = useLocation<{ background?: Location }>(); useEffect(() => { - if ( - page === undefined && - (location.pathname === routes.newWallet.root || location.pathname === routes.sharedWallet.root) - ) { - setBackgroundPage({ pathname: '/assets', search: '', hash: '', state: undefined }); + const isCreatingWallet = [routes.newWallet.root, routes.sharedWallet.root].some((path) => + location.pathname.startsWith(path) + ); + if (page === undefined) { + if (isCreatingWallet) { + setBackgroundPage({ pathname: '/assets', search: '', hash: '', state: undefined }); + } + } else if (!isCreatingWallet) { + setBackgroundPage(); } }, [location, page, setBackgroundPage]); From b08509c48039b3929e462ab9add57b2fef0bb32f Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Mon, 29 Jan 2024 15:09:41 +0200 Subject: [PATCH 07/27] feat(extension): wire up multi-account components temporarily use native prompt dialog for password input; does not have a prompt to connect hardware wallet, or any error handling when deriving xpub --- .../components/DropdownMenu/DropdownMenu.tsx | 13 +- .../DropdownMenuOverlay.tsx | 7 +- .../components/UserInfo.tsx | 111 +++++++++--- .../components/WalletAccounts.tsx | 167 +++++++++--------- .../hooks/__tests__/useWalletManager.test.tsx | 136 +++++++++++++- .../src/hooks/useWalletManager.ts | 154 ++++++++++++++-- .../src/lib/scripts/background/wallet.ts | 11 +- .../src/lib/wallet-api-ui.ts | 4 +- .../src/stores/slices/wallet-info-slice.ts | 2 + .../src/stores/types.ts | 4 +- .../src/utils/get-wallet-subtitle.ts | 6 + .../src/utils/mocks/store.tsx | 2 + .../cardano/src/wallet/lib/cardano-wallet.ts | 1 + 13 files changed, 476 insertions(+), 142 deletions(-) create mode 100644 apps/browser-extension-wallet/src/utils/get-wallet-subtitle.ts diff --git a/apps/browser-extension-wallet/src/components/DropdownMenu/DropdownMenu.tsx b/apps/browser-extension-wallet/src/components/DropdownMenu/DropdownMenu.tsx index fd63e0e86..2bfe5a98f 100644 --- a/apps/browser-extension-wallet/src/components/DropdownMenu/DropdownMenu.tsx +++ b/apps/browser-extension-wallet/src/components/DropdownMenu/DropdownMenu.tsx @@ -14,6 +14,7 @@ import { PostHogAction } from '@providers/AnalyticsProvider/analyticsTracker'; import { ProfileDropdown } from '@lace/ui'; import { useGetHandles } from '@hooks'; import { getAssetImageUrl } from '@src/utils/get-asset-image-url'; +import { getActiveWalletSubtitle } from '@src/utils/get-wallet-subtitle'; export interface DropdownMenuProps { isPopup?: boolean; @@ -21,7 +22,7 @@ export interface DropdownMenuProps { export const DropdownMenu = ({ isPopup }: DropdownMenuProps): React.ReactElement => { const analytics = useAnalyticsContext(); - const { walletInfo } = useWalletStore(); + const { cardanoWallet } = useWalletStore(); const [open, setOpen] = useState(false); const [handle] = useGetHandles(); const handleImage = handle?.profilePic; @@ -38,6 +39,8 @@ export const DropdownMenu = ({ isPopup }: DropdownMenuProps): React.ReactElement } }; + const walletName = cardanoWallet.source.wallet.metadata.name; + return ( - + = ({ ...props }): React.ReactElement => { const [currentSection, setCurrentSection] = useState(Sections.Main); - const { environmentName } = useWalletStore(); + const { environmentName, setManageAccountsWallet } = useWalletStore(); - const openWalletAccounts = () => { + const openWalletAccounts = (wallet: AnyBip32Wallet) => { + setManageAccountsWallet(wallet); setCurrentSection(Sections.WalletAccounts); }; diff --git a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx index 920b56261..d9b817238 100644 --- a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx +++ b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx @@ -1,18 +1,20 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import classnames from 'classnames'; import { useWalletStore } from '@src/stores'; import { Menu, Tooltip as AntdTooltip } from 'antd'; import { useTranslation } from 'react-i18next'; import styles from '../DropdownMenuOverlay.module.scss'; import { CopyToClipboard } from 'react-copy-to-clipboard'; -import { toast, addEllipsis } from '@lace/common'; +import { toast, addEllipsis, useObservable } from '@lace/common'; import { WalletStatusContainer } from '@components/WalletStatus'; import { UserAvatar } from './UserAvatar'; -import { useGetHandles } from '@hooks'; +import { useGetHandles, useWalletManager } from '@hooks'; import { useAnalyticsContext } from '@providers'; import { PostHogAction } from '@providers/AnalyticsProvider/analyticsTracker'; import { ProfileDropdown } from '@lace/ui'; -import { getAssetImageUrl } from '@src/utils/get-asset-image-url'; +import { AnyBip32Wallet, AnyWallet, Bip32WalletAccount, WalletType } from '@cardano-sdk/web-extension'; +import { Wallet } from '@lace/cardano'; +import { Separator } from './Separator'; const ADRESS_FIRST_PART_LENGTH = 10; const ADRESS_LAST_PART_LENGTH = 5; @@ -27,25 +29,94 @@ const overlayInnerStyle = { interface UserInfoProps { avatarVisible?: boolean; - onOpenWalletAccounts?: (walletAddress: string) => void; + onOpenWalletAccounts?: (wallet: AnyBip32Wallet) => void; } +const NO_WALLETS: AnyWallet[] = []; + export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInfoProps): React.ReactElement => { const { t } = useTranslation(); - const { walletInfo } = useWalletStore(); + const { walletInfo, cardanoWallet } = useWalletStore(); + const { activateWallet, walletRepository } = useWalletManager(); const analytics = useAnalyticsContext(); + const wallets = useObservable(walletRepository.wallets$, NO_WALLETS); const walletAddress = walletInfo.addresses[0].address.toString(); const shortenedWalletAddress = addEllipsis(walletAddress, ADRESS_FIRST_PART_LENGTH, ADRESS_LAST_PART_LENGTH); - const walletName = addEllipsis(walletInfo.name.toString(), WALLET_NAME_MAX_LENGTH, 0); + const fullWalletName = cardanoWallet.source.wallet.metadata.name; + const walletName = addEllipsis(fullWalletName, WALLET_NAME_MAX_LENGTH, 0); const [handle] = useGetHandles(); const handleName = handle?.nftMetadata?.name; - const handleImage = handle?.profilePic; const handleOnAddressCopy = () => { toast.notify({ duration: TOAST_DEFAULT_DURATION, text: t('general.clipboard.copiedToClipboard') }); analytics.sendEventToPostHog(PostHogAction.UserWalletProfileWalletAddressClick); }; + const getLastActiveAccount = useCallback( + ( + wallet: AnyBip32Wallet + ): Bip32WalletAccount => { + if (wallet.accounts.length === 1) return wallet.accounts[0]; + if (wallet.walletId === cardanoWallet?.source.wallet.walletId) { + const currentlyActiveAccount = wallet.accounts.find( + ({ accountIndex }) => accountIndex === cardanoWallet.source.account?.accountIndex + ); + if (currentlyActiveAccount) return currentlyActiveAccount; + } + if (typeof wallet.metadata.lastActiveAccountIndex !== 'undefined') { + const lastActiveAccount = wallet.accounts.find( + ({ accountIndex }) => accountIndex === wallet.metadata.lastActiveAccountIndex + ); + if (lastActiveAccount) return lastActiveAccount; + } + // If last active account is deleted, fall back to any (1st) account + return wallet.accounts[0]; + }, + [cardanoWallet] + ); + + const renderBip32Wallet = useCallback( + (wallet: AnyBip32Wallet) => { + const lastActiveAccount = getLastActiveAccount(wallet); + return ( + onOpenWalletAccounts(wallet)} + onClick={() => { + activateWallet({ + walletId: wallet.walletId, + accountIndex: lastActiveAccount.accountIndex + }); + }} + type="cold" + /> + ); + }, + [activateWallet, getLastActiveAccount, onOpenWalletAccounts, walletName] + ); + + const renderWallet = useCallback( + (wallet: AnyWallet, isLast: boolean) => ( +
+ {wallet.type !== WalletType.Script + ? renderBip32Wallet(wallet) + : (() => { + throw new Error('Script wallets are not implemented'); + })()} + {wallet.walletId === cardanoWallet?.source.wallet.walletId ? ( +
+ +
+ ) : undefined} + {isLast ? undefined : } +
+ ), + [renderBip32Wallet, cardanoWallet?.source.wallet.walletId] + ); + return (
{process.env.USE_MULTI_WALLET === 'true' ? ( - onOpenWalletAccounts(walletAddress)} - type={process.env.USE_SHARED_WALLET === 'true' ? 'shared' : 'cold'} - /> +
{wallets.map((wallet, i) => renderWallet(wallet, i === wallets.length - 1))}
) : ( )}
-
- -
+ {process.env.USE_MULTI_WALLET === 'true' ? undefined : ( +
+ +
+ )}
); diff --git a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx index 991bc0396..dee846382 100644 --- a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx +++ b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx @@ -1,70 +1,97 @@ /* eslint-disable react/jsx-handler-names */ -import React, { useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { NavigationButton } 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 { useWalletStore } from '@src/stores'; +import { useWalletManager } from '@hooks'; -const exampleAccountData = [ - { - accountNumber: 1, - label: 'Account #1', - isUnlocked: true - }, - { - accountNumber: 2, - label: 'Account #2', - isUnlocked: true - }, - { - accountNumber: 3, - label: 'Account #3', - isUnlocked: true - }, - { - accountNumber: 4, - label: 'Account #4', - isUnlocked: true - }, - { - accountNumber: 5, - label: 'Account #5', - isUnlocked: true - }, - { - accountNumber: 6, - label: 'Account #6', - isUnlocked: false - }, - { - accountNumber: 7, - label: 'Account #7', - isUnlocked: false - }, - { - accountNumber: 8, - label: 'Account #8', - isUnlocked: false - }, - { - accountNumber: 9, - label: 'Account #9', - isUnlocked: false - }, - { - accountNumber: 10, - label: 'Account #10', - isUnlocked: false - } -]; +const defaultAccountName = (accountNumber: number) => `Account #${accountNumber}`; + +const NUMBER_OF_ACCOUNTS_PER_WALLET = 24; export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack: () => void }): React.ReactElement => { const { t } = useTranslation(); const editAccountDrawer = useAccountDataModal(); const disableAccountConfirmation = useAccountDataModal(); - const [mockAccountData, setMockAccountData] = useState(exampleAccountData); + const { manageAccountsWallet: wallet, cardanoWallet } = useWalletStore(); + const { + source: { + wallet: { walletId: activeWalletId }, + account: activeAccount + } + } = cardanoWallet; + const { walletRepository, addAccount, activateWallet } = useWalletManager(); + const accountsData = useMemo( + () => + Array.from({ length: NUMBER_OF_ACCOUNTS_PER_WALLET }).map((_, accountNumber): AccountData => { + const account = wallet.accounts.find(({ accountIndex }) => accountIndex === accountNumber); + return { + isUnlocked: !!account, + label: account ? account.metadata.name : defaultAccountName(accountNumber), + accountNumber, + isActive: activeWalletId === wallet.walletId && activeAccount?.accountIndex === accountNumber + }; + }), + [wallet, activeAccount?.accountIndex, activeWalletId] + ); + + const activateAccount = useCallback( + async (accountIndex: number) => { + await activateWallet({ + walletId: wallet.walletId, + accountIndex + }); + }, + [wallet.walletId, activateWallet] + ); + + const editAccount = useCallback( + (accountIndex: number) => editAccountDrawer.open(accountsData.find((a) => a.accountNumber === accountIndex)), + [editAccountDrawer, accountsData] + ); + + const deleteAccount = useCallback( + async (accountIndex: number) => { + disableAccountConfirmation.open(accountsData.find((a) => a.accountNumber === accountIndex)); + }, + [disableAccountConfirmation, accountsData] + ); + + const unlockAccount = useCallback( + async (accountIndex: number) => { + await addAccount({ + walletId: wallet.walletId, + accountIndex, + metadata: { name: defaultAccountName(accountIndex) } + }); + }, + [wallet.walletId, addAccount] + ); + + const lockAccount = useCallback(async () => { + await walletRepository.removeAccount({ + walletId: wallet.walletId, + accountIndex: disableAccountConfirmation.accountData.accountNumber + }); + + disableAccountConfirmation.hide(); + }, [walletRepository, disableAccountConfirmation, wallet.walletId]); + + const renameAccount = useCallback( + async (newAccountName: string) => { + await walletRepository.updateAccountMetadata({ + walletId: wallet.walletId, + accountIndex: editAccountDrawer.accountData.accountNumber, + metadata: { name: newAccountName } + }); + editAccountDrawer.hide(); + }, + [walletRepository, wallet.walletId, editAccountDrawer] + ); return ( <> @@ -86,26 +113,16 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack: > - editAccountDrawer.open(mockAccountData.find((a) => a.accountNumber === accountNumber)) - } - onAccountDeleteClick={(accountNumber) => { - disableAccountConfirmation.open(mockAccountData.find((a) => a.accountNumber === accountNumber)); - }} - accounts={mockAccountData} + onAccountActivateClick={activateAccount} + onAccountEditClick={editAccount} + onAccountDeleteClick={deleteAccount} + onAccountUnlockClick={unlockAccount} + accounts={accountsData} /> { - const newAccountData = [...mockAccountData]; - const modifiedAccount = newAccountData.find( - (a) => a.accountNumber === editAccountDrawer.accountData?.accountNumber - ); - modifiedAccount.label = newAccountName; - setMockAccountData(newAccountData); - editAccountDrawer.hide(); - }} + onSave={renameAccount} visible={editAccountDrawer.isOpen} hide={editAccountDrawer.hide} name={editAccountDrawer.accountData?.label} @@ -122,15 +139,7 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack: zIndex={10_000} open={disableAccountConfirmation.isOpen} onCancel={disableAccountConfirmation.hide} - onConfirm={() => { - const newAccountData = [...mockAccountData]; - const modifiedAccount = newAccountData.find( - (a) => a.accountNumber === disableAccountConfirmation.accountData?.accountNumber - ); - modifiedAccount.isUnlocked = false; - setMockAccountData(newAccountData); - disableAccountConfirmation.hide(); - }} + onConfirm={lockAccount} translations={{ title: t('account.disable.title'), description: t('account.disable.description'), diff --git a/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx b/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx index 6b7830d88..c0d43f7f6 100644 --- a/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx +++ b/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx @@ -15,6 +15,8 @@ const mockEmip3encrypt = jest.fn(); const mockConnectDevice = jest.fn(); const mockRestoreWalletFromKeyAgent = jest.fn(); const mockSwitchKeyAgents = jest.fn(); +const mockLedgerGetXpub = jest.fn(); +const mockTrezorGetXpub = jest.fn(); const mockLedgerCreateWithDevice = jest.fn(); const mockUseAppSettingsContext = jest.fn().mockReturnValue([{}, jest.fn()]); import React from 'react'; @@ -32,7 +34,7 @@ import * as localStorage from '@src/utils/local-storage'; import * as AppSettings from '@providers/AppSettings'; import * as walletApiUi from '@src/lib/wallet-api-ui'; import { of } from 'rxjs'; -import { AnyWallet, WalletType } from '@cardano-sdk/web-extension'; +import { AnyBip32Wallet, AnyWallet, Bip32Wallet, WalletType } from '@cardano-sdk/web-extension'; import { Wallet } from '@lace/cardano'; jest.mock('@providers/AppSettings', () => ({ @@ -60,7 +62,13 @@ jest.mock('@lace/cardano', () => { ...actual.Wallet, Ledger: { LedgerKeyAgent: { - createWithDevice: mockLedgerCreateWithDevice + createWithDevice: mockLedgerCreateWithDevice, + getXpub: mockLedgerGetXpub + } + }, + Trezor: { + TrezorKeyAgent: { + getXpub: mockTrezorGetXpub } }, restoreWalletFromKeyAgent: mockRestoreWalletFromKeyAgent, @@ -96,6 +104,11 @@ const getWrapper = ); +const render = () => + renderHook(() => useWalletManager(), { + wrapper: getWrapper({}) + }).result.current; + describe('Testing useWalletManager hook', () => { beforeEach(() => { jest.resetAllMocks(); @@ -484,6 +497,7 @@ describe('Testing useWalletManager hook', () => { (walletApiUi.walletManager as any).deactivate = jest.fn().mockResolvedValue(undefined); (walletApiUi.walletManager as any).destroyData = jest.fn().mockResolvedValue(undefined); (walletApiUi.walletRepository as any).removeWallet = jest.fn().mockResolvedValue(undefined); + (walletApiUi.walletRepository as any).wallets$ = of([]); jest.spyOn(stores, 'useWalletStore').mockImplementation(() => ({ updateAppSettings: jest.fn(), settings: {}, @@ -632,4 +646,122 @@ describe('Testing useWalletManager hook', () => { expect(setAddressesDiscoveryCompleted).toBeCalledWith(false); }); }); + + describe('addAccount', () => { + it('throws an error when wallet with specified id does not exist', async () => { + (walletApiUi.walletRepository as any).wallets$ = of([]); + const { addAccount } = render(); + await expect( + addAccount({ walletId: 'walletid', accountIndex: 1, metadata: { name: 'new account' } }) + ).rejects.toThrowError('Wallet not found: walletid'); + }); + + it('throws an error when wallet with specified id is not a bip32 wallet', async () => { + const walletId = 'script-wallet-id'; + (walletApiUi.walletRepository as any).wallets$ = of([ + { + walletId, + type: WalletType.Script + } + ]); + const { addAccount } = render(); + await expect(addAccount({ walletId, accountIndex: 1, metadata: { name: 'new account' } })).rejects.toThrowError( + 'Cannot add account to a script wallet' + ); + }); + + describe('for existing bip32 wallet', () => { + const extendedAccountPublicKey = + '12b608b67a743891656d6463f72aa6e5f0e62ba6dc47e32edfebafab1acf0fa9f3033c2daefa3cb2ac16916b08c7e7424d4e1aafae2206d23c4d002299c07128'; + const walletTypes = [ + { + type: WalletType.InMemory, + walletProps: { + encryptedSecrets: { + rootPrivateKeyBytes: + '8403cf9d8267a7169381dd476f4fda48e1926fec8942ec51892e428e152fbed4835711cccb7efcae379627f477abb46c883f6b0c221f3aea40f9d931d2e8fdc69f85f16eb91ca380fc2e1edc2543e4dd71c1866208ea6c6960bca99f974e25776067e9a242b0e4066b96bd4d89ca99db5bd77bb65573b9cbeef85222ceed6d5a4dc516213ace986f03b183365505119b9a0abdc4375bfdf2363d7433' + } + }, + prepare: () => { + global.prompt = jest.fn(() => 'passphrase1'); + mockEmip3decrypt.mockImplementationOnce( + jest.requireActual('@lace/cardano').Wallet.KeyManagement.emip3decrypt + ); + } + }, + { + type: WalletType.Trezor, + prepare: () => mockTrezorGetXpub.mockResolvedValueOnce(extendedAccountPublicKey) + }, + { + type: WalletType.Ledger, + prepare: () => mockLedgerGetXpub.mockResolvedValueOnce(extendedAccountPublicKey) + } + ]; + + beforeEach(() => { + walletApiUi.walletRepository.addAccount = jest.fn().mockResolvedValueOnce(void 0); + }); + + it.each(walletTypes)( + 'derives extended account public key for $type wallet and adds new account into the repository', + async ({ type, walletProps, prepare }) => { + prepare(); + const walletId = 'bip32-wallet-id'; + (walletApiUi.walletRepository as any).wallets$ = of([ + { + walletId, + type, + accounts: [], + ...walletProps + } as unknown as Bip32Wallet + ]); + const addAccountProps = { walletId, accountIndex: 0, metadata: { name: 'new account' } }; + + const { addAccount } = render(); + await addAccount(addAccountProps); + expect(walletApiUi.walletRepository.addAccount).toBeCalledWith({ + ...addAccountProps, + extendedAccountPublicKey + }); + } + ); + }); + }); + + describe('activateWallet', () => { + it('stores lastActiveAccountIndex in wallet metadata and activates wallet via WalletManager', async () => { + const walletId = 'walletId'; + const accountIndex = 1; + const originalMetadata = { name: 'wallet' }; + walletApiUi.walletRepository.wallets$ = of([ + { + walletId, + metadata: originalMetadata + } as AnyBip32Wallet + ]); + walletApiUi.walletRepository.updateWalletMetadata = jest.fn().mockResolvedValueOnce(void 0); + walletApiUi.walletManager.activate = jest.fn().mockResolvedValueOnce(void 0); + + const { activateWallet } = render(); + await activateWallet({ walletId, accountIndex }); + + expect(walletApiUi.walletRepository.updateWalletMetadata).toBeCalledWith({ + walletId, + metadata: { + ...originalMetadata, + lastActiveAccountIndex: accountIndex + } + }); + + expect(walletApiUi.walletManager.activate).toBeCalledWith({ + walletId, + accountIndex, + chainId: expect.objectContaining({ + networkMagic: expect.anything(), + networkId: expect.anything() + }) + }); + }); + }); }); diff --git a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts index c71d60162..383bc5516 100644 --- a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts +++ b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts @@ -21,7 +21,9 @@ import { ENHANCED_ANALYTICS_OPT_IN_STATUS_LS_KEY } from '@providers/AnalyticsPro import { ILocalStorage } from '@src/types'; import { firstValueFrom } from 'rxjs'; import { + AddAccountProps, AddWalletProps, + AnyBip32Wallet, AnyWallet, WalletManagerActivateProps, WalletManagerApi, @@ -57,6 +59,9 @@ export interface CreateHardwareWallet { connectedDevice: Wallet.HardwareWallets; } +type WalletManagerAddAccountProps = Omit, 'extendedAccountPublicKey'>; +type ActivateWalletProps = Omit; + export interface UseWalletManager { walletManager: WalletManagerApi; walletRepository: WalletRepositoryApi; @@ -67,14 +72,72 @@ export interface UseWalletManager { activeWalletProps: WalletManagerActivateProps | null ) => Promise; createWallet: (args: CreateWallet) => Promise; + activateWallet: (args: Omit) => Promise; createHardwareWallet: (args: CreateHardwareWallet) => Promise; connectHardwareWallet: (model: Wallet.HardwareWallets) => Promise; saveHardwareWallet: (wallet: Wallet.CardanoWallet, chainName?: Wallet.ChainName) => Promise; deleteWallet: (isForgotPasswordFlow?: boolean) => Promise; switchNetwork: (chainName: Wallet.ChainName) => Promise; + addAccount: (props: WalletManagerAddAccountProps) => Promise; } -const provider = { type: Wallet.WalletManagerProviderTypes.CARDANO_SERVICES_PROVIDER, options: {} }; +const clearBytes = (bytes: Uint8Array) => { + for (let i = 0; i < bytes.length; i++) { + bytes[i] = 0; + } +}; + +const getHwExtendedAccountPublicKey = async ( + walletType: Wallet.HardwareWallets, + accountIndex: number, + deviceConnection?: Wallet.DeviceConnection +) => { + switch (walletType) { + case WalletType.Ledger: + return Wallet.Ledger.LedgerKeyAgent.getXpub({ + communicationType: Wallet.KeyManagement.CommunicationType.Web, + deviceConnection: typeof deviceConnection !== 'boolean' ? deviceConnection : undefined, + accountIndex + }); + case WalletType.Trezor: + return Wallet.Trezor.TrezorKeyAgent.getXpub({ + communicationType: Wallet.KeyManagement.CommunicationType.Web, + accountIndex + }); + } +}; + +const getExtendedAccountPublicKey = async ( + wallet: AnyBip32Wallet, + accountIndex: number +) => { + // 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 + ); + const rootPrivateKeyBuffer = Buffer.from(rootPrivateKeyBytes); + const accountPrivateKey = await Wallet.KeyManagement.util.deriveAccountPrivateKey({ + bip32Ed25519: Wallet.bip32Ed25519, + accountIndex, + rootPrivateKey: Wallet.Crypto.Bip32PrivateKeyHex(rootPrivateKeyBuffer.toString('hex')) + }); + const accountPublicKey = await Wallet.bip32Ed25519.getBip32PublicKey(accountPrivateKey); + clearBytes(passphrase); + clearBytes(rootPrivateKeyBytes); + clearBytes(rootPrivateKeyBuffer); + return accountPublicKey; + } + case WalletType.Ledger: + case WalletType.Trezor: + return getHwExtendedAccountPublicKey(wallet.type, accountIndex); + } +}; + const chainIdFromName = (chainName: Wallet.ChainName) => { const chainId = Wallet.Cardano.ChainIds[chainName]; if (!chainId || !AVAILABLE_CHAINS.includes(chainName)) throw new Error('Chain not supported'); @@ -86,8 +149,7 @@ const defaultAccountName = (accountIndex: number) => `Account #${accountIndex}`; const keyAgentDataToAddWalletProps = async ( data: Wallet.KeyManagement.SerializableKeyAgentData, backgroundService: BackgroundService, - name: string, - lockValue: Wallet.HexBlob | undefined + metadata: Wallet.WalletMetadata ): Promise> => { const accounts = [ { @@ -102,7 +164,7 @@ const keyAgentDataToAddWalletProps = async ( if (!mnemonic) throw new Error('Inconsistent state: mnemonic not found for in-memory wallet'); return { type: WalletType.InMemory, - metadata: { name, lockValue }, + metadata, encryptedSecrets: { rootPrivateKeyBytes: HexBlob.fromBytes(Buffer.from(data.encryptedRootPrivateKeyBytes)), keyMaterial: HexBlob.fromBytes(Buffer.from(JSON.parse(mnemonic).data)) @@ -113,13 +175,13 @@ const keyAgentDataToAddWalletProps = async ( case Wallet.KeyManagement.KeyAgentType.Ledger: return { type: WalletType.Ledger, - metadata: { name }, + metadata, accounts }; case Wallet.KeyManagement.KeyAgentType.Trezor: return { type: WalletType.Trezor, - metadata: { name }, + metadata, accounts }; default: @@ -170,7 +232,7 @@ const createHardwareWallet = async ({ ); const addWalletProps: AddWalletProps = { - metadata: { name }, + metadata: { name, lastActiveAccountIndex: accountIndex }, type: connectedDevice, accounts: [ { @@ -184,8 +246,7 @@ const createHardwareWallet = async ({ await walletManager.activate({ walletId, chainId: DEFAULT_CHAIN_ID, - accountIndex, - provider + accountIndex }); return { @@ -216,7 +277,9 @@ export const useWalletManager = (): UseWalletManager => { currentChain, setCurrentChain, setCardanoCoin, - setAddressesDiscoveryCompleted + setAddressesDiscoveryCompleted, + manageAccountsWallet, + setManageAccountsWallet } = useWalletStore(); const [settings, updateAppSettings] = useAppSettingsContext(); const { @@ -267,15 +330,18 @@ export const useWalletManager = (): UseWalletManager => { // deleteFromLocalStorage('wallet'); const walletId = await walletRepository.addWallet( - await keyAgentDataToAddWalletProps(keyAgentData, backgroundService, walletName, lockValue) + await keyAgentDataToAddWalletProps(keyAgentData, backgroundService, { + name: walletName, + lockValue, + lastActiveAccountIndex: keyAgentData.accountIndex + }) ); await walletManager.activate({ // when wallet is locked before migration, keyAgentData.chainId is the default one instead of last active one chainId: getCurrentChainId(), walletId, - accountIndex: keyAgentData.accountIndex, - provider + accountIndex: keyAgentData.accountIndex }); return firstValueFrom(walletRepository.wallets$); @@ -338,9 +404,30 @@ export const useWalletManager = (): UseWalletManager => { ) { setCardanoWallet(newCardanoWallet); } + + // Synchronize the currently managed wallet UI state with service worker + if (manageAccountsWallet) { + const managedWallet = wallets.find( + (w): w is AnyBip32Wallet => + w.walletId === manageAccountsWallet.walletId && w.type !== WalletType.Script + ); + if (!deepEquals(managedWallet, manageAccountsWallet)) { + setManageAccountsWallet(managedWallet); + } + } + return newCardanoWallet; }, - [setCardanoWallet, setCurrentChain, tryMigrateToWalletRepository, cardanoWallet, currentChain, setCardanoCoin] + [ + setCardanoWallet, + setCurrentChain, + tryMigrateToWalletRepository, + cardanoWallet, + currentChain, + manageAccountsWallet, + setCardanoCoin, + setManageAccountsWallet + ] ); /** @@ -382,7 +469,7 @@ export const useWalletManager = (): UseWalletManager => { const lockValue = HexBlob.fromBytes(await Wallet.KeyManagement.emip3encrypt(LOCK_VALUE, passphrase)); const addWalletProps: AddWalletProps = { - metadata: { name, lockValue }, + metadata: { name, lockValue, lastActiveAccountIndex: accountIndex }, encryptedSecrets: { keyMaterial: await encryptMnemonic(mnemonic, passphrase), rootPrivateKeyBytes: HexBlob.fromBytes( @@ -406,8 +493,7 @@ export const useWalletManager = (): UseWalletManager => { await walletManager.activate({ walletId, chainId: getCurrentChainId(), - accountIndex, - provider + accountIndex }); // Needed for reset password flow @@ -434,6 +520,24 @@ export const useWalletManager = (): UseWalletManager => { [getCurrentChainId] ); + const activateWallet = useCallback( + async (props: ActivateWalletProps): Promise => { + const wallets = await firstValueFrom(walletRepository.wallets$); + await walletRepository.updateWalletMetadata({ + walletId: props.walletId, + metadata: { + ...wallets.find(({ walletId }) => walletId === props.walletId).metadata, + lastActiveAccountIndex: props.accountIndex + } + }); + await walletManager.activate({ + ...props, + chainId: getCurrentChainId() + }); + }, + [getCurrentChainId] + ); + /** * Deletes Wallet from memory, all info from browser storage and destroys all stores */ @@ -548,7 +652,23 @@ export const useWalletManager = (): UseWalletManager => { [walletLock, loadWallet] ); + const addAccount = async ({ walletId, accountIndex, metadata }: WalletManagerAddAccountProps): Promise => { + const wallets = await firstValueFrom(walletRepository.wallets$); + const wallet = wallets.find((w) => w.walletId === walletId); + if (!wallet) throw new Error(`Wallet not found: ${walletId}`); + if (wallet.type === WalletType.Script) throw new Error('Cannot add account to a script wallet'); + const extendedAccountPublicKey = await getExtendedAccountPublicKey(wallet, accountIndex); + await walletRepository.addAccount({ + accountIndex, + extendedAccountPublicKey, + metadata, + walletId + }); + }; + return { + activateWallet, + addAccount, lockWallet, unlockWallet, loadWallet, diff --git a/apps/browser-extension-wallet/src/lib/scripts/background/wallet.ts b/apps/browser-extension-wallet/src/lib/scripts/background/wallet.ts index 6617be828..4e74fa959 100644 --- a/apps/browser-extension-wallet/src/lib/scripts/background/wallet.ts +++ b/apps/browser-extension-wallet/src/lib/scripts/background/wallet.ts @@ -20,7 +20,6 @@ import { walletManagerProperties, walletRepositoryProperties } from '@cardano-sdk/web-extension'; -import { config } from '@src/config'; import { Wallet } from '@lace/cardano'; import { ADA_HANDLE_POLICY_ID, HANDLE_SERVER_URLS } from '@src/features/ada-handle/config'; import { Cardano, NotImplementedError } from '@cardano-sdk/core'; @@ -54,14 +53,8 @@ const chainIdToChainName = (chainId: Cardano.ChainId): Wallet.ChainName => { }; const walletFactory: WalletFactory = { - create: async ({ chainId, accountIndex, provider }, wallet, { stores, witnesser }) => { - const chainName: Wallet.ChainName = - // provider.options are useless now, because they are bound to wallet (not to wallet per network). - // When we need to support custom provider URLs, add argument to WalletManager.switchNetwork - // and store it in wallet manager's storage for loading custom URL providers - provider?.type === (Wallet.WalletManagerProviderTypes.CARDANO_SERVICES_PROVIDER as unknown as string) - ? chainIdToChainName(chainId) - : config().CHAIN; + create: async ({ chainId, accountIndex }, wallet, { stores, witnesser }) => { + const chainName: Wallet.ChainName = chainIdToChainName(chainId); const providers = getProviders(chainName); if (wallet.type === WalletType.Script || typeof accountIndex !== 'number') { throw new NotImplementedError('Script wallet support is not implemented'); diff --git a/apps/browser-extension-wallet/src/lib/wallet-api-ui.ts b/apps/browser-extension-wallet/src/lib/wallet-api-ui.ts index 114f03142..dd44c05b6 100644 --- a/apps/browser-extension-wallet/src/lib/wallet-api-ui.ts +++ b/apps/browser-extension-wallet/src/lib/wallet-api-ui.ts @@ -48,9 +48,11 @@ export const walletRepository = consumeRemoteApi( { hwOptions: { manifest: Wallet.manifest, communicationType: Wallet.KeyManagement.CommunicationType.Web } }, - { keyAgentFactory: createKeyAgentFactory({ bip32Ed25519: Wallet.bip32Ed25519, logger }) } + { keyAgentFactory } ); exposeSigningCoordinatorApi({ signingCoordinator }, { logger, runtime }); diff --git a/apps/browser-extension-wallet/src/stores/slices/wallet-info-slice.ts b/apps/browser-extension-wallet/src/stores/slices/wallet-info-slice.ts index ee3c8efad..dad9a491c 100644 --- a/apps/browser-extension-wallet/src/stores/slices/wallet-info-slice.ts +++ b/apps/browser-extension-wallet/src/stores/slices/wallet-info-slice.ts @@ -12,7 +12,9 @@ export const walletInfoSlice: SliceCreator ({ // Wallet info and storage setWalletInfo: (walletInfo) => set({ walletInfo }), + setManageAccountsWallet: (manageAccountsWallet) => set({ manageAccountsWallet }), // Loaded wallet + manageAccountsWallet: undefined, inMemoryWallet: undefined, cardanoWallet: undefined, walletState: undefined, diff --git a/apps/browser-extension-wallet/src/stores/types.ts b/apps/browser-extension-wallet/src/stores/types.ts index 173afa543..152c6b1f2 100644 --- a/apps/browser-extension-wallet/src/stores/types.ts +++ b/apps/browser-extension-wallet/src/stores/types.ts @@ -21,7 +21,7 @@ import { import { FetchWalletActivitiesProps, FetchWalletActivitiesReturn, IBlockchainProvider } from './slices'; import { IAssetDetails } from '@src/views/browser-view/features/assets/types'; import { TokenInfo } from '@src/utils/get-assets-information'; -import { WalletManagerApi, WalletType } from '@cardano-sdk/web-extension'; +import { AnyBip32Wallet, WalletManagerApi, WalletType } from '@cardano-sdk/web-extension'; import { AddressesDiscoveryStatus } from '@lib/communication/addresses-discoverer'; import { Reward } from '@cardano-sdk/core'; import { EpochNo } from '@cardano-sdk/core/dist/cjs/Cardano'; @@ -101,6 +101,8 @@ export interface StakePoolSearchSlice { export type EnvironmentTypes = Wallet.ChainName; export interface WalletInfoSlice { + manageAccountsWallet: AnyBip32Wallet | undefined; + setManageAccountsWallet: (wallet: AnyBip32Wallet) => void; walletInfo?: WalletInfo | undefined; setWalletInfo: (info?: WalletInfo) => void; inMemoryWallet: Wallet.ObservableWallet | undefined; diff --git a/apps/browser-extension-wallet/src/utils/get-wallet-subtitle.ts b/apps/browser-extension-wallet/src/utils/get-wallet-subtitle.ts new file mode 100644 index 000000000..2a624aedb --- /dev/null +++ b/apps/browser-extension-wallet/src/utils/get-wallet-subtitle.ts @@ -0,0 +1,6 @@ +import { Wallet } from '@lace/cardano'; +import { Bip32WalletAccount } from '@cardano-sdk/web-extension'; + +export const getActiveWalletSubtitle = (account?: Bip32WalletAccount): string => + // LW-9574 update to appropriate/non-hardcoded subtitle for shared wallets + account ? account.metadata.name : 'Shared Wallet'; diff --git a/apps/browser-extension-wallet/src/utils/mocks/store.tsx b/apps/browser-extension-wallet/src/utils/mocks/store.tsx index 519b36105..5ccd61e8a 100644 --- a/apps/browser-extension-wallet/src/utils/mocks/store.tsx +++ b/apps/browser-extension-wallet/src/utils/mocks/store.tsx @@ -31,6 +31,8 @@ export const walletStoreMock = async ( // TODO: If possible use real methods/states and mock only needed ones, like inMemoryWallet [LW-5454] return { + setManageAccountsWallet: jest.fn(), + manageAccountsWallet: undefined, walletState: undefined, setWalletState: jest.fn(), fetchNetworkInfo: jest.fn(), diff --git a/packages/cardano/src/wallet/lib/cardano-wallet.ts b/packages/cardano/src/wallet/lib/cardano-wallet.ts index 968d93b1c..f04e33829 100644 --- a/packages/cardano/src/wallet/lib/cardano-wallet.ts +++ b/packages/cardano/src/wallet/lib/cardano-wallet.ts @@ -26,6 +26,7 @@ export type KeyAgentsByChain = Record Date: Wed, 14 Feb 2024 14:13:32 +0200 Subject: [PATCH 08/27] feat(extension): wire up add wallet components --- .../hooks/__tests__/useWalletManager.test.tsx | 63 ++++--- .../src/hooks/useWalletManager.ts | 166 ++++++++++-------- .../features/multi-wallet/MultiWallet.tsx | 31 ++-- .../create-wallet/CreateWallet.test.tsx | 38 +++- .../create-wallet/steps/NewRecoveryPhrase.tsx | 19 +- .../hardware-wallet/steps/NameWallet.tsx | 23 ++- .../restore-wallet/RestoreWallet.test.tsx | 24 ++- .../steps/RestoreRecoveryPhrase.tsx | 33 +++- .../components/SettingsRemoveWallet.tsx | 4 +- .../components/HardwareWalletFlow.tsx | 8 +- 10 files changed, 262 insertions(+), 147 deletions(-) diff --git a/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx b/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx index c0d43f7f6..04f03723d 100644 --- a/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx +++ b/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx @@ -21,7 +21,7 @@ const mockLedgerCreateWithDevice = jest.fn(); const mockUseAppSettingsContext = jest.fn().mockReturnValue([{}, jest.fn()]); import React from 'react'; import { renderHook } from '@testing-library/react-hooks'; -import { LOCK_VALUE, useWalletManager } from '../useWalletManager'; +import { LOCK_VALUE, UseWalletManager, useWalletManager } from '../useWalletManager'; import { AppSettingsProvider, BackgroundServiceAPIProvider, @@ -376,19 +376,19 @@ describe('Testing useWalletManager hook', () => { describe('createHardwareWallet', () => { test('should use cardano manager to create wallet', async () => { const walletId = 'walletId'; - mockLedgerCreateWithDevice.mockResolvedValue({ - extendedAccountPublicKey: 'pubkey' - }); + mockLedgerGetXpub.mockResolvedValue('pubkey'); (walletApiUi.walletRepository as any).addWallet = jest.fn().mockResolvedValue(walletId); (walletApiUi.walletRepository as any).addAccount = jest.fn().mockResolvedValue(undefined); (walletApiUi.walletManager as any).activate = jest.fn().mockResolvedValue(undefined); const accountIndex = 1; const name = 'name'; - const chainId = { - networkId: 0, - networkMagic: 0 - }; + jest.spyOn(stores, 'useWalletStore').mockImplementation(() => ({ + currentChain: { + networkId: 0, + networkMagic: 0 + } + })); const connectedDevice = 'Ledger' as any; const deviceConnection = 'deviceConnection' as any; @@ -403,7 +403,6 @@ describe('Testing useWalletManager hook', () => { deviceConnection, accountIndex, name, - chainId, connectedDevice }); expect(walletApiUi.walletRepository.addWallet).toBeCalledTimes(1); @@ -491,13 +490,17 @@ describe('Testing useWalletManager hook', () => { describe('deleteWallet', () => { const walletId = 'walletId'; + let clearBackgroundStorage: jest.Mock; + let clearLocalStorage: jest.Mock; + let resetWalletLock: jest.Mock; + let setCardanoWallet: jest.Mock; + let deleteWallet: UseWalletManager['deleteWallet']; beforeEach(() => { (walletApiUi.walletManager as any).activeWalletId$ = of({ walletId }); (walletApiUi.walletManager as any).deactivate = jest.fn().mockResolvedValue(undefined); (walletApiUi.walletManager as any).destroyData = jest.fn().mockResolvedValue(undefined); (walletApiUi.walletRepository as any).removeWallet = jest.fn().mockResolvedValue(undefined); - (walletApiUi.walletRepository as any).wallets$ = of([]); jest.spyOn(stores, 'useWalletStore').mockImplementation(() => ({ updateAppSettings: jest.fn(), settings: {}, @@ -505,21 +508,17 @@ describe('Testing useWalletManager hook', () => { setCardanoCoin: jest.fn(), setAddressesDiscoveryCompleted: () => {} })); - }); - - test('should shutdown wallet, delete data from the LS, indexed DB and background storage, reset lock and current chain ', async () => { - const clearBackgroundStorage = jest.fn(); - const clearLocalStorage = jest.fn(); + clearBackgroundStorage = jest.fn(); + clearLocalStorage = jest.fn(); jest.spyOn(localStorage, 'clearLocalStorage').mockImplementation(clearLocalStorage); - const resetWalletLock = jest.fn(); - const setCardanoWallet = jest.fn(); + resetWalletLock = jest.fn(); + setCardanoWallet = jest.fn(); jest.spyOn(stores, 'useWalletStore').mockImplementation(() => ({ resetWalletLock, setCardanoWallet })); - - const { + ({ result: { current: { deleteWallet } } @@ -529,8 +528,12 @@ describe('Testing useWalletManager hook', () => { clearBackgroundStorage } as unknown as BackgroundServiceAPIProviderProps['value'] }) - }); - expect(deleteWallet).toBeDefined(); + })); + }); + + test('should shutdown wallet, delete data from the LS, indexed DB and background storage, reset lock and current chain ', async () => { + (walletApiUi.walletRepository as any).wallets$ = of([]); + await deleteWallet(); expect(walletApiUi.walletManager.deactivate).toBeCalledTimes(1); expect(walletApiUi.walletManager.destroyData).toBeCalled(); @@ -552,6 +555,24 @@ describe('Testing useWalletManager hook', () => { expect(resetWalletLock).toBeCalledWith(); expect(setCardanoWallet).toBeCalledWith(); }); + + test('should activate another wallet if exists after deletion', async () => { + const remainingWallet = { + type: WalletType.InMemory, + walletId: 'remaining-wallet-id', + accounts: [{ accountIndex: 1 }] + }; + (walletApiUi.walletRepository as any).wallets$ = of([remainingWallet]); + + await deleteWallet(); + + expect(walletApiUi.walletManager.activate).toBeCalledWith( + expect.objectContaining({ + walletId: remainingWallet.walletId, + accountIndex: remainingWallet.accounts[0].accountIndex + }) + ); + }); }); describe('switchNetwork', () => { diff --git a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts index 383bc5516..50a16b877 100644 --- a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts +++ b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts @@ -42,7 +42,7 @@ export interface CreateWallet { name: string; mnemonic: string[]; password: string; - chainId: Wallet.Cardano.ChainId; + chainId?: Wallet.Cardano.ChainId; } export interface SetWallet { @@ -55,7 +55,6 @@ export interface CreateHardwareWallet { accountIndex?: number; name: string; deviceConnection: Wallet.DeviceConnection; - chainId: Wallet.Cardano.ChainId; connectedDevice: Wallet.HardwareWallets; } @@ -76,7 +75,10 @@ export interface UseWalletManager { createHardwareWallet: (args: CreateHardwareWallet) => Promise; connectHardwareWallet: (model: Wallet.HardwareWallets) => Promise; saveHardwareWallet: (wallet: Wallet.CardanoWallet, chainName?: Wallet.ChainName) => Promise; - deleteWallet: (isForgotPasswordFlow?: boolean) => Promise; + /** + * @returns active wallet id after deleting the wallet; undefined if deleted the last wallet + */ + deleteWallet: (isForgotPasswordFlow?: boolean) => Promise; switchNetwork: (chainName: Wallet.ChainName) => Promise; addAccount: (props: WalletManagerAddAccountProps) => Promise; } @@ -197,72 +199,6 @@ const encryptMnemonic = async (mnemonic: string[], passphrase: Uint8Array) => { return HexBlob.fromBytes(walletEncrypted); }; -/** - * Creates a Ledger or Trezor hardware wallet - * and saves it in browser storage with the data to lock/unlock it - */ -const createHardwareWallet = async ({ - accountIndex = 0, - deviceConnection, - name, - chainId, - connectedDevice -}: CreateHardwareWallet): Promise => { - const keyAgent = - connectedDevice === WalletType.Ledger - ? await Wallet.Ledger.LedgerKeyAgent.createWithDevice( - { - chainId, - communicationType: Wallet.KeyManagement.CommunicationType.Web, - accountIndex, - deviceConnection: typeof deviceConnection === 'object' ? deviceConnection : undefined - }, - { bip32Ed25519: Wallet.bip32Ed25519, logger } - ) - : await Wallet.Trezor.TrezorKeyAgent.createWithDevice( - { - chainId, - trezorConfig: { - communicationType: Wallet.KeyManagement.CommunicationType.Web, - manifest: Wallet.manifest - }, - accountIndex - }, - { bip32Ed25519: Wallet.bip32Ed25519, logger } - ); - - const addWalletProps: AddWalletProps = { - metadata: { name, lastActiveAccountIndex: accountIndex }, - type: connectedDevice, - accounts: [ - { - extendedAccountPublicKey: keyAgent.extendedAccountPublicKey, - accountIndex, - metadata: { name: defaultAccountName(accountIndex) } - } - ] - }; - const walletId = await walletRepository.addWallet(addWalletProps); - await walletManager.activate({ - walletId, - chainId: DEFAULT_CHAIN_ID, - accountIndex - }); - - return { - name, - signingCoordinator, - wallet: observableWallet, - source: { - wallet: { - ...addWalletProps, - walletId - }, - account: addWalletProps.accounts[0] - } - }; -}; - /** Connects a hardware wallet device */ export const connectHardwareWallet = async (model: Wallet.HardwareWallets): Promise => await Wallet.connectDevice(model); @@ -298,6 +234,56 @@ export const useWalletManager = (): UseWalletManager => { return (storedChain?.chainName && chainIdFromName(storedChain.chainName)) || DEFAULT_CHAIN_ID; }, [currentChain]); + /** + * Creates a Ledger or Trezor hardware wallet + * and saves it in browser storage with the data to lock/unlock it + */ + const createHardwareWallet = useCallback( + async ({ + accountIndex = 0, + deviceConnection, + name, + connectedDevice + }: CreateHardwareWallet): Promise => { + const extendedAccountPublicKey = await getHwExtendedAccountPublicKey( + connectedDevice, + accountIndex, + deviceConnection + ); + const addWalletProps: AddWalletProps = { + metadata: { name, lastActiveAccountIndex: accountIndex }, + type: connectedDevice, + accounts: [ + { + extendedAccountPublicKey, + accountIndex, + metadata: { name: defaultAccountName(accountIndex) } + } + ] + }; + const walletId = await walletRepository.addWallet(addWalletProps); + await walletManager.activate({ + walletId, + chainId: getCurrentChainId(), + accountIndex + }); + + return { + name, + signingCoordinator, + wallet: observableWallet, + source: { + wallet: { + ...addWalletProps, + walletId + }, + account: addWalletProps.accounts[0] + } + }; + }, + [getCurrentChainId] + ); + const tryMigrateToWalletRepository = useCallback(async (): Promise< AnyWallet[] | undefined > => { @@ -435,11 +421,7 @@ export const useWalletManager = (): UseWalletManager => { */ const saveHardwareWallet = useCallback( async (wallet: Wallet.CardanoWallet, chainName = CHAIN): Promise => { - updateAppSettings({ - chainName, - // Doesn't make sense for hardware wallets - mnemonicVerificationFrequency: '' - }); + updateAppSettings({ chainName }); setCardanoWallet(wallet); setCurrentChain(chainName); @@ -451,7 +433,12 @@ export const useWalletManager = (): UseWalletManager => { * Creates or restores a new in-memory wallet with the cardano-js-sdk and saves it in wallet repository */ const createWallet = useCallback( - async ({ mnemonic, name, password, chainId }: CreateWallet): Promise => { + async ({ + mnemonic, + name, + password, + chainId = getCurrentChainId() + }: CreateWallet): Promise => { const accountIndex = 0; const passphrase = Buffer.from(password, 'utf8'); const keyAgent = await Wallet.KeyManagement.InMemoryKeyAgent.fromBip39MnemonicWords( @@ -492,7 +479,7 @@ export const useWalletManager = (): UseWalletManager => { const walletId = await walletRepository.addWallet(addWalletProps); await walletManager.activate({ walletId, - chainId: getCurrentChainId(), + chainId, accountIndex }); @@ -540,12 +527,27 @@ export const useWalletManager = (): UseWalletManager => { /** * Deletes Wallet from memory, all info from browser storage and destroys all stores + * + * @returns active wallet id after deleting the wallet */ const deleteWallet = useCallback( - async (isForgotPasswordFlow = false): Promise => { + async (isForgotPasswordFlow = false): Promise => { const activeWallet = await firstValueFrom(walletManager.activeWalletId$); await walletManager.deactivate(); await walletRepository.removeWallet(activeWallet.walletId); + + const wallets = await firstValueFrom(walletRepository.wallets$); + if (wallets.length > 0) { + const activateProps = { + walletId: wallets[0].walletId, + chainId: getCurrentChainId(), + accountIndex: wallets[0].type === WalletType.Script ? undefined : wallets[0].accounts[0].accountIndex + }; + await walletManager.activate(activateProps); + return activateProps; + } + + // deleting last wallet clears all data if (!isForgotPasswordFlow) { deleteFromLocalStorage('appSettings'); } @@ -590,7 +592,15 @@ export const useWalletManager = (): UseWalletManager => { await walletManager.destroyData(activeWallet.walletId, chainIdFromName(chainName)); } }, - [resetWalletLock, setCardanoWallet, backgroundService, userIdService, clearAddressBook, clearNftsFolders] + [ + resetWalletLock, + setCardanoWallet, + backgroundService, + userIdService, + clearAddressBook, + clearNftsFolders, + getCurrentChainId + ] ); /** diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/MultiWallet.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/MultiWallet.tsx index fb71d9ce8..f55fe1b89 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/MultiWallet.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/MultiWallet.tsx @@ -23,6 +23,7 @@ import { Subject } from 'rxjs'; import { Wallet } from '@lace/cardano'; import { NavigationButton } from '@lace/common'; import { useBackgroundPage } from '@providers/BackgroundPageProvider'; +import { Providers } from './hardware-wallet/types'; const { newWallet } = walletRoutePaths; @@ -33,9 +34,26 @@ interface ConfirmationDialog { } export const SetupHardwareWallet = ({ shouldShowDialog$ }: ConfirmationDialog): JSX.Element => { - const { connectHardwareWallet } = useWalletManager(); + const { connectHardwareWallet, createHardwareWallet } = useWalletManager(); const disconnectHardwareWallet$ = useMemo(() => new Subject(), []); + const hardwareWalletProviders = useMemo( + (): Providers => ({ + connectHardwareWallet, + disconnectHardwareWallet$, + shouldShowDialog$, + createWallet: async ({ account, connection, model, name }) => { + await createHardwareWallet({ + connectedDevice: model, + deviceConnection: connection, + name, + accountIndex: account + }); + } + }), + [connectHardwareWallet, createHardwareWallet, disconnectHardwareWallet$, shouldShowDialog$] + ); + useEffect(() => { const onHardwareWalletDisconnect = (event: HIDConnectionEvent) => { disconnectHardwareWallet$.next(event); @@ -49,16 +67,7 @@ export const SetupHardwareWallet = ({ shouldShowDialog$ }: ConfirmationDialog): }; }, [disconnectHardwareWallet$]); - return ( - - ); + return ; }; export const SetupCreateWallet = (confirmationDialog: ConfirmationDialog): JSX.Element => ( diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/create-wallet/CreateWallet.test.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/create-wallet/CreateWallet.test.tsx index 1d4440dce..dc79f4b17 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/create-wallet/CreateWallet.test.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/create-wallet/CreateWallet.test.tsx @@ -7,6 +7,16 @@ import { Providers } from './types'; import { walletRoutePaths } from '@routes'; import { createAssetsRoute, fillMnemonic, getNextButton, mnemonicWords, setupStep } from '../tests/utils'; import { BehaviorSubject, firstValueFrom } from 'rxjs'; +import { StoreProvider } from '@src/stores'; +import { APP_MODE_BROWSER } from '@src/utils/constants'; +import { AppSettingsProvider, DatabaseProvider } from '@providers'; +import { UseWalletManager } from '@hooks/useWalletManager'; + +jest.mock('@hooks/useWalletManager', () => ({ + useWalletManager: jest.fn().mockReturnValue({ + createWallet: jest.fn().mockResolvedValue(void 0) as UseWalletManager['createWallet'] + } as UseWalletManager) +})); const keepWalletSecureStep = async () => { const nextButton = getNextButton(); @@ -61,10 +71,16 @@ describe('Multi Wallet Setup/Create Wallet', () => { providers.createWallet.mockResolvedValue(void 0); render( - - - {createAssetsRoute()} - + + + + + + {createAssetsRoute()} + + + + ); await setupStep(); @@ -74,10 +90,16 @@ describe('Multi Wallet Setup/Create Wallet', () => { test('should emit correct value for shouldShowDialog', async () => { render( - - - {createAssetsRoute()} - + + + + + + {createAssetsRoute()} + + + + ); const nameInput = screen.getByTestId('wallet-name-input'); diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/create-wallet/steps/NewRecoveryPhrase.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/create-wallet/steps/NewRecoveryPhrase.tsx index 8aea77430..9b5a6e13f 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/create-wallet/steps/NewRecoveryPhrase.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/create-wallet/steps/NewRecoveryPhrase.tsx @@ -1,11 +1,12 @@ import { MnemonicStage, WalletSetupMnemonicStep } from '@lace/core'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router'; import { wordlists } from 'bip39'; import { WarningModal } from '@src/views/browser-view/components'; import { useCreateWallet } from '../context'; import { walletRoutePaths } from '@routes'; +import { useWalletManager } from '@hooks/useWalletManager'; const noop = (): void => void 0; @@ -20,6 +21,7 @@ export const NewRecoveryPhrase = (): JSX.Element => { const history = useHistory(); const { t } = useTranslation(); const { generatedMnemonic, data } = useCreateWallet(); + const { createWallet } = useWalletManager(); const [state, setState] = useState(() => ({ isResetMnemonicModalOpen: false, resetMnemonicStage: 'writedown' @@ -36,6 +38,19 @@ export const NewRecoveryPhrase = (): JSX.Element => { passphraseError: t('core.walletSetupMnemonicStep.passphraseError') }; + const clearSecrets = useCallback(() => { + for (let i = 0; i < data.mnemonic.length; i++) { + data.mnemonic[i] = ''; + } + data.password = ''; + }, [data]); + + const saveWallet = useCallback(async () => { + await createWallet(data); + clearSecrets(); + history.push(walletRoutePaths.assets); + }, [data, createWallet, history, clearSecrets]); + return ( <> { onReset={(resetStage) => setState((s) => ({ ...s, isResetMnemonicModalOpen: true, resetMnemonicStage: resetStage })) } - onNext={() => history.push(walletRoutePaths.assets)} + onNext={saveWallet} onStepNext={noop} translations={walletSetupMnemonicStepTranslations} suggestionList={wordList} diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/NameWallet.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/NameWallet.tsx index 357ffee90..50ce0c1e5 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/NameWallet.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/NameWallet.tsx @@ -1,5 +1,5 @@ import { WalletSetupWalletNameStep } from '@lace/core'; -import React, { useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useHistory } from 'react-router'; import { StartOverDialog } from '../../../wallet-setup/components/StartOverDialog'; import { useTranslation } from 'react-i18next'; @@ -14,7 +14,7 @@ interface State { export const NameWallet = (): JSX.Element => { const history = useHistory(); const { t } = useTranslation(); - const { createWallet, setName, resetConnection } = useHardwareWallet(); + const { createWallet, setName, data, resetConnection } = useHardwareWallet(); const [isStartOverDialogVisible, setIsStartOverDialogVisible] = useState(false); const [state, setState] = useState({}); @@ -26,8 +26,9 @@ export const NameWallet = (): JSX.Element => { chooseName: t('core.walletSetupWalletNameStep.chooseName') }; - const handleOnNext = async (name: string) => { - setName(name); + // if createWallet is called in handleOnNext, then it will get name as '' + useEffect(() => { + if (!data?.name) return; createWallet() .then(() => { setState({ error: undefined }); @@ -38,11 +39,19 @@ export const NameWallet = (): JSX.Element => { error: 'common' }); }); - }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data.name]); + + const handleOnNext = useCallback( + async (name: string) => { + setName(name); + }, + [setName] + ); - const onRetry = () => { + const onRetry = useCallback(() => { setState({ error: undefined }); - }; + }, [setState]); return ( <> diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/restore-wallet/RestoreWallet.test.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/restore-wallet/RestoreWallet.test.tsx index ac98bbb7e..e1932f2e5 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/restore-wallet/RestoreWallet.test.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/restore-wallet/RestoreWallet.test.tsx @@ -7,6 +7,16 @@ import { Providers } from './types'; import { walletRoutePaths } from '@routes'; import { createAssetsRoute, fillMnemonic, getNextButton, setupStep } from '../tests/utils'; import { Subject } from 'rxjs'; +import { StoreProvider } from '@src/stores'; +import { APP_MODE_BROWSER } from '@src/utils/constants'; +import { AppSettingsProvider, DatabaseProvider } from '@providers'; +import { UseWalletManager } from '@hooks/useWalletManager'; + +jest.mock('@hooks/useWalletManager', () => ({ + useWalletManager: jest.fn().mockReturnValue({ + createWallet: jest.fn().mockResolvedValue(void 0) as UseWalletManager['createWallet'] + } as UseWalletManager) +})); const keepWalletSecureStep = async () => { const nextButton = getNextButton(); @@ -68,10 +78,16 @@ describe('Multi Wallet Setup/Restore Wallet', () => { providers.createWallet.mockResolvedValue(void 0); render( - - - {createAssetsRoute()} - + + + + + + {createAssetsRoute()} + + + + ); await setupStep(); diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/restore-wallet/steps/RestoreRecoveryPhrase.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/restore-wallet/steps/RestoreRecoveryPhrase.tsx index fdd17b822..c50d31d8d 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/restore-wallet/steps/RestoreRecoveryPhrase.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/restore-wallet/steps/RestoreRecoveryPhrase.tsx @@ -1,13 +1,13 @@ import { WalletSetupMnemonicVerificationStep } from '@lace/core'; -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { wordlists } from 'bip39'; import { Wallet } from '@lace/cardano'; import { useHistory } from 'react-router-dom'; import { useRestoreWallet } from '../context'; import { walletRoutePaths } from '@routes'; - -const noop = (): void => void 0; +import noop from 'lodash/noop'; +import { useWalletManager } from '@hooks'; const wordList = wordlists.english; @@ -15,6 +15,18 @@ export const RestoreRecoveryPhrase = (): JSX.Element => { const { t } = useTranslation(); const history = useHistory(); const { data, setMnemonic } = useRestoreWallet(); + const { createWallet } = useWalletManager(); + const isValidMnemonic = useMemo( + () => Wallet.KeyManagement.util.validateMnemonic(Wallet.KeyManagement.util.joinMnemonicWords(data.mnemonic)), + [data.mnemonic] + ); + + const clearSecrets = useCallback(() => { + for (let i = 0; i < data.mnemonic.length; i++) { + data.mnemonic[i] = ''; + } + data.password = ''; + }, [data]); const walletSetupMnemonicStepTranslations = { writePassphrase: t('core.walletSetupMnemonicStep.writePassphrase'), @@ -32,12 +44,17 @@ export const RestoreRecoveryPhrase = (): JSX.Element => { setMnemonic(mnemonic)} - onCancel={() => history.goBack()} - onSubmit={() => history.push(walletRoutePaths.assets)} + onCancel={() => { + clearSecrets(); + history.goBack(); + }} + onSubmit={useCallback(async () => { + await createWallet(data); + clearSecrets(); + history.push(walletRoutePaths.assets); + }, [data, clearSecrets, createWallet, history])} onStepNext={noop} - isSubmitEnabled={Wallet.KeyManagement.util.validateMnemonic( - Wallet.KeyManagement.util.joinMnemonicWords(data.mnemonic) - )} + isSubmitEnabled={isValidMnemonic} translations={walletSetupMnemonicStepTranslations} suggestionList={wordList} /> diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/settings/components/SettingsRemoveWallet.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/settings/components/SettingsRemoveWallet.tsx index 06c2a3011..504f77264 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/settings/components/SettingsRemoveWallet.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/settings/components/SettingsRemoveWallet.tsx @@ -35,7 +35,9 @@ export const SettingsRemoveWallet = ({ popupView }: { popupView?: boolean }): Re const removeWallet = async () => { analytics.sendEventToPostHog(PostHogAction.SettingsHoldUpRemoveWalletClick); setDeletingWallet(true); - await deleteWallet(); + const nextActiveWallet = await deleteWallet(); + setDeletingWallet(false); + if (nextActiveWallet) return; if (popupView) await backgroundServices.handleOpenBrowser({ section: BrowserViewSections.HOME }); // force reload to ensure all stores are cleaned up location.reload(); diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow.tsx index 558665f97..f3d0718ad 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow.tsx @@ -30,12 +30,7 @@ import { WalletType } from '@cardano-sdk/web-extension'; import { useWalletStore } from '@src/stores'; const { CHAIN } = config(); -const { - Cardano: { ChainIds }, - AVAILABLE_WALLETS -} = Wallet; -const DEFAULT_CHAIN_ID = ChainIds[CHAIN]; - +const { AVAILABLE_WALLETS } = Wallet; export interface HardwareWalletFlowProps { onCancel: () => void; onAppReload: () => void; @@ -163,7 +158,6 @@ export const HardwareWalletFlow = ({ accountIndex, deviceConnection, name, - chainId: DEFAULT_CHAIN_ID, connectedDevice }); setWalletCreated(cardanoWallet); From 447bdd51f572caaac14e4bbff6427bb99eaa77ea Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Tue, 20 Feb 2024 17:04:33 +0200 Subject: [PATCH 09/27] fix(extension): connect to hw device asap when adding account If it takes more than some time (probably 50ms), Chrome will reject hardware device connection with an error that says WebHID connections must be initiated from user gesture. Apparently, fetching wallets from repository can sometimes take longer. This fix updates useWalletManager addAccount method to take in the entire AnyBip32Wallet object instead of just walletId, so that it doesn't have to fetch the wallet object from indexeddb in service worker. Consequently, some validations from addAccount can be removed, because it's signature no longer accepts walletId of a script wallet. Also it has to trust that wallet actually exists, otherwise it will reject with an error that comes from the WalletRepository (it is no longer a responsibility of this method to check the existence of the wallet). --- .../components/WalletAccounts.tsx | 4 +- .../hooks/__tests__/useWalletManager.test.tsx | 42 ++++--------------- .../src/hooks/useWalletManager.ts | 16 +++---- 3 files changed, 19 insertions(+), 43 deletions(-) diff --git a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx index dee846382..3761a6e84 100644 --- a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx +++ b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx @@ -64,12 +64,12 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack: const unlockAccount = useCallback( async (accountIndex: number) => { await addAccount({ - walletId: wallet.walletId, + wallet, accountIndex, metadata: { name: defaultAccountName(accountIndex) } }); }, - [wallet.walletId, addAccount] + [wallet, addAccount] ); const lockAccount = useCallback(async () => { diff --git a/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx b/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx index 04f03723d..40385141c 100644 --- a/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx +++ b/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx @@ -34,7 +34,7 @@ import * as localStorage from '@src/utils/local-storage'; import * as AppSettings from '@providers/AppSettings'; import * as walletApiUi from '@src/lib/wallet-api-ui'; import { of } from 'rxjs'; -import { AnyBip32Wallet, AnyWallet, Bip32Wallet, WalletType } from '@cardano-sdk/web-extension'; +import { AnyBip32Wallet, AnyWallet, WalletType } from '@cardano-sdk/web-extension'; import { Wallet } from '@lace/cardano'; jest.mock('@providers/AppSettings', () => ({ @@ -669,28 +669,6 @@ describe('Testing useWalletManager hook', () => { }); describe('addAccount', () => { - it('throws an error when wallet with specified id does not exist', async () => { - (walletApiUi.walletRepository as any).wallets$ = of([]); - const { addAccount } = render(); - await expect( - addAccount({ walletId: 'walletid', accountIndex: 1, metadata: { name: 'new account' } }) - ).rejects.toThrowError('Wallet not found: walletid'); - }); - - it('throws an error when wallet with specified id is not a bip32 wallet', async () => { - const walletId = 'script-wallet-id'; - (walletApiUi.walletRepository as any).wallets$ = of([ - { - walletId, - type: WalletType.Script - } - ]); - const { addAccount } = render(); - await expect(addAccount({ walletId, accountIndex: 1, metadata: { name: 'new account' } })).rejects.toThrowError( - 'Cannot add account to a script wallet' - ); - }); - describe('for existing bip32 wallet', () => { const extendedAccountPublicKey = '12b608b67a743891656d6463f72aa6e5f0e62ba6dc47e32edfebafab1acf0fa9f3033c2daefa3cb2ac16916b08c7e7424d4e1aafae2206d23c4d002299c07128'; @@ -729,20 +707,18 @@ describe('Testing useWalletManager hook', () => { async ({ type, walletProps, prepare }) => { prepare(); const walletId = 'bip32-wallet-id'; - (walletApiUi.walletRepository as any).wallets$ = of([ - { - walletId, - type, - accounts: [], - ...walletProps - } as unknown as Bip32Wallet - ]); - const addAccountProps = { walletId, accountIndex: 0, metadata: { name: 'new account' } }; + const addAccountProps = { + wallet: { walletId, type, ...walletProps } as AnyBip32Wallet, + accountIndex: 0, + metadata: { name: 'new account' } + }; const { addAccount } = render(); await addAccount(addAccountProps); expect(walletApiUi.walletRepository.addAccount).toBeCalledWith({ - ...addAccountProps, + walletId, + accountIndex: addAccountProps.accountIndex, + metadata: addAccountProps.metadata, extendedAccountPublicKey }); } diff --git a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts index 50a16b877..027c9c949 100644 --- a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts +++ b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts @@ -21,7 +21,6 @@ import { ENHANCED_ANALYTICS_OPT_IN_STATUS_LS_KEY } from '@providers/AnalyticsPro import { ILocalStorage } from '@src/types'; import { firstValueFrom } from 'rxjs'; import { - AddAccountProps, AddWalletProps, AnyBip32Wallet, AnyWallet, @@ -58,7 +57,12 @@ export interface CreateHardwareWallet { connectedDevice: Wallet.HardwareWallets; } -type WalletManagerAddAccountProps = Omit, 'extendedAccountPublicKey'>; +type WalletManagerAddAccountProps = { + wallet: AnyBip32Wallet; + metadata: Wallet.AccountMetadata; + accountIndex: number; +}; + type ActivateWalletProps = Omit; export interface UseWalletManager { @@ -662,17 +666,13 @@ export const useWalletManager = (): UseWalletManager => { [walletLock, loadWallet] ); - const addAccount = async ({ walletId, accountIndex, metadata }: WalletManagerAddAccountProps): Promise => { - const wallets = await firstValueFrom(walletRepository.wallets$); - const wallet = wallets.find((w) => w.walletId === walletId); - if (!wallet) throw new Error(`Wallet not found: ${walletId}`); - if (wallet.type === WalletType.Script) throw new Error('Cannot add account to a script wallet'); + const addAccount = async ({ wallet, accountIndex, metadata }: WalletManagerAddAccountProps): Promise => { const extendedAccountPublicKey = await getExtendedAccountPublicKey(wallet, accountIndex); await walletRepository.addAccount({ accountIndex, extendedAccountPublicKey, metadata, - walletId + walletId: wallet.walletId }); }; From 0796af441eedc60080c3b58f7fafb42b4f7301e8 Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Wed, 21 Feb 2024 12:29:51 +0200 Subject: [PATCH 10/27] fix: display correct wallet type icon --- .../src/components/DropdownMenu/DropdownMenu.tsx | 3 ++- .../MainMenu/DropdownMenuOverlay/components/UserInfo.tsx | 3 ++- .../src/utils/get-ui-wallet-type.ts | 7 +++++++ packages/ui/src/design-system/profile-dropdown/index.ts | 1 + 4 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 apps/browser-extension-wallet/src/utils/get-ui-wallet-type.ts diff --git a/apps/browser-extension-wallet/src/components/DropdownMenu/DropdownMenu.tsx b/apps/browser-extension-wallet/src/components/DropdownMenu/DropdownMenu.tsx index 2bfe5a98f..1539fab03 100644 --- a/apps/browser-extension-wallet/src/components/DropdownMenu/DropdownMenu.tsx +++ b/apps/browser-extension-wallet/src/components/DropdownMenu/DropdownMenu.tsx @@ -15,6 +15,7 @@ import { ProfileDropdown } from '@lace/ui'; import { useGetHandles } from '@hooks'; import { getAssetImageUrl } from '@src/utils/get-asset-image-url'; import { getActiveWalletSubtitle } from '@src/utils/get-wallet-subtitle'; +import { getUiWalletType } from '@src/utils/get-ui-wallet-type'; export interface DropdownMenuProps { isPopup?: boolean; @@ -62,7 +63,7 @@ export const DropdownMenu = ({ isPopup }: DropdownMenuProps): React.ReactElement } : undefined } - type={process.env.USE_SHARED_WALLET === 'true' ? 'shared' : 'cold'} + type={getUiWalletType(cardanoWallet.source.wallet.type)} id="menu" /> diff --git a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx index d9b817238..a921b6919 100644 --- a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx +++ b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx @@ -15,6 +15,7 @@ import { ProfileDropdown } from '@lace/ui'; import { AnyBip32Wallet, AnyWallet, Bip32WalletAccount, WalletType } from '@cardano-sdk/web-extension'; import { Wallet } from '@lace/cardano'; import { Separator } from './Separator'; +import { getUiWalletType } from '@src/utils/get-ui-wallet-type'; const ADRESS_FIRST_PART_LENGTH = 10; const ADRESS_LAST_PART_LENGTH = 5; @@ -91,7 +92,7 @@ export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInf accountIndex: lastActiveAccount.accountIndex }); }} - type="cold" + type={getUiWalletType(wallet.type)} /> ); }, diff --git a/apps/browser-extension-wallet/src/utils/get-ui-wallet-type.ts b/apps/browser-extension-wallet/src/utils/get-ui-wallet-type.ts new file mode 100644 index 000000000..67eb9f046 --- /dev/null +++ b/apps/browser-extension-wallet/src/utils/get-ui-wallet-type.ts @@ -0,0 +1,7 @@ +import { WalletType } from '@cardano-sdk/web-extension'; +import { ProfileDropdown } from '@lace/ui'; + +export const getUiWalletType = (walletType: WalletType): ProfileDropdown.WalletType => { + if (walletType === WalletType.Script) return 'shared'; + return walletType === WalletType.InMemory ? 'hot' : 'cold'; +}; diff --git a/packages/ui/src/design-system/profile-dropdown/index.ts b/packages/ui/src/design-system/profile-dropdown/index.ts index 40545b0ab..29d10c05f 100644 --- a/packages/ui/src/design-system/profile-dropdown/index.ts +++ b/packages/ui/src/design-system/profile-dropdown/index.ts @@ -3,3 +3,4 @@ export { WalletOption } from './profile-dropdown-wallet-option.component'; export { WalletStatus } from './profile-dropdown-wallet-status.component'; export { AccountsList } from './accounts/profile-dropdown-accounts-list.component'; export type { AccountData } from './accounts/profile-dropdown-accounts-list.component'; +export type { WalletType } from './profile-dropdown.data'; From 6ffd3a02d9e4921b47afb0b3eb3ad2b21f1f107e Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Thu, 22 Feb 2024 08:53:20 +0200 Subject: [PATCH 11/27] feat(extension): do not copy address when clicking on active wallet clicking on a wallet in dropdown menu activates it removing copy feature makes the behavior more predictable --- .../DropdownMenuOverlay/components/UserInfo.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx index a921b6919..5fb73123f 100644 --- a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx +++ b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx @@ -126,10 +126,10 @@ export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInf [styles.multiWalletWrapper]: process.env.USE_MULTI_WALLET === 'true' })} > - - {process.env.USE_MULTI_WALLET === 'true' ? ( -
{wallets.map((wallet, i) => renderWallet(wallet, i === wallets.length - 1))}
- ) : ( + {process.env.USE_MULTI_WALLET === 'true' ? ( +
{wallets.map((wallet, i) => renderWallet(wallet, i === wallets.length - 1))}
+ ) : ( + - )} - +
+ )} {process.env.USE_MULTI_WALLET === 'true' ? undefined : (
From d7759339c2c9f1415aa9d2858bc1da3421384476 Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Thu, 22 Feb 2024 09:41:11 +0200 Subject: [PATCH 12/27] feat(extension): close dropdown and show toast when activating wallet or account --- .../components/DropdownMenu/DropdownMenu.tsx | 12 ++++++--- .../components/UserInfo.tsx | 15 +++++++---- .../components/WalletAccounts.tsx | 13 +++++++--- .../src/lib/translations/en.json | 4 +++ .../src/stores/slices/ui-slice.ts | 25 +++++++++++++++---- .../src/stores/types.ts | 1 + apps/browser-extension-wallet/src/types/ui.ts | 1 + .../src/utils/mocks/store.tsx | 2 ++ 8 files changed, 56 insertions(+), 17 deletions(-) diff --git a/apps/browser-extension-wallet/src/components/DropdownMenu/DropdownMenu.tsx b/apps/browser-extension-wallet/src/components/DropdownMenu/DropdownMenu.tsx index 1539fab03..3edf69b2c 100644 --- a/apps/browser-extension-wallet/src/components/DropdownMenu/DropdownMenu.tsx +++ b/apps/browser-extension-wallet/src/components/DropdownMenu/DropdownMenu.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import cn from 'classnames'; import { Dropdown } from 'antd'; import { Button } from '@lace/common'; @@ -23,8 +23,11 @@ export interface DropdownMenuProps { export const DropdownMenu = ({ isPopup }: DropdownMenuProps): React.ReactElement => { const analytics = useAnalyticsContext(); - const { cardanoWallet } = useWalletStore(); - const [open, setOpen] = useState(false); + const { + cardanoWallet, + walletUI: { isDropdownMenuOpen }, + setIsDropdownMenuOpen + } = useWalletStore(); const [handle] = useGetHandles(); const handleImage = handle?.profilePic; const Chevron = isPopup ? ChevronSmall : ChevronNormal; @@ -34,7 +37,7 @@ export const DropdownMenu = ({ isPopup }: DropdownMenuProps): React.ReactElement }; const handleDropdownState = (openDropdown: boolean) => { - setOpen(openDropdown); + setIsDropdownMenuOpen(openDropdown); if (openDropdown) { sendAnalyticsEvent(PostHogAction.UserWalletProfileIconClick); } @@ -48,6 +51,7 @@ export const DropdownMenu = ({ isPopup }: DropdownMenuProps): React.ReactElement onOpenChange={handleDropdownState} overlay={} placement="bottomRight" + open={isDropdownMenuOpen} trigger={['click']} > {process.env.USE_MULTI_WALLET === 'true' ? ( diff --git a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx index 5fb73123f..fc5fe193f 100644 --- a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx +++ b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx @@ -37,7 +37,7 @@ const NO_WALLETS: AnyWallet[] = [ export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInfoProps): React.ReactElement => { const { t } = useTranslation(); - const { walletInfo, cardanoWallet } = useWalletStore(); + const { walletInfo, cardanoWallet, setIsDropdownMenuOpen } = useWalletStore(); const { activateWallet, walletRepository } = useWalletManager(); const analytics = useAnalyticsContext(); const wallets = useObservable(walletRepository.wallets$, NO_WALLETS); @@ -86,22 +86,27 @@ export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInf subtitle={lastActiveAccount.metadata.name} id={walletName} onOpenAccountsMenu={() => onOpenWalletAccounts(wallet)} - onClick={() => { - activateWallet({ + onClick={async () => { + await activateWallet({ walletId: wallet.walletId, accountIndex: lastActiveAccount.accountIndex }); + setIsDropdownMenuOpen(false); + toast.notify({ + duration: TOAST_DEFAULT_DURATION, + text: t('multiWallet.activated.wallet', { walletName }) + }); }} type={getUiWalletType(wallet.type)} /> ); }, - [activateWallet, getLastActiveAccount, onOpenWalletAccounts, walletName] + [activateWallet, getLastActiveAccount, onOpenWalletAccounts, walletName, setIsDropdownMenuOpen, t] ); const renderWallet = useCallback( (wallet: AnyWallet, isLast: boolean) => ( -
+
{wallet.type !== WalletType.Script ? renderBip32Wallet(wallet) : (() => { diff --git a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx index 3761a6e84..717538ca1 100644 --- a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx +++ b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx @@ -1,13 +1,14 @@ /* eslint-disable react/jsx-handler-names */ import React, { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { NavigationButton } from '@lace/common'; +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 { useWalletStore } from '@src/stores'; import { useWalletManager } from '@hooks'; +import { TOAST_DEFAULT_DURATION } from '@hooks/useActionExecution'; const defaultAccountName = (accountNumber: number) => `Account #${accountNumber}`; @@ -17,7 +18,7 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack: const { t } = useTranslation(); const editAccountDrawer = useAccountDataModal(); const disableAccountConfirmation = useAccountDataModal(); - const { manageAccountsWallet: wallet, cardanoWallet } = useWalletStore(); + const { manageAccountsWallet: wallet, cardanoWallet, setIsDropdownMenuOpen } = useWalletStore(); const { source: { wallet: { walletId: activeWalletId }, @@ -45,8 +46,14 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack: 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 }) + }); }, - [wallet.walletId, activateWallet] + [wallet.walletId, activateWallet, accountsData, setIsDropdownMenuOpen, t] ); const editAccount = useCallback( diff --git a/apps/browser-extension-wallet/src/lib/translations/en.json b/apps/browser-extension-wallet/src/lib/translations/en.json index 2596248f3..b0371f02d 100644 --- a/apps/browser-extension-wallet/src/lib/translations/en.json +++ b/apps/browser-extension-wallet/src/lib/translations/en.json @@ -1407,6 +1407,10 @@ "description": "You'll have to start over.", "cancel": "Go back", "confirm": "Proceed" + }, + "activated": { + "wallet": "Wallet \"{{walletName}}\" activated", + "account": "Account \"{{accountName}}\" activated" } } } diff --git a/apps/browser-extension-wallet/src/stores/slices/ui-slice.ts b/apps/browser-extension-wallet/src/stores/slices/ui-slice.ts index 039c33693..8816d1ce9 100644 --- a/apps/browser-extension-wallet/src/stores/slices/ui-slice.ts +++ b/apps/browser-extension-wallet/src/stores/slices/ui-slice.ts @@ -1,6 +1,6 @@ import { Wallet } from '@lace/cardano'; import isNil from 'lodash/isNil'; -import { ILocalStorage, NetworkConnectionStates, WalletUIProps } from '@src/types'; +import { ILocalStorage, NetworkConnectionStates, WalletUI, WalletUIProps } from '@src/types'; import { AppMode, cardanoCoin, CARDANO_COIN_SYMBOL } from '@src/utils/constants'; import { GetState, SetState } from 'zustand'; import { SliceCreator, UISlice } from '../types'; @@ -13,8 +13,14 @@ const setWalletUI = ( { network, networkConnection, - areBalancesVisible - }: { network?: Wallet.Cardano.NetworkId; networkConnection?: NetworkConnectionStates; areBalancesVisible?: boolean }, + areBalancesVisible, + isDropdownMenuOpen + }: { + network?: Wallet.Cardano.NetworkId; + networkConnection?: NetworkConnectionStates; + areBalancesVisible?: boolean; + isDropdownMenuOpen?: boolean; + }, { get, set }: { get: GetState; set: SetState } ): void => { const { walletUI } = get(); @@ -28,13 +34,20 @@ const setWalletUI = ( symbol: CARDANO_COIN_SYMBOL[network] } }), + ...(typeof isDropdownMenuOpen !== 'undefined' && { isDropdownMenuOpen }), ...(networkConnection && { networkConnection }), ...(!isNil(areBalancesVisible) && { areBalancesVisible }) } }); }; -const getWalletUI = ({ currentNetwork, appMode }: { currentNetwork: Wallet.Cardano.NetworkId; appMode: AppMode }) => { +const getWalletUI = ({ + currentNetwork, + appMode +}: { + currentNetwork: Wallet.Cardano.NetworkId; + appMode: AppMode; +}): WalletUI => { const shouldHideBalance = HIDE_BALANCE_FEATURE_ENABLED ? getValueFromLocalStorage('hideBalance') : false; @@ -49,7 +62,8 @@ const getWalletUI = ({ currentNetwork, appMode }: { currentNetwork: Wallet.Carda areBalancesVisible: !shouldHideBalance, getHiddenBalancePlaceholder: (placeholderLength = HIDE_BALANCE_PLACEHOLDER_LENGTH, placeholderChar = '*') => placeholderChar.repeat(placeholderLength), - canManageBalancesVisibility: HIDE_BALANCE_FEATURE_ENABLED + canManageBalancesVisibility: HIDE_BALANCE_FEATURE_ENABLED, + isDropdownMenuOpen: false }; }; @@ -60,6 +74,7 @@ export const uiSlice: SliceCreator = ({ get, se }); return { + setIsDropdownMenuOpen: (isDropdownMenuOpen) => setWalletUI({ isDropdownMenuOpen }, { get, set }), walletUI: getWalletUI({ currentNetwork: currentChain.networkId, appMode }), setCardanoCoin: (chain: Wallet.Cardano.ChainId) => setWalletUI({ network: chain.networkId }, { get, set }), setNetworkConnection: (networkConnection: NetworkConnectionStates) => diff --git a/apps/browser-extension-wallet/src/stores/types.ts b/apps/browser-extension-wallet/src/stores/types.ts index 152c6b1f2..0236c7d96 100644 --- a/apps/browser-extension-wallet/src/stores/types.ts +++ b/apps/browser-extension-wallet/src/stores/types.ts @@ -136,6 +136,7 @@ export interface LockSlice { export interface UISlice { walletUI: WalletUI | undefined; + setIsDropdownMenuOpen: (isOpen: boolean) => void; setCardanoCoin: (chainId: Wallet.Cardano.ChainId) => void; setNetworkConnection: (networkConnection: NetworkConnectionStates) => void; setBalancesVisibility: (visible: boolean) => void; diff --git a/apps/browser-extension-wallet/src/types/ui.ts b/apps/browser-extension-wallet/src/types/ui.ts index 8cf068892..139260dff 100644 --- a/apps/browser-extension-wallet/src/types/ui.ts +++ b/apps/browser-extension-wallet/src/types/ui.ts @@ -15,6 +15,7 @@ export interface WalletUI { cardanoCoin: Wallet.CoinId; appMode: AppMode; networkConnection: NetworkConnectionStates; + isDropdownMenuOpen: boolean; areBalancesVisible: boolean; canManageBalancesVisibility: boolean; getHiddenBalancePlaceholder: (placeholderLength?: number, placeholderChar?: string) => string; diff --git a/apps/browser-extension-wallet/src/utils/mocks/store.tsx b/apps/browser-extension-wallet/src/utils/mocks/store.tsx index 5ccd61e8a..34c062e52 100644 --- a/apps/browser-extension-wallet/src/utils/mocks/store.tsx +++ b/apps/browser-extension-wallet/src/utils/mocks/store.tsx @@ -31,6 +31,7 @@ export const walletStoreMock = async ( // TODO: If possible use real methods/states and mock only needed ones, like inMemoryWallet [LW-5454] return { + setIsDropdownMenuOpen: jest.fn(), setManageAccountsWallet: jest.fn(), manageAccountsWallet: undefined, walletState: undefined, @@ -75,6 +76,7 @@ export const walletStoreMock = async ( setCardanoCoin: jest.fn(), setNetworkConnection: jest.fn(), walletUI: { + isDropdownMenuOpen: false, networkConnection: NetworkConnectionStates.CONNNECTED, cardanoCoin, appMode: APP_MODE_BROWSER, From 938f9b77503c7a95dd1cab372623f4ee4905a106 Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Thu, 22 Feb 2024 10:40:22 +0200 Subject: [PATCH 13/27] feat: disable enabling hw accounts in popup mode show tooltip that explains the behavior --- .../components/WalletAccounts.tsx | 14 ++++++- .../src/lib/translations/en.json | 6 ++- ...rofile-dropdown-account-item.component.tsx | 40 +++++++++++++++---- ...ofile-dropdown-accounts-list.component.tsx | 2 + 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx index 717538ca1..53ef62339 100644 --- a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx +++ b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx @@ -9,6 +9,7 @@ import { DisableAccountConfirmation, EditAccountDrawer, useAccountDataModal } fr import { useWalletStore } from '@src/stores'; import { useWalletManager } from '@hooks'; import { TOAST_DEFAULT_DURATION } from '@hooks/useActionExecution'; +import { WalletType } from '@cardano-sdk/web-extension'; const defaultAccountName = (accountNumber: number) => `Account #${accountNumber}`; @@ -26,6 +27,14 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack: } } = cardanoWallet; const { walletRepository, addAccount, activateWallet } = useWalletManager(); + const disableUnlock = useMemo( + () => + isPopup && + (wallet.type === WalletType.Ledger || wallet.type === WalletType.Trezor) && { + reason: t('multiWallet.popupHwAccountEnable') + }, + [isPopup, t, wallet.type] + ); const accountsData = useMemo( () => Array.from({ length: NUMBER_OF_ACCOUNTS_PER_WALLET }).map((_, accountNumber): AccountData => { @@ -34,10 +43,11 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack: isUnlocked: !!account, label: account ? account.metadata.name : defaultAccountName(accountNumber), accountNumber, - isActive: activeWalletId === wallet.walletId && activeAccount?.accountIndex === accountNumber + isActive: activeWalletId === wallet.walletId && activeAccount?.accountIndex === accountNumber, + disableUnlock }; }), - [wallet, activeAccount?.accountIndex, activeWalletId] + [wallet, activeAccount?.accountIndex, activeWalletId, disableUnlock] ); const activateAccount = useCallback( diff --git a/apps/browser-extension-wallet/src/lib/translations/en.json b/apps/browser-extension-wallet/src/lib/translations/en.json index b0371f02d..7f9fc0c01 100644 --- a/apps/browser-extension-wallet/src/lib/translations/en.json +++ b/apps/browser-extension-wallet/src/lib/translations/en.json @@ -792,7 +792,8 @@ "accounts": { "title": "Accounts", "description": "Select account to activate.", - "unlockLabel": "Enable" + "unlockLabel": "Enable", + "lockLabel": "Disable" }, "general": { "title": "Your keys", @@ -1411,6 +1412,7 @@ "activated": { "wallet": "Wallet \"{{walletName}}\" activated", "account": "Account \"{{accountName}}\" activated" - } + }, + "popupHwAccountEnable": "Hardware wallets require the expanded view to enable accounts" } } diff --git a/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.component.tsx b/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.component.tsx index 7fb115285..b352c79aa 100644 --- a/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.component.tsx +++ b/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-account-item.component.tsx @@ -1,7 +1,9 @@ +import type { ReactNode } from 'react'; import React from 'react'; import { ReactComponent as PencilIcon } from '@lace/icons/dist/PencilOutlineComponent'; import { ReactComponent as TrashIcon } from '@lace/icons/dist/TrashOutlineComponent'; +import { Tooltip } from 'antd'; import * as ControlButtons from '../../control-buttons'; import { Flex } from '../../flex'; @@ -14,6 +16,7 @@ export interface Props { accountNumber: number; label: string; unlockLabel: string; + disableUnlock?: { reason: string }; isUnlocked: boolean; isDeletable: boolean; onActivateClick?: (accountNumber: number) => void; @@ -22,8 +25,28 @@ export interface Props { onUnlockClick?: (accountNumber: number) => void; } +const MaybeWithDisableUnlockTooltip = ({ + disableUnlock, + children, +}: Readonly<{ + disableUnlock: Readonly; + children: ReactNode; +}>): JSX.Element => { + if (disableUnlock) { + return ( + + {children} + + ); + } + + return <>{children}; +}; + +// eslint-disable-next-line react/no-multi-comp export const AccountItem = ({ accountNumber, + disableUnlock, label, unlockLabel, isUnlocked, @@ -93,13 +116,16 @@ export const AccountItem = ({ /> ) : ( - { - onUnlockClick?.(accountNumber); - }} - /> + + { + onUnlockClick?.(accountNumber); + }} + /> + )} ); diff --git a/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-accounts-list.component.tsx b/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-accounts-list.component.tsx index 0af38a070..6e8b0b799 100644 --- a/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-accounts-list.component.tsx +++ b/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-accounts-list.component.tsx @@ -9,6 +9,7 @@ export interface AccountData { accountNumber: number; isUnlocked: boolean; isActive: boolean; + disableUnlock?: { reason: string }; } export interface Props { @@ -38,6 +39,7 @@ export const AccountsList = ({ accountNumber={a.accountNumber} isUnlocked={a.isUnlocked} label={a.label} + disableUnlock={a.disableUnlock} isDeletable={!a.isActive && hasMultipleUnlockedAccounts} unlockLabel={unlockLabel} onActivateClick={(accountNumber: number): void => { From ee1cbe3893fc5cbde2f9279de8dfbb5dc3cd6003 Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Thu, 22 Feb 2024 13:58:10 +0200 Subject: [PATCH 14/27] feat(ui): add colorScheme option to ExtraSmall also decouple button color scheme from size scheme and set minWidth for ExtraSmall button, which is currently only used for profile dropdown account item --- .../control-buttons/control-button.css.ts | 5 +++++ .../control-buttons/control-button.data.ts | 10 ++++++++++ .../control-buttons/extra-small-button.component.tsx | 11 ++++++----- .../ui/src/design-system/control-buttons/index.ts | 1 + 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/design-system/control-buttons/control-button.css.ts b/packages/ui/src/design-system/control-buttons/control-button.css.ts index 8d5a63e7c..506592f80 100644 --- a/packages/ui/src/design-system/control-buttons/control-button.css.ts +++ b/packages/ui/src/design-system/control-buttons/control-button.css.ts @@ -115,6 +115,7 @@ export const container = recipe({ paddingRight: vars.spacing.$24, }, [Scheme.ExtraSmall]: { + minWidth: vars.spacing.$60, height: vars.spacing.$24, paddingTop: vars.spacing.$2, paddingBottom: vars.spacing.$2, @@ -176,6 +177,10 @@ export const label = recipe({ }, [Scheme.ExtraSmall]: { color: vars.colors.$control_buttons_label_color_extra_small, + }, + }, + sizeScheme: { + [Scheme.ExtraSmall]: { fontWeight: vars.fontWeights.$regular, fontSize: vars.fontSizes.$12, }, diff --git a/packages/ui/src/design-system/control-buttons/control-button.data.ts b/packages/ui/src/design-system/control-buttons/control-button.data.ts index a01a97355..c46a69bf4 100644 --- a/packages/ui/src/design-system/control-buttons/control-button.data.ts +++ b/packages/ui/src/design-system/control-buttons/control-button.data.ts @@ -18,3 +18,13 @@ export type ControlButtonProps = Omit & { export type ControlButtonWithLabelProps = ControlButtonProps & { label?: string; }; + +export type ColorScheme = + | Scheme.Danger + | Scheme.ExtraSmall + | Scheme.Filled + | Scheme.Outlined; +export type ControlButtonWithLabelAndColorSchemeProps = + ControlButtonWithLabelProps & { + colorScheme?: ColorScheme; + }; diff --git a/packages/ui/src/design-system/control-buttons/extra-small-button.component.tsx b/packages/ui/src/design-system/control-buttons/extra-small-button.component.tsx index 8440a8a3e..144c558cb 100644 --- a/packages/ui/src/design-system/control-buttons/extra-small-button.component.tsx +++ b/packages/ui/src/design-system/control-buttons/extra-small-button.component.tsx @@ -7,10 +7,10 @@ import { SkeletonButton } from '../buttons'; import * as cx from './control-button.css'; import { Scheme } from './control-button.data'; -import type { ControlButtonWithLabelProps } from './control-button.data'; +import type { ControlButtonWithLabelAndColorSchemeProps } from './control-button.data'; export const ExtraSmall = ( - props: Readonly, + props: Readonly, ): JSX.Element => { return ( Date: Thu, 22 Feb 2024 14:09:34 +0200 Subject: [PATCH 15/27] feat: replace edit/delete buttons with a single Disable button temporary design until we apply a larger re-design --- .../components/WalletAccounts.tsx | 9 ++++- ...rofile-dropdown-account-item.component.tsx | 36 +++++++------------ ...ofile-dropdown-accounts-list.component.tsx | 13 ++++--- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx index 53ef62339..56c6f1a56 100644 --- a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx +++ b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx @@ -17,6 +17,13 @@ const NUMBER_OF_ACCOUNTS_PER_WALLET = 24; export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack: () => void }): React.ReactElement => { const { t } = useTranslation(); + const accountsListLabel = useMemo( + () => ({ + unlock: t('browserView.settings.wallet.accounts.unlockLabel'), + lock: t('browserView.settings.wallet.accounts.lockLabel') + }), + [t] + ); const editAccountDrawer = useAccountDataModal(); const disableAccountConfirmation = useAccountDataModal(); const { manageAccountsWallet: wallet, cardanoWallet, setIsDropdownMenuOpen } = useWalletStore(); @@ -129,7 +136,7 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack: data-testid="user-dropdown-wallet-account-list" > ): JSX.Element => ( @@ -83,7 +82,7 @@ export const AccountItem = ({ className={cx.accountLabel} data-testid="wallet-account-item-label" > - {label} + {label.name}
{isUnlocked ? ( - - } - size="extraSmall" - onClick={(): void => { - onEditClick?.(accountNumber); - }} - data-testid="wallet-account-item-edit-btn" - /> - } - size="extraSmall" - data-testid="wallet-account-item-delete-btn" - disabled={!isDeletable} + isDeletable ? ( + { onDeleteClick?.(accountNumber); }} /> - + ) : undefined ) : ( { diff --git a/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-accounts-list.component.tsx b/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-accounts-list.component.tsx index 6e8b0b799..ebf225fc4 100644 --- a/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-accounts-list.component.tsx +++ b/packages/ui/src/design-system/profile-dropdown/accounts/profile-dropdown-accounts-list.component.tsx @@ -14,7 +14,10 @@ export interface AccountData { export interface Props { accounts: AccountData[]; - unlockLabel: string; + label: { + lock: string; + unlock: string; + }; onAccountActivateClick?: (accountNumber: number) => void; onAccountUnlockClick?: (accountNumber: number) => void; onAccountEditClick?: (accountNumber: number) => void; @@ -23,7 +26,7 @@ export interface Props { export const AccountsList = ({ accounts, - unlockLabel, + label, onAccountActivateClick, onAccountUnlockClick, onAccountEditClick, @@ -38,10 +41,12 @@ export const AccountsList = ({ key={a.accountNumber} accountNumber={a.accountNumber} isUnlocked={a.isUnlocked} - label={a.label} + label={{ + name: a.label, + ...label, + }} disableUnlock={a.disableUnlock} isDeletable={!a.isActive && hasMultipleUnlockedAccounts} - unlockLabel={unlockLabel} onActivateClick={(accountNumber: number): void => { if (!a.isActive && a.isUnlocked) { onAccountActivateClick?.(accountNumber); From 8b43216c1d3d9c1c29cf77c23a72230fb80d204d Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Thu, 22 Feb 2024 14:20:35 +0200 Subject: [PATCH 16/27] feat(extension): activate account automatically after enabling it --- .../components/WalletAccounts.tsx | 10 +++++++-- .../src/hooks/useWalletManager.ts | 22 +++++++++++-------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx index 56c6f1a56..e41d8a590 100644 --- a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx +++ b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx @@ -87,13 +87,19 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack: const unlockAccount = useCallback( async (accountIndex: number) => { + const name = defaultAccountName(accountIndex); await addAccount({ wallet, accountIndex, - metadata: { name: defaultAccountName(accountIndex) } + metadata: { name } + }); + setIsDropdownMenuOpen(false); + toast.notify({ + duration: TOAST_DEFAULT_DURATION, + text: t('multiWallet.activated.account', { accountName: name }) }); }, - [wallet, addAccount] + [wallet, addAccount, t, setIsDropdownMenuOpen] ); const lockAccount = useCallback(async () => { diff --git a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts index 027c9c949..327b3465b 100644 --- a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts +++ b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts @@ -666,15 +666,19 @@ export const useWalletManager = (): UseWalletManager => { [walletLock, loadWallet] ); - const addAccount = async ({ wallet, accountIndex, metadata }: WalletManagerAddAccountProps): Promise => { - const extendedAccountPublicKey = await getExtendedAccountPublicKey(wallet, accountIndex); - await walletRepository.addAccount({ - accountIndex, - extendedAccountPublicKey, - metadata, - walletId: wallet.walletId - }); - }; + const addAccount = useCallback( + async ({ wallet, accountIndex, metadata }: WalletManagerAddAccountProps): Promise => { + const extendedAccountPublicKey = await getExtendedAccountPublicKey(wallet, accountIndex); + await walletRepository.addAccount({ + accountIndex, + extendedAccountPublicKey, + metadata, + walletId: wallet.walletId + }); + await walletManager.activate({ chainId: getCurrentChainId(), walletId: wallet.walletId, accountIndex }); + }, + [getCurrentChainId] + ); return { activateWallet, From 9365312f88d4ad9024ebc63983dcb28a2a93622f Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Thu, 22 Feb 2024 14:42:23 +0200 Subject: [PATCH 17/27] fix(extension): align wallet sync status label --- .../DropdownMenuOverlay.module.scss | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/DropdownMenuOverlay.module.scss b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/DropdownMenuOverlay.module.scss index 229388b19..e5e444b42 100644 --- a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/DropdownMenuOverlay.module.scss +++ b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/DropdownMenuOverlay.module.scss @@ -58,6 +58,7 @@ } .walletStatusInfo { + margin-left: 8px; cursor: default; display: flex; } @@ -73,13 +74,16 @@ border-radius: 12px !important; background-color: var(--bg-color-container, #ffffff) !important; color: var(--text-color-primary); + &:hover { background: var(--light-mode-light-grey, var(--dark-mode-mid-grey, #efefef)) !important; } + &.cta { cursor: pointer; } } + .menuItemTheme { :global { button.ant-switch { @@ -87,32 +91,34 @@ width: size_unit(5.5); } - .ant-switch > div.ant-switch-handle { + .ant-switch>div.ant-switch-handle { height: size_unit(2.5); width: size_unit(2.5); } - .ant-switch > span.ant-switch-inner svg { + .ant-switch>span.ant-switch-inner svg { margin-top: 1px; } - .ant-switch.ant-switch-checked > span.ant-switch-inner { + .ant-switch.ant-switch-checked>span.ant-switch-inner { margin: 0 size_unit(35) 0 size_unit(0.5); } - .ant-switch.ant-switch-checked > div.ant-switch-handle { + .ant-switch.ant-switch-checked>div.ant-switch-handle { left: calc(100% - 20px - 2px); } } display: flex; justify-content: space-between !important; + &:hover { background: transparent !important; } } } } + .separator { display: flex; height: 1.5px; @@ -141,12 +147,14 @@ display: flex; align-items: center; justify-content: center; + span { color: var(--dark-mode-mid-black, var(--text-color-white, #ffffff)); font-size: var(--body); text-transform: uppercase; font-weight: 700; } + .avatar { font-size: size_unit(4); cursor: pointer; @@ -156,6 +164,7 @@ height: 26px; width: 26px; } + .userAvatarImage { border-radius: 30px; } @@ -178,7 +187,8 @@ font-size: 16px; } -.popUpContainer, .extendedContainer { +.popUpContainer, +.extendedContainer { @include scroll-bar-style; overflow-y: scroll; margin: size_unit(1) size_unit(1) size_unit(1) 0; From 669a6afb9ee1f3751356a84e40fd280032807bb0 Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Thu, 22 Feb 2024 15:24:11 +0200 Subject: [PATCH 18/27] feat(extension): show toast when adding a duplicate wallet toast is a temporary solution, to be replaced --- .../src/lib/translations/en.json | 3 +- .../src/lib/wallet-api-ui.ts | 7 ++++- .../features/multi-wallet/MultiWallet.tsx | 28 +++++++++++++------ .../steps/RestoreRecoveryPhrase.tsx | 15 ++++++++-- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/apps/browser-extension-wallet/src/lib/translations/en.json b/apps/browser-extension-wallet/src/lib/translations/en.json index 7f9fc0c01..d9eb93166 100644 --- a/apps/browser-extension-wallet/src/lib/translations/en.json +++ b/apps/browser-extension-wallet/src/lib/translations/en.json @@ -1413,6 +1413,7 @@ "wallet": "Wallet \"{{walletName}}\" activated", "account": "Account \"{{accountName}}\" activated" }, - "popupHwAccountEnable": "Hardware wallets require the expanded view to enable accounts" + "popupHwAccountEnable": "Hardware wallets require the expanded view to enable accounts", + "walletAlreadyExists": "Wallet wasn't created because it already exists" } } diff --git a/apps/browser-extension-wallet/src/lib/wallet-api-ui.ts b/apps/browser-extension-wallet/src/lib/wallet-api-ui.ts index dd44c05b6..9138d804e 100644 --- a/apps/browser-extension-wallet/src/lib/wallet-api-ui.ts +++ b/apps/browser-extension-wallet/src/lib/wallet-api-ui.ts @@ -1,5 +1,6 @@ import { SigningCoordinator, + WalletConflictError, WalletRepositoryApi, WalletType, consumeRemoteApi, @@ -44,7 +45,11 @@ export const observableWallet = consumeRemoteApi( ); export const walletRepository = consumeRemoteApi>( - { baseChannel: repositoryChannel(process.env.WALLET_NAME), properties: walletRepositoryProperties }, + { + baseChannel: repositoryChannel(process.env.WALLET_NAME), + properties: walletRepositoryProperties, + errorTypes: [WalletConflictError] + }, { logger, runtime } ); diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/MultiWallet.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/MultiWallet.tsx index f55fe1b89..73a304898 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/MultiWallet.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/MultiWallet.tsx @@ -21,9 +21,12 @@ import { walletRoutePaths } from '@routes'; import { useWalletManager } from '@hooks'; import { Subject } from 'rxjs'; import { Wallet } from '@lace/cardano'; -import { NavigationButton } from '@lace/common'; +import { NavigationButton, toast } from '@lace/common'; import { useBackgroundPage } from '@providers/BackgroundPageProvider'; import { Providers } from './hardware-wallet/types'; +import { WalletConflictError } from '@cardano-sdk/web-extension'; +import { TOAST_DEFAULT_DURATION } from '@hooks/useActionExecution'; +import { useTranslation } from 'react-i18next'; const { newWallet } = walletRoutePaths; @@ -34,6 +37,7 @@ interface ConfirmationDialog { } export const SetupHardwareWallet = ({ shouldShowDialog$ }: ConfirmationDialog): JSX.Element => { + const { t } = useTranslation(); const { connectHardwareWallet, createHardwareWallet } = useWalletManager(); const disconnectHardwareWallet$ = useMemo(() => new Subject(), []); @@ -43,15 +47,23 @@ export const SetupHardwareWallet = ({ shouldShowDialog$ }: ConfirmationDialog): disconnectHardwareWallet$, shouldShowDialog$, createWallet: async ({ account, connection, model, name }) => { - await createHardwareWallet({ - connectedDevice: model, - deviceConnection: connection, - name, - accountIndex: account - }); + try { + await createHardwareWallet({ + connectedDevice: model, + deviceConnection: connection, + name, + accountIndex: account + }); + } catch (error) { + if (error instanceof WalletConflictError) { + toast.notify({ duration: TOAST_DEFAULT_DURATION, text: t('multiWallet.walletAlreadyExists') }); + } else { + throw error; + } + } } }), - [connectHardwareWallet, createHardwareWallet, disconnectHardwareWallet$, shouldShowDialog$] + [connectHardwareWallet, createHardwareWallet, disconnectHardwareWallet$, shouldShowDialog$, t] ); useEffect(() => { diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/restore-wallet/steps/RestoreRecoveryPhrase.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/restore-wallet/steps/RestoreRecoveryPhrase.tsx index c50d31d8d..30c8cd4f7 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/restore-wallet/steps/RestoreRecoveryPhrase.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/restore-wallet/steps/RestoreRecoveryPhrase.tsx @@ -8,6 +8,9 @@ import { useRestoreWallet } from '../context'; import { walletRoutePaths } from '@routes'; import noop from 'lodash/noop'; import { useWalletManager } from '@hooks'; +import { WalletConflictError } from '@cardano-sdk/web-extension'; +import { toast } from '@lace/common'; +import { TOAST_DEFAULT_DURATION } from '@hooks/useActionExecution'; const wordList = wordlists.english; @@ -49,10 +52,18 @@ export const RestoreRecoveryPhrase = (): JSX.Element => { history.goBack(); }} onSubmit={useCallback(async () => { - await createWallet(data); + try { + await createWallet(data); + } catch (error) { + if (error instanceof WalletConflictError) { + toast.notify({ duration: TOAST_DEFAULT_DURATION, text: t('multiWallet.walletAlreadyExists') }); + } else { + throw error; + } + } clearSecrets(); history.push(walletRoutePaths.assets); - }, [data, clearSecrets, createWallet, history])} + }, [data, clearSecrets, createWallet, history, t])} onStepNext={noop} isSubmitEnabled={isValidMnemonic} translations={walletSetupMnemonicStepTranslations} From 5a9100652ad752abe930d98979c3d5e67655a4fe Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Thu, 22 Feb 2024 18:07:05 +0200 Subject: [PATCH 19/27] fix(extension): show correct wallet name after activating wallet --- .../DropdownMenuOverlay/components/UserInfo.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx index fc5fe193f..4d92675bc 100644 --- a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx +++ b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx @@ -44,7 +44,7 @@ export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInf const walletAddress = walletInfo.addresses[0].address.toString(); const shortenedWalletAddress = addEllipsis(walletAddress, ADRESS_FIRST_PART_LENGTH, ADRESS_LAST_PART_LENGTH); const fullWalletName = cardanoWallet.source.wallet.metadata.name; - const walletName = addEllipsis(fullWalletName, WALLET_NAME_MAX_LENGTH, 0); + const activeWalletName = addEllipsis(fullWalletName, WALLET_NAME_MAX_LENGTH, 0); const [handle] = useGetHandles(); const handleName = handle?.nftMetadata?.name; @@ -84,7 +84,7 @@ export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInf key={wallet.walletId} title={wallet.metadata.name} subtitle={lastActiveAccount.metadata.name} - id={walletName} + id={`wallet-option-${wallet.walletId}`} onOpenAccountsMenu={() => onOpenWalletAccounts(wallet)} onClick={async () => { await activateWallet({ @@ -94,14 +94,14 @@ export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInf setIsDropdownMenuOpen(false); toast.notify({ duration: TOAST_DEFAULT_DURATION, - text: t('multiWallet.activated.wallet', { walletName }) + text: t('multiWallet.activated.wallet', { walletName: wallet.metadata.name }) }); }} type={getUiWalletType(wallet.type)} /> ); }, - [activateWallet, getLastActiveAccount, onOpenWalletAccounts, walletName, setIsDropdownMenuOpen, t] + [activateWallet, getLastActiveAccount, onOpenWalletAccounts, setIsDropdownMenuOpen, t] ); const renderWallet = useCallback( @@ -145,10 +145,10 @@ export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInf } >
- {avatarVisible && } + {avatarVisible && }

- {walletName} + {activeWalletName}

{handleName || shortenedWalletAddress} From 59481170532cc42e4ea1dc43757be44e038a8c39 Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Thu, 22 Feb 2024 18:31:34 +0200 Subject: [PATCH 20/27] fix(extension): show loader when activating a wallet or account --- .../hooks/__tests__/useWalletManager.test.tsx | 33 ++++++++++++++++--- .../src/hooks/useWalletManager.ts | 11 +++++-- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx b/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx index 40385141c..3700ce8ee 100644 --- a/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx +++ b/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx @@ -34,9 +34,11 @@ import * as localStorage from '@src/utils/local-storage'; import * as AppSettings from '@providers/AppSettings'; import * as walletApiUi from '@src/lib/wallet-api-ui'; import { of } from 'rxjs'; -import { AnyBip32Wallet, AnyWallet, WalletType } from '@cardano-sdk/web-extension'; +import { AnyBip32Wallet, AnyWallet, WalletManagerActivateProps, WalletType } from '@cardano-sdk/web-extension'; import { Wallet } from '@lace/cardano'; +(walletApiUi as any).logger = console; + jest.mock('@providers/AppSettings', () => ({ ...jest.requireActual('@providers/AppSettings'), useAppSettingsContext: mockUseAppSettingsContext @@ -727,18 +729,39 @@ describe('Testing useWalletManager hook', () => { }); describe('activateWallet', () => { - it('stores lastActiveAccountIndex in wallet metadata and activates wallet via WalletManager', async () => { - const walletId = 'walletId'; - const accountIndex = 1; - const originalMetadata = { name: 'wallet' }; + const walletId = 'walletId'; + const accountIndex = 1; + const originalMetadata = { name: 'wallet' }; + + beforeEach(() => { walletApiUi.walletRepository.wallets$ = of([ { walletId, metadata: originalMetadata } as AnyBip32Wallet ]); + walletApiUi.walletManager.deactivate = jest.fn().mockResolvedValueOnce(void 0); walletApiUi.walletRepository.updateWalletMetadata = jest.fn().mockResolvedValueOnce(void 0); walletApiUi.walletManager.activate = jest.fn().mockResolvedValueOnce(void 0); + walletApiUi.walletManager.deactivate = jest.fn().mockResolvedValueOnce(void 0); + }); + + it('does not re-activate an already active wallet', async () => { + walletApiUi.walletManager.activeWalletId$ = of({ walletId, accountIndex } as WalletManagerActivateProps< + any, + any + >); + + const { activateWallet } = render(); + await activateWallet({ walletId, accountIndex }); + + expect(walletApiUi.walletRepository.updateWalletMetadata).not.toBeCalled(); + expect(walletApiUi.walletManager.activate).not.toBeCalled(); + expect(walletApiUi.walletManager.deactivate).not.toBeCalled(); + }); + + it('stores lastActiveAccountIndex in wallet metadata and activates wallet via WalletManager', async () => { + walletApiUi.walletManager.activeWalletId$ = of({ walletId: 'otherId' } as WalletManagerActivateProps); const { activateWallet } = render(); await activateWallet({ walletId, accountIndex }); diff --git a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts index 327b3465b..1adf11d75 100644 --- a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts +++ b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts @@ -19,7 +19,7 @@ import { getWalletFromStorage } from '@src/utils/get-wallet-from-storage'; import { getUserIdService } from '@providers/AnalyticsProvider/getUserIdService'; import { ENHANCED_ANALYTICS_OPT_IN_STATUS_LS_KEY } from '@providers/AnalyticsProvider/config'; import { ILocalStorage } from '@src/types'; -import { firstValueFrom } from 'rxjs'; +import { combineLatest, firstValueFrom } from 'rxjs'; import { AddWalletProps, AnyBip32Wallet, @@ -513,7 +513,14 @@ export const useWalletManager = (): UseWalletManager => { const activateWallet = useCallback( async (props: ActivateWalletProps): Promise => { - const wallets = await firstValueFrom(walletRepository.wallets$); + const [wallets, activeWallet] = await firstValueFrom( + combineLatest([walletRepository.wallets$, walletManager.activeWalletId$]) + ); + if (activeWallet?.walletId === props.walletId && activeWallet?.accountIndex === props.accountIndex) { + logger.debug('Wallet is already active'); + return; + } + await walletManager.deactivate(); await walletRepository.updateWalletMetadata({ walletId: props.walletId, metadata: { From 1b679e4a303e0d2e88f0fff35b8b2ceba438a254 Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Fri, 23 Feb 2024 11:26:19 +0200 Subject: [PATCH 21/27] fix(extension): use cjs import for hardware-ledger --- packages/cardano/src/wallet/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cardano/src/wallet/types.ts b/packages/cardano/src/wallet/types.ts index 946b0e338..30db2be74 100644 --- a/packages/cardano/src/wallet/types.ts +++ b/packages/cardano/src/wallet/types.ts @@ -1,5 +1,5 @@ import { Cardano, Paginated } from '@cardano-sdk/core'; -import { LedgerKeyAgent } from '@cardano-sdk/hardware-ledger'; +import type { LedgerKeyAgent } from '@cardano-sdk/hardware-ledger'; import { WalletType } from '@cardano-sdk/web-extension'; export type DeviceConnection = LedgerKeyAgent['deviceConnection'] | boolean; From b0c01b023f26188ab160ea70bf9e7604c10998bf Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Fri, 23 Feb 2024 12:29:17 +0200 Subject: [PATCH 22/27] fix: remove all relative imports from cardano-sdk --- apps/browser-extension-wallet/src/stores/types.ts | 7 +++---- .../src/utils/__tests__/get-token-list.test.ts | 4 ++-- .../src/utils/__tests__/inspectTxType.test.ts | 4 ++-- .../src/views/browser-view/features/staking/helpers.ts | 4 ++-- .../views/browser-view/features/wallet-setup/helpers.ts | 4 ++-- packages/cardano/src/wallet/index.ts | 2 +- .../src/wallet/lib/__tests__/get-inputs-value.test.ts | 5 ++--- packages/cardano/src/wallet/lib/hardware-wallet.ts | 8 ++++---- .../src/wallet/test/mocks/NetworkInfoProviderStub.ts | 3 +-- 9 files changed, 19 insertions(+), 22 deletions(-) diff --git a/apps/browser-extension-wallet/src/stores/types.ts b/apps/browser-extension-wallet/src/stores/types.ts index 0236c7d96..c902f9f99 100644 --- a/apps/browser-extension-wallet/src/stores/types.ts +++ b/apps/browser-extension-wallet/src/stores/types.ts @@ -23,8 +23,7 @@ import { IAssetDetails } from '@src/views/browser-view/features/assets/types'; import { TokenInfo } from '@src/utils/get-assets-information'; import { AnyBip32Wallet, WalletManagerApi, WalletType } from '@cardano-sdk/web-extension'; import { AddressesDiscoveryStatus } from '@lib/communication/addresses-discoverer'; -import { Reward } from '@cardano-sdk/core'; -import { EpochNo } from '@cardano-sdk/core/dist/cjs/Cardano'; +import { Cardano, Reward } from '@cardano-sdk/core'; import { StakePoolSortOptions } from '@lace/staking'; import { ObservableWalletState } from '@hooks/useWalletState'; @@ -152,7 +151,7 @@ export interface ActivityDetailSlice { type: RewardsActivityType; status: ActivityStatus.SPENDABLE; direction?: never; - activity: { spendableEpoch: EpochNo; spendableDate: Date; rewards: Reward[] }; + activity: { spendableEpoch: Cardano.EpochNo; spendableDate: Date; rewards: Reward[] }; } | { type: TransactionActivityType; @@ -168,7 +167,7 @@ export interface ActivityDetailSlice { type: TransactionActivityType; }) => void; setRewardsActivityDetail: (params: { - activity: { spendableEpoch: EpochNo; spendableDate: Date; rewards: Reward[] }; + activity: { spendableEpoch: Cardano.EpochNo; spendableDate: Date; rewards: Reward[] }; }) => void; getActivityDetail: (params: { coinPrices: PriceResult; fiatCurrency: CurrencyInfo }) => Promise; resetActivityState: () => void; diff --git a/apps/browser-extension-wallet/src/utils/__tests__/get-token-list.test.ts b/apps/browser-extension-wallet/src/utils/__tests__/get-token-list.test.ts index 059936ca2..76f6b14f5 100644 --- a/apps/browser-extension-wallet/src/utils/__tests__/get-token-list.test.ts +++ b/apps/browser-extension-wallet/src/utils/__tests__/get-token-list.test.ts @@ -5,7 +5,7 @@ import { GetTokenListParams, getTokenList } from '../get-token-list'; import { mockAsset, mockNft } from '../mocks/test-helpers'; import { defaultCurrency } from '@providers/currency/constants'; import { Wallet } from '@lace/cardano'; -import { NftMetadata } from '@cardano-sdk/core/dist/cjs/Asset'; +import { Asset } from '@cardano-sdk/core'; import { PriceResult } from '@hooks'; const testEnvironment = 'Preprod'; @@ -27,7 +27,7 @@ describe('getTokensList', () => { Wallet.Cardano.AssetId('659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c43'), { ...mockNft, - nftMetadata: { name: undefined, image: undefined } as NftMetadata + nftMetadata: { name: undefined, image: undefined } as Asset.NftMetadata } ] ]), diff --git a/apps/browser-extension-wallet/src/utils/__tests__/inspectTxType.test.ts b/apps/browser-extension-wallet/src/utils/__tests__/inspectTxType.test.ts index 977db1d61..a5995ebcd 100644 --- a/apps/browser-extension-wallet/src/utils/__tests__/inspectTxType.test.ts +++ b/apps/browser-extension-wallet/src/utils/__tests__/inspectTxType.test.ts @@ -5,7 +5,7 @@ import { inspectTxType, getTxDirection } from '../tx-inspection'; import { buildMockTx } from '../mocks/tx'; import { Wallet } from '@lace/cardano'; import { TxDirections } from '@src/types'; -import { StakeDelegationCertificate } from '@cardano-sdk/core/dist/cjs/Cardano'; +import { Cardano } from '@cardano-sdk/core'; import { Hash28ByteBase16 } from '@cardano-sdk/crypto'; const ADDRESS_1 = Wallet.Cardano.PaymentAddress( 'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g' @@ -156,7 +156,7 @@ describe('testing tx-inspection utils', () => { test('should not return delegation in case pool id is missing', async () => { const delegationTX = buildMockTx({ - certificates: [{} as StakeDelegationCertificate] + certificates: [{} as Cardano.StakeDelegationCertificate] }); const walletAddresses = [ { address: ADDRESS_1, rewardAccount: REWARD_ACCOUNT } diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/staking/helpers.ts b/apps/browser-extension-wallet/src/views/browser-view/features/staking/helpers.ts index b851df5ec..666bcd18d 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/staking/helpers.ts +++ b/apps/browser-extension-wallet/src/views/browser-view/features/staking/helpers.ts @@ -1,7 +1,7 @@ /* eslint-disable no-magic-numbers */ import { Wallet } from '@lace/cardano'; -import * as HardwareLedger from '../../../../../../../node_modules/@cardano-sdk/hardware-ledger/dist/cjs'; -import * as HardwareTrezor from '../../../../../../../node_modules/@cardano-sdk/hardware-trezor/dist/cjs'; +import * as HardwareLedger from '@cardano-sdk/hardware-ledger'; +import * as HardwareTrezor from '@cardano-sdk/hardware-trezor'; import { WalletType } from '@cardano-sdk/web-extension'; export enum LedgerMultidelegationMinAppVersion { diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/helpers.ts b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/helpers.ts index 8671f9fb9..f7f7bfc1a 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/helpers.ts +++ b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/helpers.ts @@ -1,7 +1,7 @@ /* eslint-disable camelcase */ import { Wallet } from '@lace/cardano'; -import * as HardwareLedger from '../../../../../../../node_modules/@cardano-sdk/hardware-ledger/dist/cjs'; -import * as HardwareTrezor from '../../../../../../../node_modules/@cardano-sdk/hardware-trezor/dist/cjs'; +import * as HardwareLedger from '@cardano-sdk/hardware-ledger'; +import * as HardwareTrezor from '@cardano-sdk/hardware-trezor'; import { PostHogProperties } from '@providers/AnalyticsProvider/analyticsTracker'; import { WalletType } from '@cardano-sdk/web-extension'; diff --git a/packages/cardano/src/wallet/index.ts b/packages/cardano/src/wallet/index.ts index 98bb5e308..553e884d6 100644 --- a/packages/cardano/src/wallet/index.ts +++ b/packages/cardano/src/wallet/index.ts @@ -50,7 +50,7 @@ export { export * as KeyManagement from '@cardano-sdk/key-management'; export * as Ledger from '@cardano-sdk/hardware-ledger'; -export * as Trezor from '../../../../node_modules/@cardano-sdk/hardware-trezor/dist/cjs'; +export * as Trezor from '@cardano-sdk/hardware-trezor'; export { HexBlob, Percent, BigIntMath } from '@cardano-sdk/util'; export * as Crypto from '@cardano-sdk/crypto'; diff --git a/packages/cardano/src/wallet/lib/__tests__/get-inputs-value.test.ts b/packages/cardano/src/wallet/lib/__tests__/get-inputs-value.test.ts index 51a29509f..6522f4974 100644 --- a/packages/cardano/src/wallet/lib/__tests__/get-inputs-value.test.ts +++ b/packages/cardano/src/wallet/lib/__tests__/get-inputs-value.test.ts @@ -10,10 +10,9 @@ import { act } from 'react-dom/test-utils'; import { Cardano, ChainHistoryProvider } from '@cardano-sdk/core'; import { of } from 'rxjs'; import { ObservableWallet } from '@cardano-sdk/wallet'; -import { HydratedTx } from '@cardano-sdk/core/dist/cjs/Cardano'; -const getMockWalletProvider = (mock: HydratedTx) => ({ - transactionsByHashes: (_hashes: string[]) => new Promise((resolve) => resolve([mock])) +const getMockWalletProvider = (mock: Cardano.HydratedTx) => ({ + transactionsByHashes: (_hashes: string[]) => new Promise((resolve) => resolve([mock])) }); describe('Testing getTxInputsValueAndAddress function', () => { diff --git a/packages/cardano/src/wallet/lib/hardware-wallet.ts b/packages/cardano/src/wallet/lib/hardware-wallet.ts index 85d8e0b48..f1e8bf505 100644 --- a/packages/cardano/src/wallet/lib/hardware-wallet.ts +++ b/packages/cardano/src/wallet/lib/hardware-wallet.ts @@ -1,8 +1,8 @@ /* eslint-disable unicorn/no-null */ import * as KeyManagement from '@cardano-sdk/key-management'; import { DeviceConnection, HardwareWallets } from '../types'; -import * as HardwareLedger from '../../../../../node_modules/@cardano-sdk/hardware-ledger/dist/cjs'; -import { TrezorKeyAgent } from '../../../../../node_modules/@cardano-sdk/hardware-trezor/dist/cjs'; +import * as HardwareLedger from '@cardano-sdk/hardware-ledger'; +import * as HardwareTrezor from '@cardano-sdk/hardware-trezor'; import { WalletType } from '@cardano-sdk/web-extension'; // Using nodejs CML version to satisfy the tests requirements, but this gets replaced by webpack to the browser version in the build @@ -25,13 +25,13 @@ const connectDevices: Record Promise> = await HardwareLedger.LedgerKeyAgent.checkDeviceConnection(DEFAULT_COMMUNICATION_TYPE), ...(AVAILABLE_WALLETS.includes(WalletType.Trezor) && { [WalletType.Trezor]: async () => { - const isTrezorInitialized = await TrezorKeyAgent.initializeTrezorTransport({ + const isTrezorInitialized = await HardwareTrezor.TrezorKeyAgent.initializeTrezorTransport({ manifest, communicationType: DEFAULT_COMMUNICATION_TYPE }); // initializeTrezorTransport would still succeed even when device is not connected - await TrezorKeyAgent.checkDeviceConnection(DEFAULT_COMMUNICATION_TYPE); + await HardwareTrezor.TrezorKeyAgent.checkDeviceConnection(DEFAULT_COMMUNICATION_TYPE); return isTrezorInitialized; } diff --git a/packages/cardano/src/wallet/test/mocks/NetworkInfoProviderStub.ts b/packages/cardano/src/wallet/test/mocks/NetworkInfoProviderStub.ts index a4f029fa7..ae9f81065 100644 --- a/packages/cardano/src/wallet/test/mocks/NetworkInfoProviderStub.ts +++ b/packages/cardano/src/wallet/test/mocks/NetworkInfoProviderStub.ts @@ -2,7 +2,6 @@ /* eslint-disable max-len */ import { Cardano, NetworkInfoProvider, Seconds, EraSummary } from '@cardano-sdk/core'; import { testnetEraSummaries } from '@cardano-sdk/util-dev'; -import { ProtocolParameters } from '@cardano-sdk/core/dist/cjs/Cardano'; export const mockedNetworkInfo: { network: { @@ -49,7 +48,7 @@ const mockCurrentProtocolParameters = { poolDeposit: 500_000_000, protocolVersion: { major: 5, minor: 0 }, stakeKeyDeposit: 2_000_000 -} as ProtocolParameters; +} as Cardano.ProtocolParameters; const mockGenesisParameters: Cardano.CompactGenesis = { activeSlotsCoefficient: 0.05, From 44d7ffb6ebc92cbc45f1df1dbfe2bad67da2b4ad Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Fri, 23 Feb 2024 12:57:31 +0200 Subject: [PATCH 23/27] fix(extension): connect trezor device before exporting xpub onboarding, adding wallet, adding accounts and dapp transacations are now working --- apps/browser-extension-wallet/src/hooks/useWalletManager.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts index 1adf11d75..4ad18cf6a 100644 --- a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts +++ b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts @@ -106,6 +106,10 @@ const getHwExtendedAccountPublicKey = async ( accountIndex }); case WalletType.Trezor: + await Wallet.Trezor.TrezorKeyAgent.initializeTrezorTransport({ + manifest: Wallet.manifest, + communicationType: Wallet.KeyManagement.CommunicationType.Web + }); return Wallet.Trezor.TrezorKeyAgent.getXpub({ communicationType: Wallet.KeyManagement.CommunicationType.Web, accountIndex From d899298926eb6ac90736adecef6ae1dcf60682cd Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Fri, 23 Feb 2024 14:22:36 +0200 Subject: [PATCH 24/27] chore: bump sdk packages required for correctly bundling trezor --- apps/browser-extension-wallet/package.json | 14 +- packages/cardano/package.json | 16 +- packages/staking/package.json | 14 +- yarn.lock | 208 ++++++++++----------- 4 files changed, 126 insertions(+), 126 deletions(-) diff --git a/apps/browser-extension-wallet/package.json b/apps/browser-extension-wallet/package.json index a726ff90a..4f96dccf2 100644 --- a/apps/browser-extension-wallet/package.json +++ b/apps/browser-extension-wallet/package.json @@ -40,14 +40,14 @@ }, "dependencies": { "@ant-design/icons": "^4.7.0", - "@cardano-sdk/cardano-services-client": "0.17.6", - "@cardano-sdk/core": "0.28.2", - "@cardano-sdk/dapp-connector": "0.12.9", - "@cardano-sdk/input-selection": "0.12.20", - "@cardano-sdk/tx-construction": "0.17.10", + "@cardano-sdk/cardano-services-client": "0.17.7", + "@cardano-sdk/core": "0.28.3", + "@cardano-sdk/dapp-connector": "0.12.10", + "@cardano-sdk/input-selection": "0.12.21", + "@cardano-sdk/tx-construction": "0.17.11", "@cardano-sdk/util": "0.15.0", - "@cardano-sdk/wallet": "0.34.2", - "@cardano-sdk/web-extension": "0.24.5", + "@cardano-sdk/wallet": "0.34.3", + "@cardano-sdk/web-extension": "0.24.6", "@emurgo/cip14-js": "~3.0.1", "@koralabs/handles-public-api-interfaces": "^1.6.6", "@lace/cardano": "0.1.0", diff --git a/packages/cardano/package.json b/packages/cardano/package.json index 6c052bfbf..9d37521db 100644 --- a/packages/cardano/package.json +++ b/packages/cardano/package.json @@ -39,15 +39,15 @@ "watch": "yarn build --watch" }, "dependencies": { - "@cardano-sdk/cardano-services-client": "0.17.6", - "@cardano-sdk/core": "0.28.2", + "@cardano-sdk/cardano-services-client": "0.17.7", + "@cardano-sdk/core": "0.28.3", "@cardano-sdk/crypto": "0.1.21", - "@cardano-sdk/hardware-ledger": "0.8.13", - "@cardano-sdk/hardware-trezor": "0.4.13", - "@cardano-sdk/key-management": "0.19.7", + "@cardano-sdk/hardware-ledger": "0.8.14", + "@cardano-sdk/hardware-trezor": "0.4.14", + "@cardano-sdk/key-management": "0.19.8", "@cardano-sdk/util": "0.15.0", - "@cardano-sdk/wallet": "0.34.2", - "@cardano-sdk/web-extension": "0.24.5", + "@cardano-sdk/wallet": "0.34.3", + "@cardano-sdk/web-extension": "0.24.6", "@lace/common": "0.1.0", "@stablelib/chacha20poly1305": "1.0.1", "bignumber.js": "9.0.1", @@ -67,7 +67,7 @@ "webextension-polyfill": "0.8.0" }, "devDependencies": { - "@cardano-sdk/util-dev": "0.19.13", + "@cardano-sdk/util-dev": "0.19.14", "@emurgo/cardano-message-signing-browser": "1.0.1", "rollup-plugin-polyfill-node": "^0.8.0", "typescript": "^4.3.5" diff --git a/packages/staking/package.json b/packages/staking/package.json index 3050cddfc..11563bbc5 100644 --- a/packages/staking/package.json +++ b/packages/staking/package.json @@ -72,9 +72,9 @@ }, "devDependencies": { "@babel/core": "^7.21.0", - "@cardano-sdk/core": "0.28.2", - "@cardano-sdk/input-selection": "0.12.20", - "@cardano-sdk/tx-construction": "0.17.10", + "@cardano-sdk/core": "0.28.3", + "@cardano-sdk/input-selection": "0.12.21", + "@cardano-sdk/tx-construction": "0.17.11", "@cardano-sdk/util": "0.15.0", "@storybook/addon-actions": "^7.6.7", "@storybook/addon-essentials": "^7.6.7", @@ -118,11 +118,11 @@ "wait-on": "^7.0.1" }, "peerDependencies": { - "@cardano-sdk/input-selection": "0.12.20", - "@cardano-sdk/tx-construction": "0.17.10", + "@cardano-sdk/input-selection": "0.12.21", + "@cardano-sdk/tx-construction": "0.17.11", "@cardano-sdk/util": "0.15.0", - "@cardano-sdk/wallet": "0.34.2", - "@cardano-sdk/web-extension": "0.24.5", + "@cardano-sdk/wallet": "0.34.3", + "@cardano-sdk/web-extension": "0.24.6", "@lace/cardano": "^0.1.0", "@lace/common": "^0.1.0", "@lace/core": "0.1.0", diff --git a/yarn.lock b/yarn.lock index 9b9baeb4d..0ba42e97d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7690,23 +7690,23 @@ __metadata: languageName: node linkType: hard -"@cardano-sdk/cardano-services-client@npm:0.17.6": - version: 0.17.6 - resolution: "@cardano-sdk/cardano-services-client@npm:0.17.6" +"@cardano-sdk/cardano-services-client@npm:0.17.7": + version: 0.17.7 + resolution: "@cardano-sdk/cardano-services-client@npm:0.17.7" dependencies: - "@cardano-sdk/core": ~0.28.2 + "@cardano-sdk/core": ~0.28.3 "@cardano-sdk/util": ~0.15.0 axios: ^0.27.2 class-validator: ^0.14.0 json-bigint: ~1.0.0 ts-log: ^2.2.4 - checksum: 2d792f5795a992a8c6c90347a759eafa244a41a4864d4addfd2a254acccf31a87ea6092869cf2b9c9037708727f0b9cee1fba74d8d7ce5cc69bc6e5d81b526d9 + checksum: e3001eb8ddb0f39d959980bc6a56c5dad1407072af3cfae98a46b5c623cbfb9f9fca663b0e3f1f2d6626ea6f69f19da4d87154365790421f19fb98b478ad2983 languageName: node linkType: hard -"@cardano-sdk/core@npm:0.28.2, @cardano-sdk/core@npm:~0.28.2": - version: 0.28.2 - resolution: "@cardano-sdk/core@npm:0.28.2" +"@cardano-sdk/core@npm:0.28.3, @cardano-sdk/core@npm:~0.28.3": + version: 0.28.3 + resolution: "@cardano-sdk/core@npm:0.28.3" dependencies: "@cardano-ogmios/client": 5.6.0 "@cardano-ogmios/schema": 5.6.0 @@ -7725,7 +7725,7 @@ __metadata: peerDependenciesMeta: rxjs: optional: true - checksum: ac041d6e787a555af2b97cfb05354759c96a917912b1e2f32611a894a212eb3772780c4ca7fdbeab747f50922a560c0c23d779720b7de6147ea4f15660b80dad + checksum: 0084fb65e6b6154f4c6d6cd3470ef5574848d00a1e9243495c09c50109ba745b0ab2f2b99a798cab4155de805279eca2a06abdb8c9b702c64b862a9a399e9207 languageName: node linkType: hard @@ -7757,78 +7757,78 @@ __metadata: languageName: node linkType: hard -"@cardano-sdk/dapp-connector@npm:0.12.9, @cardano-sdk/dapp-connector@npm:~0.12.9": - version: 0.12.9 - resolution: "@cardano-sdk/dapp-connector@npm:0.12.9" +"@cardano-sdk/dapp-connector@npm:0.12.10, @cardano-sdk/dapp-connector@npm:~0.12.10": + version: 0.12.10 + resolution: "@cardano-sdk/dapp-connector@npm:0.12.10" dependencies: - "@cardano-sdk/core": ~0.28.2 + "@cardano-sdk/core": ~0.28.3 "@cardano-sdk/crypto": ~0.1.21 "@cardano-sdk/util": ~0.15.0 ts-custom-error: ^3.2.0 ts-log: ^2.2.4 webextension-polyfill: ^0.8.0 - checksum: 44696681af0d0d6f3fc73cfc4ff78ca5078e95da870ec482e598b94d408e365b369148d06beaca27de223c2df049a60ef6ea6111ab4f65163a647af7e4cdb756 + checksum: 952fef65dd422c5f8b8e5f2f879a2f4620391137b8e6926f50fb01b39736e7a5810993075eac984112c97bbfb66268c6836d83b89b198478d73e2622b394305e languageName: node linkType: hard -"@cardano-sdk/hardware-ledger@npm:0.8.13, @cardano-sdk/hardware-ledger@npm:~0.8.13": - version: 0.8.13 - resolution: "@cardano-sdk/hardware-ledger@npm:0.8.13" +"@cardano-sdk/hardware-ledger@npm:0.8.14, @cardano-sdk/hardware-ledger@npm:~0.8.14": + version: 0.8.14 + resolution: "@cardano-sdk/hardware-ledger@npm:0.8.14" dependencies: "@cardano-foundation/ledgerjs-hw-app-cardano": ^6.0.0 - "@cardano-sdk/core": ~0.28.2 + "@cardano-sdk/core": ~0.28.3 "@cardano-sdk/crypto": ~0.1.21 - "@cardano-sdk/key-management": ~0.19.7 - "@cardano-sdk/tx-construction": ~0.17.10 + "@cardano-sdk/key-management": ~0.19.8 + "@cardano-sdk/tx-construction": ~0.17.11 "@cardano-sdk/util": ~0.15.0 "@ledgerhq/hw-transport": ^6.28.1 "@ledgerhq/hw-transport-node-hid-noevents": ^6.27.12 "@ledgerhq/hw-transport-webhid": ^6.27.12 ts-custom-error: ^3.2.0 ts-log: ^2.2.4 - checksum: 23e42bc2440b8bc547bb2dc954e6edc44340d4da66acf6a07a8ec844209351e074efde1f2c6884fe21d3e5b38a77e55a68da5bd2f575fd9f410ba4de99c6596f + checksum: c80a870fb58a69b35ba058441317366dd1fd4947fb5a52df233ce02eaf711cb8ca81f9bbc81bc2135b38f3a593b703220a8d097d92e50cf1c122b55956ba55fa languageName: node linkType: hard -"@cardano-sdk/hardware-trezor@npm:0.4.13, @cardano-sdk/hardware-trezor@npm:~0.4.13": - version: 0.4.13 - resolution: "@cardano-sdk/hardware-trezor@npm:0.4.13" +"@cardano-sdk/hardware-trezor@npm:0.4.14, @cardano-sdk/hardware-trezor@npm:~0.4.14": + version: 0.4.14 + resolution: "@cardano-sdk/hardware-trezor@npm:0.4.14" dependencies: - "@cardano-sdk/core": ~0.28.2 + "@cardano-sdk/core": ~0.28.3 "@cardano-sdk/crypto": ~0.1.21 - "@cardano-sdk/key-management": ~0.19.7 - "@cardano-sdk/tx-construction": ~0.17.10 + "@cardano-sdk/key-management": ~0.19.8 + "@cardano-sdk/tx-construction": ~0.17.11 "@cardano-sdk/util": ~0.15.0 "@trezor/connect": 9.1.6 "@trezor/connect-web": 9.1.6 lodash: ^4.17.21 ts-custom-error: ^3.2.0 ts-log: ^2.2.4 - checksum: 418e678b2cfad51fda0267fabcda4611a004669e1eb2dd203b7838c9b1e7927a033e04dc53fe5b367ffcc64ff7556cdd7e5988833824e476472190011d69e987 + checksum: e23d729fb435d4f87142055acf11ed03ee4743d6f59d00dad6d948cc1a269ac98c9baede9614f449e7bc0e99c5c0c060d60ec073bf7e29ba69ded6e44f88216b languageName: node linkType: hard -"@cardano-sdk/input-selection@npm:0.12.20, @cardano-sdk/input-selection@npm:~0.12.20": - version: 0.12.20 - resolution: "@cardano-sdk/input-selection@npm:0.12.20" +"@cardano-sdk/input-selection@npm:0.12.21, @cardano-sdk/input-selection@npm:~0.12.21": + version: 0.12.21 + resolution: "@cardano-sdk/input-selection@npm:0.12.21" dependencies: - "@cardano-sdk/core": ~0.28.2 - "@cardano-sdk/key-management": ~0.19.7 + "@cardano-sdk/core": ~0.28.3 + "@cardano-sdk/key-management": ~0.19.8 "@cardano-sdk/util": ~0.15.0 bignumber.js: ^9.1.1 lodash: ^4.17.21 ts-custom-error: ^3.2.0 - checksum: 6166083f49d3ed614fce418f7e280686d46726047dc5c33dc121cf9fd2a9d04514994103e79b16ae042434e139820fe974eb408f86d3eab6382a7b16e94c5f4e + checksum: 23f9708ef4f31d62cc0b0245359e576300337103cf9dcec51517a44d9160abaca56eae09942e2cddd0f0f9edc959986c97e3434e27583e19655ef95f74827708 languageName: node linkType: hard -"@cardano-sdk/key-management@npm:0.19.7, @cardano-sdk/key-management@npm:~0.19.7": - version: 0.19.7 - resolution: "@cardano-sdk/key-management@npm:0.19.7" +"@cardano-sdk/key-management@npm:0.19.8, @cardano-sdk/key-management@npm:~0.19.8": + version: 0.19.8 + resolution: "@cardano-sdk/key-management@npm:0.19.8" dependencies: - "@cardano-sdk/core": ~0.28.2 + "@cardano-sdk/core": ~0.28.3 "@cardano-sdk/crypto": ~0.1.21 - "@cardano-sdk/dapp-connector": ~0.12.9 + "@cardano-sdk/dapp-connector": ~0.12.10 "@cardano-sdk/util": ~0.15.0 "@emurgo/cardano-message-signing-nodejs": ^1.0.1 bip39: ^3.0.4 @@ -7839,36 +7839,36 @@ __metadata: rxjs: ^7.4.0 ts-custom-error: ^3.2.0 ts-log: ^2.2.4 - checksum: a52d2f369ec062f79b1243ea5762f43e2e16e6db4e5c5a554cd254814b228ba0d54cc5389d4151f70edacb2c67baa9feb57a9df5b5adf0a4289d69d28f8cca39 + checksum: 824327cf6fe2a4a59d3c351b30d24201db339dbc04fc7fb2c1d1d0c7c8dea1f3b82ce1b49c98f4b0ab69d5906242a47296754cf5a8bc06808212fad531edca3a languageName: node linkType: hard -"@cardano-sdk/tx-construction@npm:0.17.10, @cardano-sdk/tx-construction@npm:~0.17.10": - version: 0.17.10 - resolution: "@cardano-sdk/tx-construction@npm:0.17.10" +"@cardano-sdk/tx-construction@npm:0.17.11, @cardano-sdk/tx-construction@npm:~0.17.11": + version: 0.17.11 + resolution: "@cardano-sdk/tx-construction@npm:0.17.11" dependencies: - "@cardano-sdk/core": ~0.28.2 + "@cardano-sdk/core": ~0.28.3 "@cardano-sdk/crypto": ~0.1.21 - "@cardano-sdk/input-selection": ~0.12.20 - "@cardano-sdk/key-management": ~0.19.7 + "@cardano-sdk/input-selection": ~0.12.21 + "@cardano-sdk/key-management": ~0.19.8 "@cardano-sdk/util": ~0.15.0 - "@cardano-sdk/util-rxjs": ~0.7.4 + "@cardano-sdk/util-rxjs": ~0.7.5 lodash: ^4.17.21 npm: ^9.3.0 rxjs: ^7.4.0 ts-custom-error: ^3.2.0 ts-log: ^2.2.4 - checksum: 2cfa467a6927bb1fdf9b04d45e37579e282606fe98819a663e17d5af668f18619ef2d9fa2a0255448e27bfb246fb1f27999a4d1ab976bd05c142d9788bde5250 + checksum: d24fe6fa3688047a7998786d79087d80bf120449df2da1297909742e706df123f359b40360531851191656600e66b3c2eaae17fdaee44d588fd234f9634d7177 languageName: node linkType: hard -"@cardano-sdk/util-dev@npm:0.19.13": - version: 0.19.13 - resolution: "@cardano-sdk/util-dev@npm:0.19.13" +"@cardano-sdk/util-dev@npm:0.19.14": + version: 0.19.14 + resolution: "@cardano-sdk/util-dev@npm:0.19.14" dependencies: - "@cardano-sdk/core": ~0.28.2 + "@cardano-sdk/core": ~0.28.3 "@cardano-sdk/crypto": ~0.1.21 - "@cardano-sdk/key-management": ~0.19.7 + "@cardano-sdk/key-management": ~0.19.8 "@cardano-sdk/util": ~0.15.0 "@types/dockerode": ^3.3.8 axios: ^0.27.2 @@ -7881,18 +7881,18 @@ __metadata: lodash: ^4.17.21 rxjs: ^7.4.0 ts-log: ^2.2.4 - checksum: 2275631cd616a8d42dffc544f53f1e8c50687c2f1b7a8a69fdbee5e658cf5d23835bfd188bba0f171a9ebab8b6fcabc72f0fbd24a48de2ea5907f51040bb727b + checksum: 52dcd466306cde60adedb798c82fb910c9d2a82043c39fe912b80ed5318fa07afdb200d13954ba92c9409ff719f1ec38438e01fa5c5d5acde42fdb4330b7a08d languageName: node linkType: hard -"@cardano-sdk/util-rxjs@npm:~0.7.4": - version: 0.7.4 - resolution: "@cardano-sdk/util-rxjs@npm:0.7.4" +"@cardano-sdk/util-rxjs@npm:~0.7.5": + version: 0.7.5 + resolution: "@cardano-sdk/util-rxjs@npm:0.7.5" dependencies: "@cardano-sdk/util": ~0.15.0 backoff-rxjs: ^7.0.0 rxjs: ^7.4.0 - checksum: fe18e9fd9922bece68ca6aec079418bf6cba7c23f8c001b90566ef848109f88caced0e6feb534357809e8fe8f73dfc6b012f88a4353bcaa6859af20edf9bbafd + checksum: a965f1a83be9052ebb484a836c1a5383b3268ca92d04e39193f31904642d2a33a19d0ab58c1ed742f0c728a409e7da1a158af52571676b4154f4d5156dd5c11d languageName: node linkType: hard @@ -7910,20 +7910,20 @@ __metadata: languageName: node linkType: hard -"@cardano-sdk/wallet@npm:0.34.2, @cardano-sdk/wallet@npm:~0.34.2": - version: 0.34.2 - resolution: "@cardano-sdk/wallet@npm:0.34.2" +"@cardano-sdk/wallet@npm:0.34.3, @cardano-sdk/wallet@npm:~0.34.3": + version: 0.34.3 + resolution: "@cardano-sdk/wallet@npm:0.34.3" dependencies: - "@cardano-sdk/core": ~0.28.2 + "@cardano-sdk/core": ~0.28.3 "@cardano-sdk/crypto": ~0.1.21 - "@cardano-sdk/dapp-connector": ~0.12.9 - "@cardano-sdk/hardware-ledger": ~0.8.13 - "@cardano-sdk/hardware-trezor": ~0.4.13 - "@cardano-sdk/input-selection": ~0.12.20 - "@cardano-sdk/key-management": ~0.19.7 - "@cardano-sdk/tx-construction": ~0.17.10 + "@cardano-sdk/dapp-connector": ~0.12.10 + "@cardano-sdk/hardware-ledger": ~0.8.14 + "@cardano-sdk/hardware-trezor": ~0.4.14 + "@cardano-sdk/input-selection": ~0.12.21 + "@cardano-sdk/key-management": ~0.19.8 + "@cardano-sdk/tx-construction": ~0.17.11 "@cardano-sdk/util": ~0.15.0 - "@cardano-sdk/util-rxjs": ~0.7.4 + "@cardano-sdk/util-rxjs": ~0.7.5 backoff-rxjs: ^7.0.0 bignumber.js: ^9.1.1 delay: ^5.0.0 @@ -7933,24 +7933,24 @@ __metadata: rxjs: ^7.4.0 ts-custom-error: ^3.2.0 ts-log: ^2.2.3 - checksum: ff39322df7c6d37e481de0eca6f2d40d64bdc22f4123dd695aae0f796d37c1710248be55d8af34759078ad34a980900757173a0091d05bf7c9329c93cb3fe8f9 + checksum: 71a383767c8636e7abdc6edd1668feb44b1e69397bf58f000f27b86fc94c781238f9915594df3454988f6189717fd2211baafd7986fa4e9b2f5a07f1fa75846e languageName: node linkType: hard -"@cardano-sdk/web-extension@npm:0.24.5": - version: 0.24.5 - resolution: "@cardano-sdk/web-extension@npm:0.24.5" +"@cardano-sdk/web-extension@npm:0.24.6": + version: 0.24.6 + resolution: "@cardano-sdk/web-extension@npm:0.24.6" dependencies: - "@cardano-sdk/core": ~0.28.2 + "@cardano-sdk/core": ~0.28.3 "@cardano-sdk/crypto": ~0.1.21 - "@cardano-sdk/dapp-connector": ~0.12.9 - "@cardano-sdk/hardware-ledger": ~0.8.13 - "@cardano-sdk/hardware-trezor": ~0.4.13 - "@cardano-sdk/key-management": ~0.19.7 - "@cardano-sdk/tx-construction": ~0.17.10 + "@cardano-sdk/dapp-connector": ~0.12.10 + "@cardano-sdk/hardware-ledger": ~0.8.14 + "@cardano-sdk/hardware-trezor": ~0.4.14 + "@cardano-sdk/key-management": ~0.19.8 + "@cardano-sdk/tx-construction": ~0.17.11 "@cardano-sdk/util": ~0.15.0 - "@cardano-sdk/util-rxjs": ~0.7.4 - "@cardano-sdk/wallet": ~0.34.2 + "@cardano-sdk/util-rxjs": ~0.7.5 + "@cardano-sdk/wallet": ~0.34.3 backoff-rxjs: ^7.0.0 lodash: ^4.17.21 rxjs: ^7.4.0 @@ -7958,7 +7958,7 @@ __metadata: ts-log: ^2.2.3 uuid: ^8.3.2 webextension-polyfill: ^0.8.0 - checksum: 27ccd4d6fa01addd7b06329762af4aceebe647e5929f3071a5037e68837303cf1f9c5856927299ea13737c1373759a6d0c672f445be9d67c36c5e95191678327 + checksum: 98273ccc3f0a20c46ddcc31c25ac35b94642c55867bb718b51f313a1cdf0bd52e9ef8bfbd3dd8eb13298984677bb2062e75023838d19b1548374910d42dd9394 languageName: node linkType: hard @@ -10592,14 +10592,14 @@ __metadata: resolution: "@lace/browser-extension-wallet@workspace:apps/browser-extension-wallet" dependencies: "@ant-design/icons": ^4.7.0 - "@cardano-sdk/cardano-services-client": 0.17.6 - "@cardano-sdk/core": 0.28.2 - "@cardano-sdk/dapp-connector": 0.12.9 - "@cardano-sdk/input-selection": 0.12.20 - "@cardano-sdk/tx-construction": 0.17.10 + "@cardano-sdk/cardano-services-client": 0.17.7 + "@cardano-sdk/core": 0.28.3 + "@cardano-sdk/dapp-connector": 0.12.10 + "@cardano-sdk/input-selection": 0.12.21 + "@cardano-sdk/tx-construction": 0.17.11 "@cardano-sdk/util": 0.15.0 - "@cardano-sdk/wallet": 0.34.2 - "@cardano-sdk/web-extension": 0.24.5 + "@cardano-sdk/wallet": 0.34.3 + "@cardano-sdk/web-extension": 0.24.6 "@emurgo/cardano-message-signing-asmjs": 1.0.1 "@emurgo/cip14-js": ~3.0.1 "@koralabs/handles-public-api-interfaces": ^1.6.6 @@ -10662,16 +10662,16 @@ __metadata: version: 0.0.0-use.local resolution: "@lace/cardano@workspace:packages/cardano" dependencies: - "@cardano-sdk/cardano-services-client": 0.17.6 - "@cardano-sdk/core": 0.28.2 + "@cardano-sdk/cardano-services-client": 0.17.7 + "@cardano-sdk/core": 0.28.3 "@cardano-sdk/crypto": 0.1.21 - "@cardano-sdk/hardware-ledger": 0.8.13 - "@cardano-sdk/hardware-trezor": 0.4.13 - "@cardano-sdk/key-management": 0.19.7 + "@cardano-sdk/hardware-ledger": 0.8.14 + "@cardano-sdk/hardware-trezor": 0.4.14 + "@cardano-sdk/key-management": 0.19.8 "@cardano-sdk/util": 0.15.0 - "@cardano-sdk/util-dev": 0.19.13 - "@cardano-sdk/wallet": 0.34.2 - "@cardano-sdk/web-extension": 0.24.5 + "@cardano-sdk/util-dev": 0.19.14 + "@cardano-sdk/wallet": 0.34.3 + "@cardano-sdk/web-extension": 0.24.6 "@emurgo/cardano-message-signing-browser": 1.0.1 "@lace/common": 0.1.0 "@stablelib/chacha20poly1305": 1.0.1 @@ -10831,9 +10831,9 @@ __metadata: dependencies: "@ant-design/icons": ^4.7.0 "@babel/core": ^7.21.0 - "@cardano-sdk/core": 0.28.2 - "@cardano-sdk/input-selection": 0.12.20 - "@cardano-sdk/tx-construction": 0.17.10 + "@cardano-sdk/core": 0.28.3 + "@cardano-sdk/input-selection": 0.12.21 + "@cardano-sdk/tx-construction": 0.17.11 "@cardano-sdk/util": 0.15.0 "@lace/cardano": ^0.1.0 "@lace/common": ^0.1.0 @@ -10895,11 +10895,11 @@ __metadata: wait-on: ^7.0.1 zustand: ^4.4.1 peerDependencies: - "@cardano-sdk/input-selection": 0.12.20 - "@cardano-sdk/tx-construction": 0.17.10 + "@cardano-sdk/input-selection": 0.12.21 + "@cardano-sdk/tx-construction": 0.17.11 "@cardano-sdk/util": 0.15.0 - "@cardano-sdk/wallet": 0.34.2 - "@cardano-sdk/web-extension": 0.24.5 + "@cardano-sdk/wallet": 0.34.3 + "@cardano-sdk/web-extension": 0.24.6 "@lace/cardano": ^0.1.0 "@lace/common": ^0.1.0 "@lace/core": 0.1.0 From c22f8c299ec99bca93c913d9021611b296044c2a Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Fri, 23 Feb 2024 14:23:23 +0200 Subject: [PATCH 25/27] chore: build service worker script in development mode after removing imports from dist/cjs, service worker no longer loads this is a temporary solution --- apps/browser-extension-wallet/webpack.sw.prod.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/browser-extension-wallet/webpack.sw.prod.js b/apps/browser-extension-wallet/webpack.sw.prod.js index 1da5ef3b3..2b0a4d842 100644 --- a/apps/browser-extension-wallet/webpack.sw.prod.js +++ b/apps/browser-extension-wallet/webpack.sw.prod.js @@ -13,5 +13,7 @@ module.exports = () => prodConfig(), swConfig(), // Needed for the service worker to work with a production build - { optimization: { moduleIds: 'named', mangleExports: false } } + // TODO: after removing imports from dist/cjs, service worker no longer loads when built in production mode. + // It is likely that some optimization is triggering it, such as tree shaking. + { mode: 'development', optimization: { moduleIds: 'named', mangleExports: false } } ); From e4ed06606b9a6ed42c94246a1476aae8182ac792 Mon Sep 17 00:00:00 2001 From: Martynas Kazlauskas Date: Fri, 23 Feb 2024 16:35:50 +0200 Subject: [PATCH 26/27] fix(extension): display loader in popup view while switching wallet/account --- apps/browser-extension-wallet/src/routes/PopupView.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/browser-extension-wallet/src/routes/PopupView.tsx b/apps/browser-extension-wallet/src/routes/PopupView.tsx index a49351263..570b47055 100644 --- a/apps/browser-extension-wallet/src/routes/PopupView.tsx +++ b/apps/browser-extension-wallet/src/routes/PopupView.tsx @@ -30,6 +30,7 @@ export const PopupView = (): React.ReactElement => { walletInfo, isWalletLocked, walletLock, + walletState, initialHdDiscoveryCompleted } = useWalletStore(); @@ -66,7 +67,7 @@ export const PopupView = (): React.ReactElement => { return ; } - if (!!cardanoWallet && walletInfo && inMemoryWallet && initialHdDiscoveryCompleted) { + if (!!cardanoWallet && walletInfo && walletState && inMemoryWallet && initialHdDiscoveryCompleted) { return ; } From bd11e2ef0e4c8e2d7cc5991f899a69076156d557 Mon Sep 17 00:00:00 2001 From: Piotr Czeglik Date: Tue, 27 Feb 2024 12:48:07 +0100 Subject: [PATCH 27/27] test(extension): mock initializeTrezorTransport method --- .../src/hooks/__tests__/useWalletManager.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx b/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx index 3700ce8ee..355d5eb08 100644 --- a/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx +++ b/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx @@ -17,6 +17,7 @@ const mockRestoreWalletFromKeyAgent = jest.fn(); const mockSwitchKeyAgents = jest.fn(); const mockLedgerGetXpub = jest.fn(); const mockTrezorGetXpub = jest.fn(); +const mockInitializeTrezorTransport = jest.fn(); const mockLedgerCreateWithDevice = jest.fn(); const mockUseAppSettingsContext = jest.fn().mockReturnValue([{}, jest.fn()]); import React from 'react'; @@ -70,7 +71,8 @@ jest.mock('@lace/cardano', () => { }, Trezor: { TrezorKeyAgent: { - getXpub: mockTrezorGetXpub + getXpub: mockTrezorGetXpub, + initializeTrezorTransport: mockInitializeTrezorTransport } }, restoreWalletFromKeyAgent: mockRestoreWalletFromKeyAgent,