diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx index d5de946fa8..8f7c5a175c 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx @@ -20,10 +20,11 @@ import { createWalletAssetProvider } from '@cardano-sdk/wallet'; import { Skeleton } from 'antd'; import { useCurrencyStore, useAppSettingsContext } from '@providers'; -import { logger } from '@lib/wallet-api-ui'; +import { logger, walletRepository } from '@lib/wallet-api-ui'; import { useComputeTxCollateral } from '@hooks/useComputeTxCollateral'; import { utxoAndBackendChainHistoryResolver } from '@src/utils/utxo-chain-history-resolver'; import { AddressBookSchema, useDbStateValue } from '@lib/storage'; +import { getAllWalletsAddresses } from '@src/utils/get-all-wallets-addresses'; interface DappTransactionContainerProps { errorMessage?: string; @@ -82,6 +83,7 @@ export const DappTransactionContainer = withAddressBookContext( const userRewardAccounts = useObservable(inMemoryWallet.delegation.rewardAccounts$); const rewardAccountsAddresses = useMemo(() => userRewardAccounts?.map((key) => key.address), [userRewardAccounts]); const protocolParameters = useObservable(inMemoryWallet?.protocolParameters$); + const allWalletsAddresses = getAllWalletsAddresses(useObservable(walletRepository.wallets$)); useEffect(() => { if (!req || !protocolParameters) { @@ -151,7 +153,7 @@ export const DappTransactionContainer = withAddressBookContext( errorMessage={errorMessage} toAddress={toAddressTokens} collateral={txCollateral} - ownAddresses={ownAddresses} + ownAddresses={allWalletsAddresses.length > 0 ? allWalletsAddresses : ownAddresses} addressToNameMap={addressToNameMap} /> ) : ( diff --git a/apps/browser-extension-wallet/src/lib/scripts/background/__tests__/cache-wallets-address.test.ts b/apps/browser-extension-wallet/src/lib/scripts/background/__tests__/cache-wallets-address.test.ts new file mode 100644 index 0000000000..8a88ce90bc --- /dev/null +++ b/apps/browser-extension-wallet/src/lib/scripts/background/__tests__/cache-wallets-address.test.ts @@ -0,0 +1,107 @@ +/* eslint-disable no-magic-numbers */ +/* eslint-disable unicorn/no-useless-undefined */ +import { WalletManager, WalletRepository } from '@cardano-sdk/web-extension'; +import { cacheActivatedWalletAddressSubscription } from '../cache-wallets-address'; +import { Wallet } from '@lace/cardano'; +import { BehaviorSubject, of } from 'rxjs'; + +describe('cacheActivatedWalletAddressSubscription', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should not trigger subscription for no active wallet', () => { + const mockWalletManager = { + activeWallet$: of(undefined), + activeWalletId$: of(undefined) + } as unknown as WalletManager; + + const mockWalletRepository = { + wallets$: of([]), + updateWalletMetadata: jest.fn() + } as unknown as WalletRepository; + + cacheActivatedWalletAddressSubscription(mockWalletManager, mockWalletRepository); + + expect(mockWalletRepository.updateWalletMetadata).not.toHaveBeenCalled(); + }); + + it('should subscribe and update metadata', () => { + const mockWalletManager = { + activeWallet$: of({ addresses$: of([{ address: 'address1' }]) }), + activeWalletId$: of({ walletId: 'walletId' }) + } as unknown as WalletManager; + + const mockWalletRepository = { + wallets$: of([ + { + walletId: 'walletId', + metadata: {} + } + ]), + updateWalletMetadata: jest.fn() + } as unknown as WalletRepository; + + cacheActivatedWalletAddressSubscription(mockWalletManager, mockWalletRepository); + + expect(mockWalletRepository.updateWalletMetadata).toHaveBeenCalledWith({ + walletId: 'walletId', + metadata: { + walletAddresses: ['address1'] + } + }); + }); + + it('should subscribe and update metadata when a new wallet is added and activated', () => { + const activeWallet$ = new BehaviorSubject({ + addresses$: of([{ address: 'address1' }]) + }); + const activeWalletId$ = new BehaviorSubject({ + walletId: 'walletId1' + }); + const wallets$ = new BehaviorSubject<{ walletId: string; metadata: { walletAddresses?: string[] } }[]>([ + { + walletId: 'walletId1', + metadata: { walletAddresses: ['address1'] } + } + ]); + const mockWalletManager = { + activeWallet$, + activeWalletId$ + } as unknown as WalletManager; + + const mockWalletRepository = { + wallets$, + updateWalletMetadata: jest.fn() + } as unknown as WalletRepository; + + cacheActivatedWalletAddressSubscription(mockWalletManager, mockWalletRepository); + + wallets$.next([ + { + walletId: 'walletId1', + metadata: { walletAddresses: ['address1'] } + }, + { + walletId: 'walletId2', + metadata: {} + } + ]); + activeWalletId$.next({ walletId: 'walletId2' }); + activeWallet$.next({ addresses$: of([{ address: 'address2' }, { address: 'address3' }]) }); + + expect(mockWalletRepository.updateWalletMetadata).toHaveBeenNthCalledWith(1, { + walletId: 'walletId1', + metadata: { + walletAddresses: ['address1'] + } + }); + + expect(mockWalletRepository.updateWalletMetadata).toHaveBeenNthCalledWith(2, { + walletId: 'walletId2', + metadata: { + walletAddresses: ['address2', 'address3'] + } + }); + }); +}); diff --git a/apps/browser-extension-wallet/src/lib/scripts/background/cache-wallets-address.ts b/apps/browser-extension-wallet/src/lib/scripts/background/cache-wallets-address.ts new file mode 100644 index 0000000000..54f9857d18 --- /dev/null +++ b/apps/browser-extension-wallet/src/lib/scripts/background/cache-wallets-address.ts @@ -0,0 +1,31 @@ +import { WalletManager, WalletRepository } from '@cardano-sdk/web-extension'; +import { Wallet } from '@lace/cardano'; +import { filter, switchMap, withLatestFrom, zip } from 'rxjs'; + +export const cacheActivatedWalletAddressSubscription = ( + walletManager: WalletManager, + walletRepository: WalletRepository +): void => { + zip([ + walletManager.activeWalletId$.pipe(filter((activeWalletId) => Boolean(activeWalletId))), + walletManager.activeWallet$.pipe( + filter((wallet) => Boolean(wallet)), + switchMap((wallet) => wallet.addresses$) + ) + ]) + .pipe(withLatestFrom(walletRepository.wallets$)) + .subscribe(([[activeWallet, walletAddresses], wallets]) => { + const wallet = wallets.find(({ walletId }) => walletId === activeWallet.walletId); + const uniqueAddresses = [ + ...new Set([...(wallet.metadata.walletAddresses || []), ...walletAddresses.map(({ address }) => address)]) + ]; + + walletRepository.updateWalletMetadata({ + walletId: activeWallet.walletId, + metadata: { + ...wallet.metadata, + walletAddresses: uniqueAddresses + } + }); + }); +}; 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 2c64fa9d1c..0f58377d9b 100644 --- a/apps/browser-extension-wallet/src/lib/scripts/background/wallet.ts +++ b/apps/browser-extension-wallet/src/lib/scripts/background/wallet.ts @@ -23,6 +23,7 @@ import { 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'; +import { cacheActivatedWalletAddressSubscription } from './cache-wallets-address'; const logger = console; @@ -198,4 +199,6 @@ walletManager logger.error('Failed to initialize wallet manager', error); }); +cacheActivatedWalletAddressSubscription(walletManager, walletRepository); + export const wallet$ = walletManager.activeWallet$; diff --git a/apps/browser-extension-wallet/src/utils/__tests__/get-all-wallets-addresses.test.ts b/apps/browser-extension-wallet/src/utils/__tests__/get-all-wallets-addresses.test.ts new file mode 100644 index 0000000000..c2ade56984 --- /dev/null +++ b/apps/browser-extension-wallet/src/utils/__tests__/get-all-wallets-addresses.test.ts @@ -0,0 +1,36 @@ +import { AnyWallet } from '@cardano-sdk/web-extension'; +import { getAllWalletsAddresses } from '../get-all-wallets-addresses'; +import { Wallet } from '@lace/cardano'; + +describe('getAllWalletsAddresses', () => { + it('should return an empty array if undefined is provided', () => { + const addresses = getAllWalletsAddresses(); + + expect(addresses).toEqual([]); + }); + + it('should return an empty array if no wallets are provided', () => { + const addresses = getAllWalletsAddresses([]); + + expect(addresses).toEqual([]); + }); + + it('should return an array of payment addresses', () => { + const mockWallets = [ + { + metadata: { + walletAddresses: ['addr1', 'addr2'] + } + }, + { + metadata: { + walletAddresses: ['addr2', 'addr3'] + } + } + ] as AnyWallet[]; + + const addresses = getAllWalletsAddresses(mockWallets); + + expect(addresses).toEqual(['addr1', 'addr2', 'addr3']); + }); +}); diff --git a/apps/browser-extension-wallet/src/utils/get-all-wallets-addresses.ts b/apps/browser-extension-wallet/src/utils/get-all-wallets-addresses.ts new file mode 100644 index 0000000000..690c1c32bc --- /dev/null +++ b/apps/browser-extension-wallet/src/utils/get-all-wallets-addresses.ts @@ -0,0 +1,9 @@ +import { AnyWallet } from '@cardano-sdk/web-extension'; +import { Wallet } from '@lace/cardano'; +import flatMap from 'lodash/flatMap'; + +export const getAllWalletsAddresses = ( + wallets: AnyWallet[] = [] +): Wallet.Cardano.PaymentAddress[] => [ + ...new Set(flatMap(wallets.map(({ metadata: { walletAddresses = [] } }) => walletAddresses))) +]; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/TransactionDetailsProxy.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/TransactionDetailsProxy.tsx index e0223dbe9d..e5bab66893 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/TransactionDetailsProxy.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/TransactionDetailsProxy.tsx @@ -10,6 +10,8 @@ import { APP_MODE_POPUP } from '@src/utils/constants'; import { config } from '@src/config'; import { PostHogAction } from '@providers/AnalyticsProvider/analyticsTracker'; import { useObservable } from '@lace/common'; +import { getAllWalletsAddresses } from '@src/utils/get-all-wallets-addresses'; +import { walletRepository } from '@lib/wallet-api-ui'; type TransactionDetailsProxyProps = { name: string; @@ -31,6 +33,7 @@ export const TransactionDetailsProxy = withAddressBookContext( const openExternalLink = useExternalLinkOpener(); // Prepare own addresses of active account + const allWalletsAddresses = getAllWalletsAddresses(useObservable(walletRepository.wallets$)); const walletAddresses = useObservable(inMemoryWallet.addresses$)?.map((a) => a.address); // Prepare address book data as Map @@ -101,7 +104,7 @@ export const TransactionDetailsProxy = withAddressBookContext( amountTransformer={amountTransformer} headerDescription={getHeaderDescription() || cardanoCoin.symbol} txSummary={txSummary} - ownAddresses={walletAddresses} + ownAddresses={allWalletsAddresses.length > 0 ? allWalletsAddresses : walletAddresses} addressToNameMap={addressToNameMap} coinSymbol={cardanoCoin.symbol} isPopupView={isPopupView} diff --git a/packages/cardano/src/wallet/lib/cardano-wallet.ts b/packages/cardano/src/wallet/lib/cardano-wallet.ts index e17f23713f..90aadb2672 100644 --- a/packages/cardano/src/wallet/lib/cardano-wallet.ts +++ b/packages/cardano/src/wallet/lib/cardano-wallet.ts @@ -27,6 +27,7 @@ export interface WalletMetadata { name: string; lockValue?: HexBlob; lastActiveAccountIndex?: number; + walletAddresses?: Cardano.PaymentAddress[]; } export interface AccountMetadata { diff --git a/packages/core/src/ui/components/ActivityDetail/TransactionDetails.stories.tsx b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.stories.tsx index 414ddf07b8..97acb4e7bf 100644 --- a/packages/core/src/ui/components/ActivityDetail/TransactionDetails.stories.tsx +++ b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.stories.tsx @@ -56,7 +56,8 @@ const data: ComponentProps = { openExternalLink: (url) => window.open(url, '_blank', 'noopener,noreferrer'), handleOpenExternalHashLink: () => { console.log('handle on hash click', '639a43144dc2c0ead16f2fb753360f4b4f536502dbdb8aa5e424b00abb7534ff'); - } + }, + ownAddresses: [] }; const stakeVoteDelegationCertificate = [ diff --git a/packages/core/src/ui/components/DappAddressSections/DappAddressSections.tsx b/packages/core/src/ui/components/DappAddressSections/DappAddressSections.tsx index 79d439d0f6..69a0657018 100644 --- a/packages/core/src/ui/components/DappAddressSections/DappAddressSections.tsx +++ b/packages/core/src/ui/components/DappAddressSections/DappAddressSections.tsx @@ -1,8 +1,8 @@ /* eslint-disable sonarjs/no-identical-functions */ import React from 'react'; -import { addEllipsis, truncate } from '@lace/common'; +import { truncate, addEllipsis } from '@lace/common'; import { Wallet } from '@lace/cardano'; -import { AssetInfoWithAmount, Cardano } from '@cardano-sdk/core'; +import { Cardano, AssetInfoWithAmount } from '@cardano-sdk/core'; import { Typography } from 'antd'; import styles from './DappAddressSections.module.scss';