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/apps/browser-extension-wallet/src/components/DropdownMenu/DropdownMenu.tsx b/apps/browser-extension-wallet/src/components/DropdownMenu/DropdownMenu.tsx index fd63e0e86..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'; @@ -14,6 +14,8 @@ 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'; +import { getUiWalletType } from '@src/utils/get-ui-wallet-type'; export interface DropdownMenuProps { isPopup?: boolean; @@ -21,8 +23,11 @@ export interface DropdownMenuProps { export const DropdownMenu = ({ isPopup }: DropdownMenuProps): React.ReactElement => { const analytics = useAnalyticsContext(); - const { walletInfo } = useWalletStore(); - const [open, setOpen] = useState(false); + const { + cardanoWallet, + walletUI: { isDropdownMenuOpen }, + setIsDropdownMenuOpen + } = useWalletStore(); const [handle] = useGetHandles(); const handleImage = handle?.profilePic; const Chevron = isPopup ? ChevronSmall : ChevronNormal; @@ -32,34 +37,37 @@ export const DropdownMenu = ({ isPopup }: DropdownMenuProps): React.ReactElement }; const handleDropdownState = (openDropdown: boolean) => { - setOpen(openDropdown); + setIsDropdownMenuOpen(openDropdown); if (openDropdown) { sendAnalyticsEvent(PostHogAction.UserWalletProfileIconClick); } }; + const walletName = cardanoWallet.source.wallet.metadata.name; + return ( } placement="bottomRight" + open={isDropdownMenuOpen} trigger={['click']} > {process.env.USE_MULTI_WALLET === 'true' ? (
@@ -71,7 +79,7 @@ export const DropdownMenu = ({ isPopup }: DropdownMenuProps): React.ReactElement data-testid="header-menu-button" > - + 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; diff --git a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/DropdownMenuOverlay.tsx b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/DropdownMenuOverlay.tsx index 8bda01d26..588b9f3f2 100644 --- a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/DropdownMenuOverlay.tsx +++ b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/DropdownMenuOverlay.tsx @@ -19,6 +19,8 @@ import { WalletAccounts } from './components/WalletAccounts'; import { AddSharedWalletLink } from '@components/MainMenu/DropdownMenuOverlay/components/AddSharedWalletLink'; import { useWalletStore } from '@stores'; import classNames from 'classnames'; +import { AnyBip32Wallet } from '@cardano-sdk/web-extension'; +import { Wallet } from '@lace/cardano'; interface Props extends MenuProps { isPopup?: boolean; @@ -35,9 +37,10 @@ export const DropdownMenuOverlay: VFC = ({ ...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..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 @@ -1,18 +1,21 @@ -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'; +import { getUiWalletType } from '@src/utils/get-ui-wallet-type'; const ADRESS_FIRST_PART_LENGTH = 10; const ADRESS_LAST_PART_LENGTH = 5; @@ -27,25 +30,99 @@ 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, setIsDropdownMenuOpen } = 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 activeWalletName = 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={async () => { + await activateWallet({ + walletId: wallet.walletId, + accountIndex: lastActiveAccount.accountIndex + }); + setIsDropdownMenuOpen(false); + toast.notify({ + duration: TOAST_DEFAULT_DURATION, + text: t('multiWallet.activated.wallet', { walletName: wallet.metadata.name }) + }); + }} + type={getUiWalletType(wallet.type)} + /> + ); + }, + [activateWallet, getLastActiveAccount, onOpenWalletAccounts, setIsDropdownMenuOpen, t] + ); + + 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'} - /> - ) : ( + {process.env.USE_MULTI_WALLET === 'true' ? ( +
{wallets.map((wallet, i) => renderWallet(wallet, i === wallets.length - 1))}
+ ) : ( +
- {avatarVisible && } + {avatarVisible && }

- {walletName} + {activeWalletName}

{handleName || shortenedWalletAddress} @@ -93,11 +156,13 @@ export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInf

- )} -
-
- -
+
+ )} + {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..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 @@ -1,70 +1,127 @@ /* 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 { 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'; +import { WalletType } from '@cardano-sdk/web-extension'; -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 accountsListLabel = useMemo( + () => ({ + unlock: t('browserView.settings.wallet.accounts.unlockLabel'), + lock: t('browserView.settings.wallet.accounts.lockLabel') + }), + [t] + ); const editAccountDrawer = useAccountDataModal(); const disableAccountConfirmation = useAccountDataModal(); - const [mockAccountData, setMockAccountData] = useState(exampleAccountData); + const { manageAccountsWallet: wallet, cardanoWallet, setIsDropdownMenuOpen } = useWalletStore(); + const { + source: { + wallet: { walletId: activeWalletId }, + account: activeAccount + } + } = 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 => { + 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, + disableUnlock + }; + }), + [wallet, activeAccount?.accountIndex, activeWalletId, disableUnlock] + ); + + const activateAccount = useCallback( + async (accountIndex: number) => { + await activateWallet({ + walletId: wallet.walletId, + accountIndex + }); + setIsDropdownMenuOpen(false); + const accountName = accountsData.find((acc) => acc.accountNumber === accountIndex)?.label; + toast.notify({ + duration: TOAST_DEFAULT_DURATION, + text: t('multiWallet.activated.account', { accountName }) + }); + }, + [wallet.walletId, activateWallet, accountsData, setIsDropdownMenuOpen, t] + ); + + 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) => { + const name = defaultAccountName(accountIndex); + await addAccount({ + wallet, + accountIndex, + metadata: { name } + }); + setIsDropdownMenuOpen(false); + toast.notify({ + duration: TOAST_DEFAULT_DURATION, + text: t('multiWallet.activated.account', { accountName: name }) + }); + }, + [wallet, addAccount, t, setIsDropdownMenuOpen] + ); + + 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 ( <> @@ -85,27 +142,17 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack: data-testid="user-dropdown-wallet-account-list" > - editAccountDrawer.open(mockAccountData.find((a) => a.accountNumber === accountNumber)) - } - onAccountDeleteClick={(accountNumber) => { - disableAccountConfirmation.open(mockAccountData.find((a) => a.accountNumber === accountNumber)); - }} - accounts={mockAccountData} + label={accountsListLabel} + 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 +169,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..355d5eb08 100644 --- a/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx +++ b/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx @@ -15,11 +15,14 @@ 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 mockInitializeTrezorTransport = jest.fn(); 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, @@ -32,9 +35,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 { 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 @@ -60,7 +65,14 @@ jest.mock('@lace/cardano', () => { ...actual.Wallet, Ledger: { LedgerKeyAgent: { - createWithDevice: mockLedgerCreateWithDevice + createWithDevice: mockLedgerCreateWithDevice, + getXpub: mockLedgerGetXpub + } + }, + Trezor: { + TrezorKeyAgent: { + getXpub: mockTrezorGetXpub, + initializeTrezorTransport: mockInitializeTrezorTransport } }, restoreWalletFromKeyAgent: mockRestoreWalletFromKeyAgent, @@ -96,6 +108,11 @@ const getWrapper = ); +const render = () => + renderHook(() => useWalletManager(), { + wrapper: getWrapper({}) + }).result.current; + describe('Testing useWalletManager hook', () => { beforeEach(() => { jest.resetAllMocks(); @@ -363,19 +380,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; @@ -390,7 +407,6 @@ describe('Testing useWalletManager hook', () => { deviceConnection, accountIndex, name, - chainId, connectedDevice }); expect(walletApiUi.walletRepository.addWallet).toBeCalledTimes(1); @@ -478,6 +494,11 @@ 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 }); @@ -491,21 +512,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 } } @@ -515,8 +532,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(); @@ -538,6 +559,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', () => { @@ -632,4 +671,119 @@ describe('Testing useWalletManager hook', () => { expect(setAddressesDiscoveryCompleted).toBeCalledWith(false); }); }); + + describe('addAccount', () => { + 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'; + 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({ + walletId, + accountIndex: addAccountProps.accountIndex, + metadata: addAccountProps.metadata, + extendedAccountPublicKey + }); + } + ); + }); + }); + + describe('activateWallet', () => { + 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 }); + + 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..4ad18cf6a 100644 --- a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts +++ b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts @@ -19,9 +19,10 @@ 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, AnyWallet, WalletManagerActivateProps, WalletManagerApi, @@ -40,7 +41,7 @@ export interface CreateWallet { name: string; mnemonic: string[]; password: string; - chainId: Wallet.Cardano.ChainId; + chainId?: Wallet.Cardano.ChainId; } export interface SetWallet { @@ -53,10 +54,17 @@ export interface CreateHardwareWallet { accountIndex?: number; name: string; deviceConnection: Wallet.DeviceConnection; - chainId: Wallet.Cardano.ChainId; connectedDevice: Wallet.HardwareWallets; } +type WalletManagerAddAccountProps = { + wallet: AnyBip32Wallet; + metadata: Wallet.AccountMetadata; + accountIndex: number; +}; + +type ActivateWalletProps = Omit; + export interface UseWalletManager { walletManager: WalletManagerApi; walletRepository: WalletRepositoryApi; @@ -67,14 +75,79 @@ 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; + /** + * @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; } -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: + await Wallet.Trezor.TrezorKeyAgent.initializeTrezorTransport({ + manifest: Wallet.manifest, + communicationType: Wallet.KeyManagement.CommunicationType.Web + }); + 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 +159,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 +174,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 +185,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: @@ -135,73 +207,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 }, - 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, - provider - }); - - 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); @@ -216,7 +221,9 @@ export const useWalletManager = (): UseWalletManager => { currentChain, setCurrentChain, setCardanoCoin, - setAddressesDiscoveryCompleted + setAddressesDiscoveryCompleted, + manageAccountsWallet, + setManageAccountsWallet } = useWalletStore(); const [settings, updateAppSettings] = useAppSettingsContext(); const { @@ -235,6 +242,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 > => { @@ -267,15 +324,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 +398,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 + ] ); /** @@ -348,11 +429,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); @@ -364,7 +441,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( @@ -382,7 +464,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( @@ -405,9 +487,8 @@ export const useWalletManager = (): UseWalletManager => { const walletId = await walletRepository.addWallet(addWalletProps); await walletManager.activate({ walletId, - chainId: getCurrentChainId(), - accountIndex, - provider + chainId, + accountIndex }); // Needed for reset password flow @@ -434,14 +515,54 @@ export const useWalletManager = (): UseWalletManager => { [getCurrentChainId] ); + const activateWallet = useCallback( + async (props: ActivateWalletProps): Promise => { + 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: { + ...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 + * + * @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'); } @@ -486,7 +607,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 + ] ); /** @@ -548,7 +677,23 @@ export const useWalletManager = (): UseWalletManager => { [walletLock, loadWallet] ); + 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, + 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/translations/en.json b/apps/browser-extension-wallet/src/lib/translations/en.json index 2596248f3..d9eb93166 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", @@ -1407,6 +1408,12 @@ "description": "You'll have to start over.", "cancel": "Go back", "confirm": "Proceed" - } + }, + "activated": { + "wallet": "Wallet \"{{walletName}}\" activated", + "account": "Account \"{{accountName}}\" activated" + }, + "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 114f03142..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,13 +45,19 @@ 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 } ); +export const keyAgentFactory = createKeyAgentFactory({ bip32Ed25519: Wallet.bip32Ed25519, logger }); + export const signingCoordinator = new SigningCoordinator( { 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/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 ; } 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/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..c902f9f99 100644 --- a/apps/browser-extension-wallet/src/stores/types.ts +++ b/apps/browser-extension-wallet/src/stores/types.ts @@ -21,10 +21,9 @@ 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'; +import { Cardano, Reward } from '@cardano-sdk/core'; import { StakePoolSortOptions } from '@lace/staking'; import { ObservableWalletState } from '@hooks/useWalletState'; @@ -101,6 +100,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; @@ -134,6 +135,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; @@ -149,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; @@ -165,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/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/__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/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/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..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,9 @@ 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, setWalletState: jest.fn(), fetchNetworkInfo: jest.fn(), @@ -73,6 +76,7 @@ export const walletStoreMock = async ( setCardanoCoin: jest.fn(), setNetworkConnection: jest.fn(), walletUI: { + isDropdownMenuOpen: false, networkConnection: NetworkConnectionStates.CONNNECTED, cardanoCoin, appMode: APP_MODE_BROWSER, 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..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,8 +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; @@ -33,9 +37,35 @@ interface ConfirmationDialog { } export const SetupHardwareWallet = ({ shouldShowDialog$ }: ConfirmationDialog): JSX.Element => { - const { connectHardwareWallet } = useWalletManager(); + const { t } = useTranslation(); + const { connectHardwareWallet, createHardwareWallet } = useWalletManager(); const disconnectHardwareWallet$ = useMemo(() => new Subject(), []); + const hardwareWalletProviders = useMemo( + (): Providers => ({ + connectHardwareWallet, + disconnectHardwareWallet$, + shouldShowDialog$, + createWallet: async ({ account, connection, model, name }) => { + 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$, t] + ); + useEffect(() => { const onHardwareWalletDisconnect = (event: HIDConnectionEvent) => { disconnectHardwareWallet$.next(event); @@ -49,16 +79,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..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 @@ -1,13 +1,16 @@ 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'; +import { WalletConflictError } from '@cardano-sdk/web-extension'; +import { toast } from '@lace/common'; +import { TOAST_DEFAULT_DURATION } from '@hooks/useActionExecution'; const wordList = wordlists.english; @@ -15,6 +18,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 +47,25 @@ export const RestoreRecoveryPhrase = (): JSX.Element => { setMnemonic(mnemonic)} - onCancel={() => history.goBack()} - onSubmit={() => history.push(walletRoutePaths.assets)} + onCancel={() => { + clearSecrets(); + history.goBack(); + }} + onSubmit={useCallback(async () => { + 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, t])} 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/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/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); 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/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]); 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 } } ); 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/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/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 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, 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; 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'}

))} 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/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 ( void; onEditClick?: (accountNumber: number) => void; onDeleteClick?: (accountNumber: number) => void; 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, - onEditClick, + isDeletable, + onActivateClick, onDeleteClick, onUnlockClick, }: Readonly): JSX.Element => ( @@ -36,57 +62,60 @@ export const AccountItem = ({ className={cx.root} data-testid="wallet-account-item" > - - - - - {label} - - - m/1842`/1841/{accountNumber} - +
{ + onActivateClick?.(accountNumber); + }} + > + + + + + {label.name} + + + m/1852'/1815'/{accountNumber}' + + - +
{isUnlocked ? ( - - } - size="extraSmall" + isDeletable ? ( + { - onEditClick?.(accountNumber); + onDeleteClick?.(accountNumber); }} - data-testid="wallet-account-item-edit-btn" /> - } - size="extraSmall" - data-testid="wallet-account-item-delete-btn" + ) : undefined + ) : ( + + { - onDeleteClick?.(accountNumber); + onUnlockClick?.(accountNumber); }} /> - - ) : ( - { - onUnlockClick?.(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..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 @@ -8,11 +8,17 @@ export interface AccountData { label: string; accountNumber: number; isUnlocked: boolean; + isActive: boolean; + disableUnlock?: { reason: string }; } export interface Props { accounts: AccountData[]; - unlockLabel: string; + label: { + lock: string; + unlock: string; + }; + onAccountActivateClick?: (accountNumber: number) => void; onAccountUnlockClick?: (accountNumber: number) => void; onAccountEditClick?: (accountNumber: number) => void; onAccountDeleteClick?: (accountNumber: number) => void; @@ -20,23 +26,37 @@ export interface Props { export const AccountsList = ({ accounts, - unlockLabel, + label, + onAccountActivateClick, 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 => ( + { + if (!a.isActive && a.isUnlocked) { + onAccountActivateClick?.(accountNumber); + } + }} + onUnlockClick={onAccountUnlockClick} + onEditClick={onAccountEditClick} + onDeleteClick={onAccountDeleteClick} + /> + ))} + + ); +}; 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, }, ]} /> 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'; 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" 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