diff --git a/apps/mobile/src/modules/accounts/components/AccountAssetList/__tests__/AccountAssetList.spec.tsx b/apps/mobile/src/modules/accounts/components/AccountAssetList/__tests__/AccountAssetList.spec.tsx index f218026fe..ff8d79a10 100644 --- a/apps/mobile/src/modules/accounts/components/AccountAssetList/__tests__/AccountAssetList.spec.tsx +++ b/apps/mobile/src/modules/accounts/components/AccountAssetList/__tests__/AccountAssetList.spec.tsx @@ -37,6 +37,11 @@ vi.mock('@perawallet/wallet-core-accounts', async importOriginal => { isPending: false, })), useAllAccounts: vi.fn(() => []), + useAccountAuthAddresses: vi.fn(() => ({ + authAddresses: new Map(), + isPending: false, + isFetched: true, + })), isSigningAccount: vi.fn(() => true), } }) @@ -202,7 +207,6 @@ describe('AccountAssetList', () => { address: 'REKEYED_ADDR', name: 'Rekeyed Account', type: 'watch', - rekeyAddress: 'AUTH_ADDR', } as unknown as WalletAccount beforeEach(async () => { diff --git a/apps/mobile/src/modules/accounts/components/AccountAssetList/useAccountAssetList.ts b/apps/mobile/src/modules/accounts/components/AccountAssetList/useAccountAssetList.ts index 7dc630e16..44b57bcb5 100644 --- a/apps/mobile/src/modules/accounts/components/AccountAssetList/useAccountAssetList.ts +++ b/apps/mobile/src/modules/accounts/components/AccountAssetList/useAccountAssetList.ts @@ -17,6 +17,7 @@ import { isSigningAccount, useAccountBalancesQuery, useAllAccounts, + useAccountAuthAddresses, useSortedAssetBalances, WalletAccount, AssetWithAccountBalance, @@ -152,7 +153,12 @@ export const useAccountAssetList = ({ }, [sortedBalances, debouncedSearchFilter, assets]) const allAccounts = useAllAccounts() - const isWatch = !isSigningAccount(account, allAccounts) + const { authAddresses } = useAccountAuthAddresses() + const isWatch = !isSigningAccount( + account, + allAccounts, + authAddresses.get(account.address), + ) const goToAssetScreen = useCallback( (item: AssetWithAccountBalance) => { diff --git a/apps/mobile/src/modules/accounts/components/AccountIcon/AccountIcon.tsx b/apps/mobile/src/modules/accounts/components/AccountIcon/AccountIcon.tsx index 75e18a714..2d615d25c 100644 --- a/apps/mobile/src/modules/accounts/components/AccountIcon/AccountIcon.tsx +++ b/apps/mobile/src/modules/accounts/components/AccountIcon/AccountIcon.tsx @@ -17,6 +17,7 @@ import { AccountStatus, resolveAccountStatus, useAllAccounts, + useAccountAuthAddresses, WalletAccount, } from '@perawallet/wallet-core-accounts' import { useIsDarkMode } from '@hooks/useIsDarkMode' @@ -45,12 +46,17 @@ export const AccountIcon = (props: AccountIconProps) => { const { account, size = 'md', ...rest } = props const darkmode = useIsDarkMode() const accounts = useAllAccounts() + const { authAddresses } = useAccountAuthAddresses() const icon = useMemo(() => { if (!account) return <> const theme = darkmode ? 'dark' : 'light' - const accountStatus = resolveAccountStatus(account, accounts) + const accountStatus = resolveAccountStatus( + account, + accounts, + authAddresses.get(account.address), + ) const icon = iconNames[accountStatus] ?? FALLBACK_ASSET const iconName: IconName = icon.replaceAll( THEME_TOKEN, diff --git a/apps/mobile/src/modules/accounts/components/AccountIcon/__tests__/AccountIcon.spec.tsx b/apps/mobile/src/modules/accounts/components/AccountIcon/__tests__/AccountIcon.spec.tsx index b7ddcda8e..f5930e86b 100644 --- a/apps/mobile/src/modules/accounts/components/AccountIcon/__tests__/AccountIcon.spec.tsx +++ b/apps/mobile/src/modules/accounts/components/AccountIcon/__tests__/AccountIcon.spec.tsx @@ -26,6 +26,11 @@ vi.mock('@perawallet/wallet-core-accounts', () => ({ resolveAccountStatus: (...args: unknown[]) => mockResolveAccountStatus(...(args as [])), useAllAccounts: vi.fn(() => []), + useAccountAuthAddresses: vi.fn(() => ({ + authAddresses: new Map(), + isPending: false, + isFetched: true, + })), })) const account = { address: 'addr' } as WalletAccount diff --git a/apps/mobile/src/modules/accounts/components/AccountInfoCard/AccountTypeInfoContent/__tests__/useAccountTypeInfo.spec.ts b/apps/mobile/src/modules/accounts/components/AccountInfoCard/AccountTypeInfoContent/__tests__/useAccountTypeInfo.spec.ts index 508672e0f..7a7a53f06 100644 --- a/apps/mobile/src/modules/accounts/components/AccountInfoCard/AccountTypeInfoContent/__tests__/useAccountTypeInfo.spec.ts +++ b/apps/mobile/src/modules/accounts/components/AccountInfoCard/AccountTypeInfoContent/__tests__/useAccountTypeInfo.spec.ts @@ -43,6 +43,8 @@ vi.mock('@perawallet/wallet-core-config', () => ({ })) const mockUseAllAccounts = vi.fn() +const mockUseAccountAuthAddresses = + vi.fn<() => { authAddresses: Map }>() vi.mock('@perawallet/wallet-core-accounts', async importOriginal => { const actual = await importOriginal< @@ -51,6 +53,7 @@ vi.mock('@perawallet/wallet-core-accounts', async importOriginal => { return { ...actual, useAllAccounts: () => mockUseAllAccounts(), + useAccountAuthAddresses: () => mockUseAccountAuthAddresses(), } }) @@ -104,24 +107,36 @@ const multisigAccount: WalletAccount = { const rekeyedWithAuthAccount: WalletAccount = { type: 'algo25', address: 'REKEYEDADDR1234567890ABCDEFGHIJKLMNOPQRSTUVW', - rekeyAddress: 'ALGO25ADDR1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', keyPairId: 'key-4', } const rekeyedToLedgerAccount: WalletAccount = { type: 'algo25', address: 'REKEYLEDGADDR1234567890ABCDEFGHIJKLMNOPQRSTUV', - rekeyAddress: 'LEDGERADDR1234567890ABCDEFGHIJKLMNOPQRSTUVWX', keyPairId: 'key-5', } const rekeyedNoAuthAccount: WalletAccount = { type: 'algo25', address: 'REKEYNOAUTHADDR1234567890ABCDEFGHIJKLMNOPQRST', - rekeyAddress: 'UNKNOWNADDR1234567890ABCDEFGHIJKLMNOPQRSTUV', keyPairId: 'key-6', } +const authAddressMap = new Map([ + [ + rekeyedWithAuthAccount.address, + 'ALGO25ADDR1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', + ], + [ + rekeyedToLedgerAccount.address, + 'LEDGERADDR1234567890ABCDEFGHIJKLMNOPQRSTUVWX', + ], + [ + rekeyedNoAuthAccount.address, + 'UNKNOWNADDR1234567890ABCDEFGHIJKLMNOPQRSTUV', + ], +]) + describe('useAccountTypeInfo', () => { const onClose = vi.fn() @@ -133,6 +148,9 @@ describe('useAccountTypeInfo', () => { watchAccount, hdWalletAccount, ]) + mockUseAccountAuthAddresses.mockReturnValue({ + authAddresses: authAddressMap, + }) }) it('resolves standard account type', () => { diff --git a/apps/mobile/src/modules/accounts/components/AccountInfoCard/AccountTypeInfoContent/useAccountTypeInfo.ts b/apps/mobile/src/modules/accounts/components/AccountInfoCard/AccountTypeInfoContent/useAccountTypeInfo.ts index e89840fb2..2f8b86b2f 100644 --- a/apps/mobile/src/modules/accounts/components/AccountInfoCard/AccountTypeInfoContent/useAccountTypeInfo.ts +++ b/apps/mobile/src/modules/accounts/components/AccountInfoCard/AccountTypeInfoContent/useAccountTypeInfo.ts @@ -17,6 +17,7 @@ import { canSignWithAccount, hasSigningKeys, useAllAccounts, + useAccountAuthAddresses, resolveAccountStatus, type AccountStatus, } from '@perawallet/wallet-core-accounts' @@ -91,10 +92,12 @@ export const useAccountTypeInfo = ({ const { showToast } = useToast() const { pushWebView } = useWebView() const accounts = useAllAccounts() + const { authAddresses } = useAccountAuthAddresses() + const accountAuthAddress = authAddresses.get(account.address) ?? null const status = useMemo( - () => resolveAccountStatus(account, accounts), - [account, accounts], + () => resolveAccountStatus(account, accounts, accountAuthAddress), + [account, accounts, accountAuthAddress], ) const title = t(STATUS_I18N_MAP[status].title) @@ -116,7 +119,7 @@ export const useAccountTypeInfo = ({ const actions = useMemo(() => { const items: AccountTypeAction[] = [] - if (canSignWithAccount(account, accounts)) { + if (canSignWithAccount(account, accounts, authAddresses)) { items.push({ id: 'rekey-to-ledger', title: t('account_type_info.rekey_to_ledger'), @@ -131,7 +134,10 @@ export const useAccountTypeInfo = ({ }) } - if (isRekeyedAccount(account) && hasSigningKeys(account)) { + if ( + isRekeyedAccount(account, accountAuthAddress) && + hasSigningKeys(account) + ) { items.push({ id: 'undo-rekey', title: t('account_type_info.undo_rekey'), @@ -140,7 +146,7 @@ export const useAccountTypeInfo = ({ }) } - if (isRekeyedAccount(account)) { + if (isRekeyedAccount(account, accountAuthAddress)) { items.push({ id: 'rescan-rekeyed', title: t('account_type_info.rescan_rekeyed'), @@ -150,7 +156,14 @@ export const useAccountTypeInfo = ({ } return items - }, [account, accounts, t, notImplemented]) + }, [ + account, + accounts, + accountAuthAddress, + authAddresses, + t, + notImplemented, + ]) return { title, diff --git a/apps/mobile/src/modules/accounts/components/AccountInfoCard/__tests__/AccountInfoCard.spec.tsx b/apps/mobile/src/modules/accounts/components/AccountInfoCard/__tests__/AccountInfoCard.spec.tsx index 6c92052db..43ef2d02f 100644 --- a/apps/mobile/src/modules/accounts/components/AccountInfoCard/__tests__/AccountInfoCard.spec.tsx +++ b/apps/mobile/src/modules/accounts/components/AccountInfoCard/__tests__/AccountInfoCard.spec.tsx @@ -132,6 +132,11 @@ vi.mock('@perawallet/wallet-core-accounts', async importOriginal => { useAccountInformationQuery: (...args: unknown[]) => mockUseAccountInformationQuery(...args), useHDWalletGroups: () => mockUseHDWalletGroups(), + useAccountAuthAddresses: () => ({ + authAddresses: new Map(), + isPending: false, + isFetched: true, + }), } }) diff --git a/apps/mobile/src/modules/accounts/components/AccountInfoCard/useAccountInfoCard.ts b/apps/mobile/src/modules/accounts/components/AccountInfoCard/useAccountInfoCard.ts index 39c6c5380..6a7f853e2 100644 --- a/apps/mobile/src/modules/accounts/components/AccountInfoCard/useAccountInfoCard.ts +++ b/apps/mobile/src/modules/accounts/components/AccountInfoCard/useAccountInfoCard.ts @@ -17,6 +17,7 @@ import { isHDWalletAccount, isSigningAccount, useAllAccounts, + useAccountAuthAddresses, useHDWalletGroups, useAccountInformationQuery, } from '@perawallet/wallet-core-accounts' @@ -56,8 +57,13 @@ export const useAccountInfoCard = ({ const { hdWalletGroups } = useHDWalletGroups() const allAccounts = useAllAccounts() + const { authAddresses } = useAccountAuthAddresses() const isHDWallet = isHDWalletAccount(account) - const showMinBalance = isSigningAccount(account, allAccounts) + const showMinBalance = isSigningAccount( + account, + allAccounts, + authAddresses.get(account.address), + ) const handleToggleExpanded = useCallback(() => { setIsExpanded(prev => !prev) diff --git a/apps/mobile/src/modules/accounts/components/AccountNfts/__tests__/useAccountNfts.spec.ts b/apps/mobile/src/modules/accounts/components/AccountNfts/__tests__/useAccountNfts.spec.ts index f533e9349..6119b8ca8 100644 --- a/apps/mobile/src/modules/accounts/components/AccountNfts/__tests__/useAccountNfts.spec.ts +++ b/apps/mobile/src/modules/accounts/components/AccountNfts/__tests__/useAccountNfts.spec.ts @@ -44,6 +44,11 @@ vi.mock('@perawallet/wallet-core-accounts', async importOriginal => { useAccountBalancesQuery: (...args: unknown[]) => mockUseAccountBalancesQuery(...args), useAllAccounts: (...args: unknown[]) => mockUseAllAccounts(...args), + useAccountAuthAddresses: () => ({ + authAddresses: new Map(), + isPending: false, + isFetched: true, + }), isSigningAccount: (...args: unknown[]) => mockIsSigningAccount(...args), } }) diff --git a/apps/mobile/src/modules/accounts/components/AccountNfts/useAccountNfts.ts b/apps/mobile/src/modules/accounts/components/AccountNfts/useAccountNfts.ts index e4bd00ce0..98dc018d5 100644 --- a/apps/mobile/src/modules/accounts/components/AccountNfts/useAccountNfts.ts +++ b/apps/mobile/src/modules/accounts/components/AccountNfts/useAccountNfts.ts @@ -17,6 +17,7 @@ import { useSelectedAccount, useAccountBalancesQuery, useAllAccounts, + useAccountAuthAddresses, isSigningAccount, } from '@perawallet/wallet-core-accounts' import { @@ -105,6 +106,7 @@ const sortCollectibles = ( export const useAccountNfts = (): UseAccountNftsResult => { const account = useSelectedAccount() const allAccounts = useAllAccounts() + const { authAddresses } = useAccountAuthAddresses() const [searchFilter, setSearchFilter] = useState('') const sortMode = useCollectiblePreferencesStore( @@ -139,8 +141,14 @@ export const useAccountNfts = (): UseAccountNftsResult => { const navigation = useNavigation>() const canOptIn = useMemo( - () => account !== null && isSigningAccount(account, allAccounts), - [account, allAccounts], + () => + account !== null && + isSigningAccount( + account, + allAccounts, + authAddresses.get(account.address), + ), + [account, allAccounts, authAddresses], ) const { accountBalances, isPending } = useAccountBalancesQuery( diff --git a/apps/mobile/src/modules/accounts/components/AccountOptionsBottomSheet/__tests__/useAccountOptions.spec.ts b/apps/mobile/src/modules/accounts/components/AccountOptionsBottomSheet/__tests__/useAccountOptions.spec.ts index c52c1c57d..2f5dd9737 100644 --- a/apps/mobile/src/modules/accounts/components/AccountOptionsBottomSheet/__tests__/useAccountOptions.spec.ts +++ b/apps/mobile/src/modules/accounts/components/AccountOptionsBottomSheet/__tests__/useAccountOptions.spec.ts @@ -30,6 +30,13 @@ const { mockRemoveAccountById } = vi.hoisted(() => ({ const { mockAllAccounts } = vi.hoisted(() => ({ mockAllAccounts: vi.fn((): WalletAccount[] => []), })) +const { mockAuthAddresses } = vi.hoisted(() => ({ + mockAuthAddresses: vi.fn( + (): { authAddresses: Map } => ({ + authAddresses: new Map(), + }), + ), +})) const { mockUpdateAccount } = vi.hoisted(() => ({ mockUpdateAccount: vi.fn(), })) @@ -76,6 +83,7 @@ vi.mock('@perawallet/wallet-core-accounts', async importOriginal => { useRemoveAccountById: () => mockRemoveAccountById, useUpdateAccount: () => mockUpdateAccount, useAllAccounts: () => mockAllAccounts(), + useAccountAuthAddresses: () => mockAuthAddresses(), } }) @@ -102,14 +110,12 @@ describe('useAccountOptions', () => { address: 'REKEYEDADDRESS', type: AccountTypes.algo25, keyPairId: 'key-3', - rekeyAddress: 'AUTHADDRESS', } const rekeyedWatchAccount: WalletAccount = { id: 'acc-5', address: 'REKEYEDWATCHADDRESS', type: AccountTypes.watch, - rekeyAddress: 'ALGO25ADDRESS', } const hardwareAccount: WalletAccount = { @@ -127,6 +133,7 @@ describe('useAccountOptions', () => { beforeEach(() => { vi.clearAllMocks() mockIsAccountEnabled.mockReturnValue(true) + mockAuthAddresses.mockReturnValue({ authAddresses: new Map() }) mockAllAccounts.mockReturnValue([algo25Account, watchAccount]) }) @@ -173,6 +180,12 @@ describe('useAccountOptions', () => { }) it('shows all options including rekey for a rekeyed account', () => { + mockAuthAddresses.mockReturnValue({ + authAddresses: new Map([ + [rekeyedAccount.address, 'AUTHADDRESS'], + ]), + }) + const { result } = renderHook(() => useAccountOptions({ account: rekeyedAccount, @@ -201,6 +214,11 @@ describe('useAccountOptions', () => { algo25Account, rekeyedWatchAccount, ]) + mockAuthAddresses.mockReturnValue({ + authAddresses: new Map([ + [rekeyedWatchAccount.address, algo25Account.address], + ]), + }) const { result } = renderHook(() => useAccountOptions({ @@ -475,6 +493,12 @@ describe('useAccountOptions', () => { }) it('copies rekey auth address when auth address option is pressed', () => { + mockAuthAddresses.mockReturnValue({ + authAddresses: new Map([ + [rekeyedAccount.address, 'AUTHADDRESS'], + ]), + }) + const { result } = renderHook(() => useAccountOptions({ account: rekeyedAccount, @@ -632,6 +656,12 @@ describe('useAccountOptions', () => { }) it('shows not implemented toast for undo-rekey', () => { + mockAuthAddresses.mockReturnValue({ + authAddresses: new Map([ + [rekeyedAccount.address, 'AUTHADDRESS'], + ]), + }) + const { result } = renderHook(() => useAccountOptions({ account: rekeyedAccount, @@ -680,9 +710,13 @@ describe('useAccountOptions', () => { address: 'SOMEOTHERADDRESS', type: AccountTypes.algo25, keyPairId: 'key-rekeyed', - rekeyAddress: 'ALGO25ADDRESS', } mockAllAccounts.mockReturnValue([algo25Account, rekeyedToAlgo25]) + mockAuthAddresses.mockReturnValue({ + authAddresses: new Map([ + [rekeyedToAlgo25.address, algo25Account.address], + ]), + }) const { result } = renderHook(() => useAccountOptions({ diff --git a/apps/mobile/src/modules/accounts/components/AccountOptionsBottomSheet/useAccountOptions.ts b/apps/mobile/src/modules/accounts/components/AccountOptionsBottomSheet/useAccountOptions.ts index 8bd7801e1..9409af8b3 100644 --- a/apps/mobile/src/modules/accounts/components/AccountOptionsBottomSheet/useAccountOptions.ts +++ b/apps/mobile/src/modules/accounts/components/AccountOptionsBottomSheet/useAccountOptions.ts @@ -21,6 +21,7 @@ import { useRemoveAccountById, useUpdateAccount, useAllAccounts, + useAccountAuthAddresses, } from '@perawallet/wallet-core-accounts' import { useNotificationPreferences } from '@perawallet/wallet-core-messages' import { truncateAlgorandAddress } from '@perawallet/wallet-core-shared' @@ -70,6 +71,8 @@ export const useAccountOptions = ({ const { copyToClipboard } = useClipboard() const { isAccountEnabled, setAccountEnabled } = useNotificationPreferences() const accounts = useAllAccounts() + const { authAddresses } = useAccountAuthAddresses() + const accountAuthAddress = authAddresses.get(account.address) ?? null const removeAccountById = useRemoveAccountById() const updateAccount = useUpdateAccount() const navigation = useAppNavigation() @@ -116,11 +119,11 @@ export const useAccountOptions = ({ }, [notImplemented]) const handleAuthAddress = useCallback(() => { - if (account.rekeyAddress) { - copyToClipboard(account.rekeyAddress) + if (accountAuthAddress) { + copyToClipboard(accountAuthAddress) } onClose() - }, [copyToClipboard, account.rekeyAddress, onClose]) + }, [copyToClipboard, accountAuthAddress, onClose]) const handleUndoRekey = useCallback(() => { notImplemented() @@ -189,9 +192,11 @@ export const useAccountOptions = ({ ) const handleConfirmRemove = useCallback(() => { - const rekeyedToThisAccount = accounts.filter( - a => a.rekeyAddress === account.address && a.id !== account.id, - ) + const rekeyedToThisAccount = accounts.filter(a => { + if (a.id === account.id) return false + const auth = authAddresses.get(a.address) ?? null + return auth === account.address + }) if (rekeyedToThisAccount.length > 0) { handleCloseRemoveConfirm() @@ -215,6 +220,7 @@ export const useAccountOptions = ({ } }, [ accounts, + authAddresses, account.address, account.id, removeAccountById, @@ -253,7 +259,7 @@ export const useAccountOptions = ({ }) } - if (isRekeyedAccount(account)) { + if (isRekeyedAccount(account, accountAuthAddress)) { items.push({ id: 'auth-address', icon: 'account-rekeyed', @@ -262,7 +268,10 @@ export const useAccountOptions = ({ }) } - if (isRekeyedAccount(account) && hasSigningKeys(account)) { + if ( + isRekeyedAccount(account, accountAuthAddress) && + hasSigningKeys(account) + ) { items.push({ id: 'undo-rekey', icon: 'undo', @@ -271,7 +280,7 @@ export const useAccountOptions = ({ }) } - if (canSignWithAccount(account, accounts)) { + if (canSignWithAccount(account, accounts, authAddresses)) { items.push({ id: 'rekey-to-ledger', icon: 'rekey', @@ -315,6 +324,9 @@ export const useAccountOptions = ({ }, [ t, account, + accounts, + accountAuthAddress, + authAddresses, notificationsEnabled, handleCopyAddress, handleShowAddress, diff --git a/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/AccountOverview.spec.tsx b/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/AccountOverview.spec.tsx index af72e6181..88139c27b 100644 --- a/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/AccountOverview.spec.tsx +++ b/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/AccountOverview.spec.tsx @@ -66,6 +66,11 @@ vi.mock('@perawallet/wallet-core-accounts', () => ({ })), useSelectedAccount: vi.fn(() => undefined), useAllAccounts: vi.fn(() => []), + useAccountAuthAddresses: vi.fn(() => ({ + authAddresses: new Map(), + isPending: false, + isFetched: true, + })), isSigningAccount: vi.fn(() => true), })) @@ -373,7 +378,6 @@ describe('AccountOverview', () => { const rekeyedAccount = { address: 'REKEYED_ADDR', type: 'watch', - rekeyAddress: 'AUTH_ADDR', } as unknown as WalletAccount beforeEach(async () => { diff --git a/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/useAccountOverviewHeader.spec.tsx b/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/useAccountOverviewHeader.spec.tsx index a57de9a0a..71be1be2e 100644 --- a/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/useAccountOverviewHeader.spec.tsx +++ b/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/useAccountOverviewHeader.spec.tsx @@ -40,6 +40,11 @@ vi.mock('@perawallet/wallet-core-accounts', () => ({ isPending: false, })), useAllAccounts: vi.fn(() => []), + useAccountAuthAddresses: vi.fn(() => ({ + authAddresses: new Map(), + isPending: false, + isFetched: true, + })), isSigningAccount: vi.fn(() => true), })) diff --git a/apps/mobile/src/modules/accounts/components/AccountOverview/useAccountOverviewHeader.ts b/apps/mobile/src/modules/accounts/components/AccountOverview/useAccountOverviewHeader.ts index 0b6e30ae1..287432680 100644 --- a/apps/mobile/src/modules/accounts/components/AccountOverview/useAccountOverviewHeader.ts +++ b/apps/mobile/src/modules/accounts/components/AccountOverview/useAccountOverviewHeader.ts @@ -17,6 +17,7 @@ import { isSigningAccount, useAccountBalancesQuery, useAllAccounts, + useAccountAuthAddresses, usePortfolioTotals, WalletAccount, } from '@perawallet/wallet-core-accounts' @@ -46,6 +47,7 @@ export const useAccountOverviewHeader = ( ): UseAccountOverviewHeaderResult => { const { usdToPreferred } = useCurrency() const allAccounts = useAllAccounts() + const { authAddresses } = useAccountAuthAddresses() const { portfolioAlgoValue, accountBalances, isPending } = useAccountBalancesQuery(account ? [account] : []) const { portfolioUsdValue } = usePortfolioTotals(accountBalances) @@ -80,7 +82,11 @@ export const useAccountOverviewHeader = ( setPeriod, selectedPoint, hasBalance: portfolioAlgoValue.gt(0), - canSign: isSigningAccount(account, allAccounts), + canSign: isSigningAccount( + account, + allAccounts, + authAddresses.get(account.address), + ), togglePrivacyMode, handleChartSelectionChange, } diff --git a/apps/mobile/src/modules/assets/components/holdings/AssetActionButtons/AssetActionButtons.tsx b/apps/mobile/src/modules/assets/components/holdings/AssetActionButtons/AssetActionButtons.tsx index 60e46ea04..f792e8cf7 100644 --- a/apps/mobile/src/modules/assets/components/holdings/AssetActionButtons/AssetActionButtons.tsx +++ b/apps/mobile/src/modules/assets/components/holdings/AssetActionButtons/AssetActionButtons.tsx @@ -24,6 +24,7 @@ import { ReceiveFundsBottomSheet } from '@modules/transactions/components/receiv import { useSelectedAccount, useAllAccounts, + useAccountAuthAddresses, isSigningAccount, AssetWithAccountBalance, } from '@perawallet/wallet-core-accounts' @@ -47,10 +48,17 @@ export const AssetActionButtons = ({ const receiveFunds = useModalState() const account = useSelectedAccount() const allAccounts = useAllAccounts() + const { authAddresses } = useAccountAuthAddresses() const { setSelectedAssetId, setCanSelectAsset } = useSendFunds() const { copyToClipboard } = useClipboard() const { showToast } = useToast() - const isWatch = account ? !isSigningAccount(account, allAccounts) : true + const isWatch = account + ? !isSigningAccount( + account, + allAccounts, + authAddresses.get(account.address), + ) + : true const goToRootPage = (name: string) => { navigation.replace('TabBar', { screen: name }) diff --git a/apps/mobile/src/modules/assets/components/holdings/AssetActionButtons/__tests__/AssetActionButtons.spec.tsx b/apps/mobile/src/modules/assets/components/holdings/AssetActionButtons/__tests__/AssetActionButtons.spec.tsx index 1846c6e0f..83f6901a5 100644 --- a/apps/mobile/src/modules/assets/components/holdings/AssetActionButtons/__tests__/AssetActionButtons.spec.tsx +++ b/apps/mobile/src/modules/assets/components/holdings/AssetActionButtons/__tests__/AssetActionButtons.spec.tsx @@ -80,6 +80,11 @@ vi.mock('@perawallet/wallet-core-accounts', async importOriginal => { ...actual, useSelectedAccount: vi.fn(() => ({ address: 'test-address' })), useAllAccounts: vi.fn(() => []), + useAccountAuthAddresses: vi.fn(() => ({ + authAddresses: new Map(), + isPending: false, + isFetched: true, + })), isSigningAccount: vi.fn(() => true), } }) @@ -193,7 +198,6 @@ describe('AssetActionButtons', () => { vi.mocked(useSelectedAccount).mockReturnValue({ address: 'REKEYED_ADDR', type: 'watch', - rekeyAddress: 'AUTH_ADDR', } as unknown as WalletAccount) vi.mocked(isSigningAccount).mockReturnValue(true) }) diff --git a/apps/mobile/src/modules/assets/screens/CollectibleDetailScreen/__tests__/useCollectibleDetail.spec.ts b/apps/mobile/src/modules/assets/screens/CollectibleDetailScreen/__tests__/useCollectibleDetail.spec.ts index c28a01f24..46f93664b 100644 --- a/apps/mobile/src/modules/assets/screens/CollectibleDetailScreen/__tests__/useCollectibleDetail.spec.ts +++ b/apps/mobile/src/modules/assets/screens/CollectibleDetailScreen/__tests__/useCollectibleDetail.spec.ts @@ -111,6 +111,11 @@ vi.mock('@perawallet/wallet-core-accounts', async importOriginal => { useSelectedAccount: (...args: unknown[]) => mockUseSelectedAccount(...args), useAllAccounts: (...args: unknown[]) => mockUseAllAccounts(...args), + useAccountAuthAddresses: () => ({ + authAddresses: new Map(), + isPending: false, + isFetched: true, + }), useAccountAssetBalanceQuery: (...args: unknown[]) => mockUseAccountAssetBalanceQuery(...args), } diff --git a/apps/mobile/src/modules/assets/screens/CollectibleDetailScreen/useCollectibleDetail.ts b/apps/mobile/src/modules/assets/screens/CollectibleDetailScreen/useCollectibleDetail.ts index 85564dc38..3621f3368 100644 --- a/apps/mobile/src/modules/assets/screens/CollectibleDetailScreen/useCollectibleDetail.ts +++ b/apps/mobile/src/modules/assets/screens/CollectibleDetailScreen/useCollectibleDetail.ts @@ -23,6 +23,7 @@ import { import { useSelectedAccount, useAllAccounts, + useAccountAuthAddresses, useAccountAssetBalanceQuery, isSigningAccount, type AssetWithAccountBalance, @@ -83,6 +84,7 @@ export const useCollectibleDetail = ( const account = useSelectedAccount() const { network } = useNetwork() const allAccounts = useAllAccounts() + const { authAddresses } = useAccountAuthAddresses() const { t } = useLanguage() const { data: assetBalance } = useAccountAssetBalanceQuery( account ?? undefined, @@ -98,7 +100,13 @@ export const useCollectibleDetail = ( const [fullScreenInitialIndex, setFullScreenInitialIndex] = useState(0) const collectible = asset?.peraMetadata?.collectible - const isWatch = account ? !isSigningAccount(account, allAccounts) : true + const isWatch = account + ? !isSigningAccount( + account, + allAccounts, + authAddresses.get(account.address), + ) + : true const traits = collectible?.traits ?? [] const media = collectible?.media ?? [] diff --git a/apps/mobile/src/modules/onboarding/routes/types.ts b/apps/mobile/src/modules/onboarding/routes/types.ts index 40656217a..c9929c034 100644 --- a/apps/mobile/src/modules/onboarding/routes/types.ts +++ b/apps/mobile/src/modules/onboarding/routes/types.ts @@ -14,6 +14,7 @@ import { WalletAccount, HDWalletAccount, ImportAccountType, + DiscoveredRekeyedAccount, } from '@perawallet/wallet-core-accounts' import type { LedgerAccount } from '@perawallet/wallet-core-ledger' @@ -28,7 +29,7 @@ export type OnboardingStackParamList = { accounts: HDWalletAccount[] } ImportRekeyedAddresses: { - accounts: WalletAccount[] + accounts: DiscoveredRekeyedAccount[] } ImportInfo: { accountType: ImportAccountType @@ -75,7 +76,7 @@ export type AddAccountStackParamList = { accounts: HDWalletAccount[] } ImportRekeyedAddresses: { - accounts: WalletAccount[] + accounts: DiscoveredRekeyedAccount[] } ImportInfo: { accountType: ImportAccountType diff --git a/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/ImportRekeyedAddressesItem.tsx b/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/ImportRekeyedAddressesItem.tsx index e8b32cc24..a92427ce7 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/ImportRekeyedAddressesItem.tsx +++ b/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/ImportRekeyedAddressesItem.tsx @@ -22,25 +22,26 @@ import { } from '@components/core' import { truncateAlgorandAddress } from '@perawallet/wallet-core-shared' import { CopyableText } from '@components/CopyableText' -import { WalletAccount } from '@perawallet/wallet-core-accounts' +import type { DiscoveredRekeyedAccount } from '@perawallet/wallet-core-accounts' import { useLanguage } from '@hooks/useLanguage' import { useModalState } from '@hooks/useModalState' import { useStyles } from './styles' import { RekeyedAccountInfoBottomSheet } from './RekeyedAccountInfoBottomSheet' type ImportRekeyedAddressesItemProps = { - account: WalletAccount + discovered: DiscoveredRekeyedAccount isImported: boolean isSelected: boolean onToggle: (address: string) => void } export const ImportRekeyedAddressesItem = ({ - account, + discovered, isImported, isSelected, onToggle, }: ImportRekeyedAddressesItemProps) => { + const { account } = discovered const styles = useStyles() const { t } = useLanguage() const bottomSheetState = useModalState() @@ -109,7 +110,7 @@ export const ImportRekeyedAddressesItem = ({ {isImported && ( diff --git a/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/ImportRekeyedAddressesScreen.tsx b/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/ImportRekeyedAddressesScreen.tsx index 066ac6106..7385e812a 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/ImportRekeyedAddressesScreen.tsx +++ b/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/ImportRekeyedAddressesScreen.tsx @@ -49,12 +49,14 @@ export const ImportRekeyedAddressesScreen = () => { )} data={accounts} extraData={selectedAddresses} - keyExtractor={item => item.address} + keyExtractor={item => item.account.address} renderItem={({ item }) => ( )} diff --git a/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/RekeyedAccountInfoBottomSheet/RekeyedAccountInfoBottomSheet.tsx b/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/RekeyedAccountInfoBottomSheet/RekeyedAccountInfoBottomSheet.tsx index 1fbdfa817..44746e388 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/RekeyedAccountInfoBottomSheet/RekeyedAccountInfoBottomSheet.tsx +++ b/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/RekeyedAccountInfoBottomSheet/RekeyedAccountInfoBottomSheet.tsx @@ -22,7 +22,7 @@ import { } from '@components/core' import { CopyableText } from '@components/CopyableText' import { BottomSheetScrollView } from '@gorhom/bottom-sheet' -import { WalletAccount } from '@perawallet/wallet-core-accounts' +import type { DiscoveredRekeyedAccount } from '@perawallet/wallet-core-accounts' import { ALGO_ASSET_ID } from '@perawallet/wallet-core-assets' import { useResolvedAddress } from '@hooks/useResolvedAddress' import { useLanguage } from '@hooks/useLanguage' @@ -35,16 +35,17 @@ import { useStyles } from './styles' export type RekeyedAccountInfoBottomSheetProps = { isVisible: boolean onClose: () => void - account: WalletAccount + discovered: DiscoveredRekeyedAccount } export const RekeyedAccountInfoBottomSheet = ({ isVisible, onClose, - account, + discovered, }: RekeyedAccountInfoBottomSheetProps) => { const styles = useStyles() const { t } = useLanguage() + const { account } = discovered const { displayName: accountDisplayName } = useResolvedAddress( account.address, ) @@ -53,7 +54,7 @@ export const RekeyedAccountInfoBottomSheet = ({ rekeyedAccountAlgoValue, authAddress, authAccountAlgoValue, - } = useRekeyedAccountInfoBottomSheet({ account, isVisible }) + } = useRekeyedAccountInfoBottomSheet({ discovered, isVisible }) return ( { , ) @@ -100,7 +106,7 @@ describe('RekeyedAccountInfoBottomSheet', () => { , ) @@ -114,11 +120,11 @@ describe('RekeyedAccountInfoBottomSheet', () => { , ) - expect(screen.getByText(MOCK_ACCOUNT.rekeyAddress)).toBeTruthy() + expect(screen.getByText(MOCK_AUTH_ADDRESS)).toBeTruthy() }) it('calls onClose when close button is pressed', () => { @@ -127,7 +133,7 @@ describe('RekeyedAccountInfoBottomSheet', () => { , ) @@ -145,7 +151,10 @@ describe('RekeyedAccountInfoBottomSheet', () => { , ) diff --git a/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/RekeyedAccountInfoBottomSheet/useRekeyedAccountInfoBottomSheet.ts b/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/RekeyedAccountInfoBottomSheet/useRekeyedAccountInfoBottomSheet.ts index e89d510aa..1af06eff8 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/RekeyedAccountInfoBottomSheet/useRekeyedAccountInfoBottomSheet.ts +++ b/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/RekeyedAccountInfoBottomSheet/useRekeyedAccountInfoBottomSheet.ts @@ -15,14 +15,14 @@ import { AccountTypes, AssetWithAccountBalance, useAccountBalancesQuery, - WalletAccount, WatchAccount, + type DiscoveredRekeyedAccount, } from '@perawallet/wallet-core-accounts' import { ALGO_ASSET_ID } from '@perawallet/wallet-core-assets' import { Decimal } from 'decimal.js' type UseRekeyedAccountInfoBottomSheetParams = { - account: WalletAccount + discovered: DiscoveredRekeyedAccount isVisible: boolean } @@ -35,19 +35,20 @@ export type UseRekeyedAccountInfoBottomSheetResult = { } export function useRekeyedAccountInfoBottomSheet({ - account, + discovered, isVisible, }: UseRekeyedAccountInfoBottomSheetParams): UseRekeyedAccountInfoBottomSheetResult { + const { account, authAddress: discoveredAuthAddress } = discovered const { accountBalances: rekeyedBalances, isPending: isRekeyedPending } = useAccountBalancesQuery([account], isVisible) const authAccount = useMemo(() => { - if (!account.rekeyAddress) return undefined + if (!discoveredAuthAddress) return undefined return { - address: account.rekeyAddress, + address: discoveredAuthAddress, type: AccountTypes.watch, } - }, [account.rekeyAddress]) + }, [discoveredAuthAddress]) const { accountBalances: authBalances, isPending: isAuthPending } = useAccountBalancesQuery( @@ -85,7 +86,7 @@ export function useRekeyedAccountInfoBottomSheet({ return { rekeyedAccountBalances: rekeyedAccountData.balances, rekeyedAccountAlgoValue: rekeyedAccountData.algoValue, - authAddress: account.rekeyAddress, + authAddress: discoveredAuthAddress, authAccountAlgoValue, isPending: isRekeyedPending || isAuthPending, } diff --git a/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/__tests__/ImportRekeyedAddressesItem.spec.tsx b/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/__tests__/ImportRekeyedAddressesItem.spec.tsx index bb8361526..40c68583e 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/__tests__/ImportRekeyedAddressesItem.spec.tsx +++ b/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/__tests__/ImportRekeyedAddressesItem.spec.tsx @@ -27,17 +27,21 @@ const MOCK_ACCOUNT = { id: '1', address: 'MOCK_ADDRESS', type: AccountTypes.algo25, - rekeyAddress: 'REKEY_ADDRESS', keyPairId: 'pk', } +const MOCK_DISCOVERED = { + account: MOCK_ACCOUNT, + authAddress: 'REKEY_ADDRESS', +} + const TRUNCATED_ADDRESS = truncateAlgorandAddress(MOCK_ACCOUNT.address) describe('ImportRekeyedAddressesItem', () => { it('renders correctly with truncated address', () => { render( { const onToggle = vi.fn() render( { it('renders as disabled and shows "already imported" chip when isImported is true', () => { render( { it('opens info bottom sheet when info icon is pressed', () => { render( { it('renders selected state correctly', () => { render( { vi.mocked(useImportRekeyedAddressesScreen).mockReturnValue({ accounts: [ { - id: '1', - address: 'MOCK_ADDRESS', - type: AccountTypes.algo25, - rekeyAddress: 'REKEY_ADDRESS', - keyPairId: 'pk', + account: { + id: '1', + address: 'MOCK_ADDRESS', + type: AccountTypes.algo25, + keyPairId: 'pk', + }, + authAddress: 'REKEY_ADDRESS', }, ], selectedAddresses: new Set(), diff --git a/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/__tests__/useImportRekeyedAddressesScreen.spec.ts b/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/__tests__/useImportRekeyedAddressesScreen.spec.ts index 41c8ad5c0..a6dfcdc99 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/__tests__/useImportRekeyedAddressesScreen.spec.ts +++ b/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/__tests__/useImportRekeyedAddressesScreen.spec.ts @@ -22,23 +22,26 @@ import { } from '@perawallet/wallet-core-accounts' import { useExitAccountFlow } from '@modules/onboarding/hooks' -const MOCK_ACCOUNTS = [ +const MOCK_WALLET_ACCOUNTS = [ { id: '1', address: 'ACC1', type: AccountTypes.algo25, - rekeyAddress: 'REKEY', keyPairId: 'pk', }, { id: '2', address: 'ACC2', type: AccountTypes.algo25, - rekeyAddress: 'REKEY', keyPairId: 'pk2', }, ] +const MOCK_ACCOUNTS = MOCK_WALLET_ACCOUNTS.map(account => ({ + account, + authAddress: 'REKEY', +})) + vi.mock('@react-navigation/native', () => ({ useRoute: vi.fn(), })) @@ -47,11 +50,18 @@ vi.mock('@perawallet/wallet-core-accounts', () => ({ useAllAccounts: vi.fn(), useSetAccounts: vi.fn(), useSelectedAccountAddress: vi.fn(), + useAccountBalancesInvalidator: vi.fn(() => ({ invalidate: vi.fn() })), + fetchAndPersistAccount: vi.fn().mockResolvedValue(undefined), + seedAuthAddress: vi.fn().mockResolvedValue(undefined), AccountTypes: { algo25: 'algo25', }, })) +vi.mock('@perawallet/wallet-core-blockchain', () => ({ + useNetwork: () => ({ network: 'mainnet' }), +})) + vi.mock('@hooks/useLanguage', () => ({ useLanguage: () => ({ t: (k: string) => k }), })) @@ -62,6 +72,7 @@ vi.mock('@modules/onboarding/hooks', () => ({ vi.mock('@perawallet/wallet-core-shared', () => ({ deferToNextCycle: (cb: () => void) => setTimeout(cb, 0), + logger: { warn: vi.fn() }, })) describe('useImportRekeyedAddressesScreen', () => { @@ -125,7 +136,7 @@ describe('useImportRekeyedAddressesScreen', () => { it('tracks already imported addresses without selecting any', () => { vi.mocked(useAllAccounts).mockReturnValue([ - { ...MOCK_ACCOUNTS[0] }, // ACC1 is already imported + { ...MOCK_WALLET_ACCOUNTS[0] }, // ACC1 is already imported ]) const { result } = renderHook(() => useImportRekeyedAddressesScreen()) @@ -155,7 +166,7 @@ describe('useImportRekeyedAddressesScreen', () => { vi.runAllTimers() }) - expect(mockSetAccounts).toHaveBeenCalledWith(MOCK_ACCOUNTS) + expect(mockSetAccounts).toHaveBeenCalledWith(MOCK_WALLET_ACCOUNTS) expect(mockSetSelectedAccountAddress).toHaveBeenCalledWith('ACC1') expect(mockExitAccountFlow).toHaveBeenCalled() }) diff --git a/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/useImportRekeyedAddressesScreen.ts b/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/useImportRekeyedAddressesScreen.ts index d656cc07e..193c8a491 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/useImportRekeyedAddressesScreen.ts +++ b/apps/mobile/src/modules/onboarding/screens/ImportRekeyedAddressesScreen/useImportRekeyedAddressesScreen.ts @@ -17,11 +17,15 @@ import { useAllAccounts, useSetAccounts, useSelectedAccountAddress, - WalletAccount, + fetchAndPersistAccount, + seedAuthAddress, + useAccountBalancesInvalidator, + type DiscoveredRekeyedAccount, } from '@perawallet/wallet-core-accounts' +import { useNetwork } from '@perawallet/wallet-core-blockchain' import { useLanguage } from '@hooks/useLanguage' import { useExitAccountFlow } from '@modules/onboarding/hooks' -import { deferToNextCycle } from '@perawallet/wallet-core-shared' +import { deferToNextCycle, logger } from '@perawallet/wallet-core-shared' type ImportRekeyedAddressesRouteProp = RouteProp< OnboardingStackParamList, @@ -29,7 +33,7 @@ type ImportRekeyedAddressesRouteProp = RouteProp< > export type UseImportRekeyedAddressesScreenResult = { - accounts: WalletAccount[] + accounts: DiscoveredRekeyedAccount[] selectedAddresses: Set isAllSelected: boolean areAllImported: boolean @@ -53,6 +57,9 @@ export function useImportRekeyedAddressesScreen(): UseImportRekeyedAddressesScre const { exitAccountFlow } = useExitAccountFlow() const { setSelectedAccountAddress } = useSelectedAccountAddress() const { setAccounts } = useSetAccounts() + const { network } = useNetwork() + const { invalidate: invalidateAccountBalances } = + useAccountBalancesInvalidator() const alreadyImportedAddresses = useMemo(() => { return new Set(allAccounts.map(acc => acc.address)) @@ -60,7 +67,7 @@ export function useImportRekeyedAddressesScreen(): UseImportRekeyedAddressesScre const newAccounts = useMemo(() => { return accounts.filter( - acc => !alreadyImportedAddresses.has(acc.address), + acc => !alreadyImportedAddresses.has(acc.account.address), ) }, [accounts, alreadyImportedAddresses]) @@ -92,16 +99,19 @@ export function useImportRekeyedAddressesScreen(): UseImportRekeyedAddressesScre if (isAllSelected) { setSelectedAddresses(new Set()) } else { - setSelectedAddresses(new Set(newAccounts.map(acc => acc.address))) + setSelectedAddresses( + new Set(newAccounts.map(acc => acc.account.address)), + ) } }, [isAllSelected, newAccounts]) const [isImporting, setIsImporting] = useState(false) const handleContinue = useCallback(() => { - const accountsToAdd = accounts.filter(acc => - selectedAddresses.has(acc.address), + const discoveredToAdd = accounts.filter(acc => + selectedAddresses.has(acc.account.address), ) + const accountsToAdd = discoveredToAdd.map(d => d.account) if (accountsToAdd.length === 0) { exitAccountFlow() @@ -112,6 +122,56 @@ export function useImportRekeyedAddressesScreen(): UseImportRekeyedAddressesScre deferToNextCycle(() => { setAccounts([...allAccounts, ...accountsToAdd]) setSelectedAccountAddress(accountsToAdd[0].address) + + // Seed the already-known auth addresses into the DB + // synchronously so the UI shows the correct rekeyed + // status immediately. The background sync will later + // fill in the full balance data. + Promise.allSettled( + discoveredToAdd.map(d => + seedAuthAddress({ + accountAddress: d.account.address, + network, + authAddress: d.authAddress, + }), + ), + ).then(() => { + invalidateAccountBalances() + }) + + // Also kick off full on-chain fetches in the background + // to populate balances and holdings. + Promise.allSettled( + accountsToAdd.map(acc => + fetchAndPersistAccount(acc.address, network), + ), + ) + .then(results => { + results.forEach((result, index) => { + if (result.status === 'rejected') { + logger.warn( + 'Failed to prefetch imported rekeyed account info', + { + address: accountsToAdd[index].address, + network, + error: + result.reason instanceof Error + ? { + message: + result.reason.message, + stack: result.reason.stack, + } + : result.reason, + }, + ) + } + }) + invalidateAccountBalances() + }) + .catch(() => { + // allSettled never rejects; guard kept for safety. + }) + exitAccountFlow() setIsImporting(false) }) @@ -122,6 +182,8 @@ export function useImportRekeyedAddressesScreen(): UseImportRekeyedAddressesScre exitAccountFlow, setSelectedAccountAddress, setAccounts, + network, + invalidateAccountBalances, ]) const handleSkip = useCallback(() => { diff --git a/apps/mobile/src/modules/onboarding/screens/NameAccountScreen/__tests__/NameAccountScreen.spec.tsx b/apps/mobile/src/modules/onboarding/screens/NameAccountScreen/__tests__/NameAccountScreen.spec.tsx index 768c2d5e8..ed94a58bc 100644 --- a/apps/mobile/src/modules/onboarding/screens/NameAccountScreen/__tests__/NameAccountScreen.spec.tsx +++ b/apps/mobile/src/modules/onboarding/screens/NameAccountScreen/__tests__/NameAccountScreen.spec.tsx @@ -43,6 +43,11 @@ vi.mock('@hooks/useAppNavigation', () => ({ vi.mock('@perawallet/wallet-core-accounts', () => ({ useAllAccounts: () => [], + useAccountAuthAddresses: () => ({ + authAddresses: new Map(), + isPending: false, + isFetched: true, + }), useUpdateAccount: () => mockUpdateAccount, useCreateAccount: () => ({ createHdWalletAccount: mockCreateHdWalletAccount, diff --git a/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/__tests__/useSearchAccountsScreen.spec.tsx b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/__tests__/useSearchAccountsScreen.spec.tsx index 24b0aaa65..d0de8dfa4 100644 --- a/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/__tests__/useSearchAccountsScreen.spec.tsx +++ b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/__tests__/useSearchAccountsScreen.spec.tsx @@ -198,10 +198,12 @@ describe('useSearchAccountsScreen', () => { } const rekeyedAccounts = [ { - id: 'rekeyed-1', - address: 'REKEYED_ADDRESS', - type: AccountTypes.watch, - rekeyAddress: 'MOCK_ADDRESS', + account: { + id: 'rekeyed-1', + address: 'REKEYED_ADDRESS', + type: AccountTypes.watch, + }, + authAddress: 'MOCK_ADDRESS', }, ] mockDiscoverAccounts.mockResolvedValue([singleAccount]) diff --git a/apps/mobile/src/modules/onboarding/screens/WatchAccountScreen/__tests__/useWatchAccountScreen.spec.ts b/apps/mobile/src/modules/onboarding/screens/WatchAccountScreen/__tests__/useWatchAccountScreen.spec.ts index 613f4e47d..74dd584cf 100644 --- a/apps/mobile/src/modules/onboarding/screens/WatchAccountScreen/__tests__/useWatchAccountScreen.spec.ts +++ b/apps/mobile/src/modules/onboarding/screens/WatchAccountScreen/__tests__/useWatchAccountScreen.spec.ts @@ -44,6 +44,10 @@ vi.mock('@perawallet/wallet-core-accounts', async () => { } return selector(state) }, + useAccountBalancesInvalidator: () => ({ + invalidate: vi.fn(), + }), + fetchAndPersistAccount: vi.fn().mockResolvedValue(undefined), } }) diff --git a/apps/mobile/src/modules/onboarding/screens/WatchAccountScreen/useWatchAccountScreen.ts b/apps/mobile/src/modules/onboarding/screens/WatchAccountScreen/useWatchAccountScreen.ts index 48afd34c1..727737216 100644 --- a/apps/mobile/src/modules/onboarding/screens/WatchAccountScreen/useWatchAccountScreen.ts +++ b/apps/mobile/src/modules/onboarding/screens/WatchAccountScreen/useWatchAccountScreen.ts @@ -17,9 +17,14 @@ import { useAllAccounts, AccountTypes, WalletAccount, + fetchAndPersistAccount, + useAccountBalancesInvalidator, } from '@perawallet/wallet-core-accounts' -import { isValidAlgorandAddress } from '@perawallet/wallet-core-blockchain' -import { generateOrderedUniqueId } from '@perawallet/wallet-core-shared' +import { + isValidAlgorandAddress, + useNetwork, +} from '@perawallet/wallet-core-blockchain' +import { generateOrderedUniqueId, logger } from '@perawallet/wallet-core-shared' import { useNfdResolve } from '@hooks/useNfdResolve' type UseWatchAccountScreenResult = { @@ -38,6 +43,9 @@ export const useWatchAccountScreen = (): UseWatchAccountScreenResult => { const navigation = useAppNavigation() const accounts = useAllAccounts() const setAccounts = useAccountsStore(state => state.setAccounts) + const { network } = useNetwork() + const { invalidate: invalidateAccountBalances } = + useAccountBalancesInvalidator() const [address, setAddress] = useState('') const { resolvedAddress, isNfdResolved, isNfdResolving, nfdName } = @@ -63,10 +71,36 @@ export const useWatchAccountScreen = (): UseWatchAccountScreenResult => { } setAccounts([...accounts, newAccount]) + + // Eagerly pull the on-chain account info so the derived status can + // upgrade this watch account to rekeyed immediately if its + // auth-addr matches another wallet account. Any failure just falls + // back to the background sync service's next tick. + fetchAndPersistAccount(resolvedAddress, network) + .then(() => invalidateAccountBalances()) + .catch(error => { + logger.warn('Failed to prefetch watch account info', { + address: resolvedAddress, + network, + error: + error instanceof Error + ? { message: error.message, stack: error.stack } + : error, + }) + }) + navigation.push('NameAccount', { account: newAccount as WalletAccount, }) - }, [resolvedAddress, isDuplicateAddress, accounts, setAccounts, navigation]) + }, [ + resolvedAddress, + isDuplicateAddress, + accounts, + setAccounts, + navigation, + network, + invalidateAccountBalances, + ]) return { address, diff --git a/apps/mobile/src/modules/settings/screens/SettingsNotificationsScreen/__tests__/SettingsNotificationsScreen.spec.tsx b/apps/mobile/src/modules/settings/screens/SettingsNotificationsScreen/__tests__/SettingsNotificationsScreen.spec.tsx index 67cae3dae..0dd1aee1f 100644 --- a/apps/mobile/src/modules/settings/screens/SettingsNotificationsScreen/__tests__/SettingsNotificationsScreen.spec.tsx +++ b/apps/mobile/src/modules/settings/screens/SettingsNotificationsScreen/__tests__/SettingsNotificationsScreen.spec.tsx @@ -18,6 +18,11 @@ import { useSettingsNotificationsScreen } from '../useSettingsNotificationsScree vi.mock('@perawallet/wallet-core-accounts', () => ({ resolveAccountStatus: () => 'standard', useAllAccounts: () => [], + useAccountAuthAddresses: () => ({ + authAddresses: new Map(), + isPending: false, + isFetched: true, + }), getAccountDisplayName: (account: { name?: string }) => account?.name ?? 'Account', })) diff --git a/apps/mobile/src/modules/transactions/screens/send-funds/SelectDestinationScreen/useSelectDestinationScreen.ts b/apps/mobile/src/modules/transactions/screens/send-funds/SelectDestinationScreen/useSelectDestinationScreen.ts index 6dcba1a9c..2355b3a7f 100644 --- a/apps/mobile/src/modules/transactions/screens/send-funds/SelectDestinationScreen/useSelectDestinationScreen.ts +++ b/apps/mobile/src/modules/transactions/screens/send-funds/SelectDestinationScreen/useSelectDestinationScreen.ts @@ -16,6 +16,7 @@ import { canSignWithAccount, useAccountBalancesQuery, useAllAccounts, + useAccountAuthAddresses, useSelectedAccount, } from '@perawallet/wallet-core-accounts' import { ALGO_ASSET_ID, useAssetsQuery } from '@perawallet/wallet-core-assets' @@ -27,6 +28,7 @@ export const useSelectDestinationScreen = () => { const { selectedAssetId, setDestination, setSendMode } = useSendFunds() const selectedAccount = useSelectedAccount() const accounts = useAllAccounts() + const { authAddresses } = useAccountAuthAddresses() const { accountBalances } = useAccountBalancesQuery(accounts) const assetIDs = useMemo( @@ -72,7 +74,8 @@ export const useSelectDestinationScreen = () => { // Check if receiver is a local account we can sign for const localAccount = accounts.find(a => a.address === address) const isLocalSignable = - localAccount && canSignWithAccount(localAccount, accounts) + localAccount && + canSignWithAccount(localAccount, accounts, authAddresses) if (isLocalSignable) { // Express send: local account, we handle opt-in + transfer @@ -87,6 +90,7 @@ export const useSelectDestinationScreen = () => { [ selectedAsset, accounts, + authAddresses, accountBalances, setSendMode, setDestination, diff --git a/apps/mobile/src/modules/webview/hooks/__tests__/usePeraWebviewInterface.test.tsx b/apps/mobile/src/modules/webview/hooks/__tests__/usePeraWebviewInterface.test.tsx index 2fbf958a9..83011d96b 100644 --- a/apps/mobile/src/modules/webview/hooks/__tests__/usePeraWebviewInterface.test.tsx +++ b/apps/mobile/src/modules/webview/hooks/__tests__/usePeraWebviewInterface.test.tsx @@ -92,6 +92,11 @@ vi.mock('@perawallet/wallet-core-accounts', () => ({ getAccountDisplayName: vi.fn(account => account.name), isHDWalletAccount: vi.fn(account => account.type === 'standard'), isRekeyedAccount: vi.fn(() => false), + useAccountAuthAddresses: vi.fn(() => ({ + authAddresses: new Map(), + isPending: false, + isFetched: true, + })), useAllAccounts: vi.fn(() => [ { address: 'addr1', diff --git a/apps/mobile/src/modules/webview/hooks/__tests__/utils.test.ts b/apps/mobile/src/modules/webview/hooks/__tests__/utils.test.ts index 31b548143..f7c68c0e2 100644 --- a/apps/mobile/src/modules/webview/hooks/__tests__/utils.test.ts +++ b/apps/mobile/src/modules/webview/hooks/__tests__/utils.test.ts @@ -26,91 +26,113 @@ describe('webview/utils - getAccountType', () => { it('returns HdKey if hdWalletDetails is present', () => { expect( - getAccountType({ - ...baseAccount, - hdWalletDetails: {} as HDWalletDetails, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any), + getAccountType( + { + ...baseAccount, + hdWalletDetails: {} as HDWalletDetails, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + null, + ), ).toBe('HdKey') }) it('returns LedgerBle for a ledger hardware wallet account', () => { expect( - getAccountType({ - ...baseAccount, - type: 'hardware', - hardwareDetails: { - manufacturer: 'ledger', - deviceId: 'test-device', - deviceName: 'Ledger Nano X', - accountIndex: 0, + getAccountType( + { + ...baseAccount, + type: 'hardware', + hardwareDetails: { + manufacturer: 'ledger', + deviceId: 'test-device', + deviceName: 'Ledger Nano X', + accountIndex: 0, + }, }, - }), + null, + ), ).toBe('LedgerBle') }) it('returns RekeyedAuth if it is rekeyed and can sign', () => { expect( - getAccountType({ - ...baseAccount, - rekeyAddress: 'ADDR2', - keyPairId: 'pk', - }), + getAccountType( + { + ...baseAccount, + keyPairId: 'pk', + }, + 'ADDR2', + ), ).toBe('RekeyedAuth') }) it('returns Rekeyed if it is rekeyed but cannot sign', () => { expect( - getAccountType({ - ...baseAccount, - rekeyAddress: 'ADDR2', - keyPairId: undefined, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any), + getAccountType( + { + ...baseAccount, + keyPairId: undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + 'ADDR2', + ), ).toBe('Rekeyed') }) it('returns Algo25 for standard accounts without HD details', () => { expect( - getAccountType({ - ...baseAccount, - type: 'algo25', - keyPairId: 'pk', - }), + getAccountType( + { + ...baseAccount, + type: 'algo25', + keyPairId: 'pk', + }, + null, + ), ).toBe('Algo25') }) it('returns NoAuth for watch accounts', () => { expect( - getAccountType({ - ...baseAccount, - type: 'watch', - }), + getAccountType( + { + ...baseAccount, + type: 'watch', + }, + null, + ), ).toBe('NoAuth') }) it('returns Multisig for multisig accounts', () => { expect( - getAccountType({ - ...baseAccount, - type: 'multisig', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any), + getAccountType( + { + ...baseAccount, + type: 'multisig', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + null, + ), ).toBe('Multisig') }) it('returns HardwareBle for non-ledger hardware wallet accounts', () => { expect( - getAccountType({ - ...baseAccount, - type: 'hardware', - hardwareDetails: { - manufacturer: 'other', - deviceId: 'test-device', - deviceName: 'Test', - accountIndex: 0, + getAccountType( + { + ...baseAccount, + type: 'hardware', + hardwareDetails: { + manufacturer: 'other', + deviceId: 'test-device', + deviceName: 'Test', + accountIndex: 0, + }, }, - }), + null, + ), ).toBe('HardwareBle') }) }) diff --git a/apps/mobile/src/modules/webview/hooks/usePeraWebviewInterface.ts b/apps/mobile/src/modules/webview/hooks/usePeraWebviewInterface.ts index 105273479..be31bb8b0 100644 --- a/apps/mobile/src/modules/webview/hooks/usePeraWebviewInterface.ts +++ b/apps/mobile/src/modules/webview/hooks/usePeraWebviewInterface.ts @@ -24,6 +24,7 @@ import { import { getAccountDisplayName, useAllAccounts, + useAccountAuthAddresses, } from '@perawallet/wallet-core-accounts' import { useCurrency } from '@perawallet/wallet-core-currencies' import { useCallback } from 'react' @@ -72,6 +73,7 @@ export const usePeraWebviewInterface = ( ) => { const { showToast } = useToast() const accounts = useAllAccounts() + const { authAddresses } = useAccountAuthAddresses() const { network } = useNetwork() const deviceID = useDeviceID(network) const darkmode = useIsDarkMode() @@ -242,12 +244,12 @@ export const usePeraWebviewInterface = ( const payload = accounts.map(a => ({ name: getAccountDisplayName(a), address: a.address, - type: getAccountType(a), + type: getAccountType(a, authAddresses.get(a.address)), })) sendMessageToWebview(message.id, payload, webview) }) }, - [securedConnection, accounts, webview], + [securedConnection, accounts, authAddresses, webview], ) const getSettings = useCallback( diff --git a/apps/mobile/src/modules/webview/hooks/utils.ts b/apps/mobile/src/modules/webview/hooks/utils.ts index 30d39af69..c335b6d51 100644 --- a/apps/mobile/src/modules/webview/hooks/utils.ts +++ b/apps/mobile/src/modules/webview/hooks/utils.ts @@ -22,9 +22,13 @@ import { WalletAccount, } from '@perawallet/wallet-core-accounts' -export const getAccountType = (account: WalletAccount) => { - if (isRekeyedAccount(account) && !hasSigningKeys(account)) return 'Rekeyed' - if (isRekeyedAccount(account) && hasSigningKeys(account)) +export const getAccountType = ( + account: WalletAccount, + authAddress: string | null | undefined, +) => { + if (isRekeyedAccount(account, authAddress) && !hasSigningKeys(account)) + return 'Rekeyed' + if (isRekeyedAccount(account, authAddress) && hasSigningKeys(account)) return 'RekeyedAuth' if (isHDWalletAccount(account)) return 'HdKey' if (isLedgerAccount(account)) return 'LedgerBle' diff --git a/apps/mobile/vitest.setup.ts b/apps/mobile/vitest.setup.ts index 4c744250c..cff194877 100644 --- a/apps/mobile/vitest.setup.ts +++ b/apps/mobile/vitest.setup.ts @@ -1992,6 +1992,11 @@ vi.mock('@perawallet/wallet-core-settings', () => { vi.mock('@perawallet/wallet-core-accounts', () => { return { useAllAccounts: vi.fn(() => []), + useAccountAuthAddresses: vi.fn(() => ({ + authAddresses: new Map(), + isPending: false, + isFetched: true, + })), useAccountDiscovery: vi.fn(() => ({ discoverRekeyedAccounts: vi.fn(), })), @@ -2022,7 +2027,19 @@ vi.mock('@perawallet/wallet-core-accounts', () => { account?.type === 'hardware' && account?.hardwareDetails?.manufacturer === 'ledger', ), - isRekeyedAccount: vi.fn((account: any) => !!account?.rekeyAddress), + isRekeyedAccount: vi.fn( + (_account: any, authAddress: string | null | undefined) => + !!authAddress, + ), + resolveAccountStatus: vi.fn(() => 'standard'), + resolveAuthAccount: vi.fn((account: any) => account), + isSigningAccount: vi.fn( + (account: any) => !!account?.keyPairId && account?.type !== 'watch', + ), + fetchAndPersistAccount: vi.fn().mockResolvedValue(undefined), + useAccountBalancesInvalidator: vi.fn(() => ({ + invalidate: vi.fn(), + })), isHDWalletAccount: vi.fn((account: any) => !!account?.hdWalletDetails), isAlgo25Account: vi.fn((account: any) => account?.type === 'algo25'), isWatchAccount: vi.fn((account: any) => account?.type === 'watch'), diff --git a/packages/accounts/src/__tests__/account-discovery.spec.ts b/packages/accounts/src/__tests__/account-discovery.spec.ts index 274b716f7..f874624da 100644 --- a/packages/accounts/src/__tests__/account-discovery.spec.ts +++ b/packages/accounts/src/__tests__/account-discovery.spec.ts @@ -231,8 +231,8 @@ describe('discoverRekeyedAccounts', () => { accountGapLimit: 1, }) - expect(accounts[0].address).toBe('REKEYED_ACC_1') - expect(accounts[0].rekeyAddress).toBe('ADDRESS_0_0') + expect(accounts[0].account.address).toBe('REKEYED_ACC_1') + expect(accounts[0].authAddress).toBe('ADDRESS_0_0') }) it('should use provided accountAddresses instead of HD derivation', async () => { @@ -264,7 +264,7 @@ describe('discoverRekeyedAccounts', () => { }) expect(accounts).toHaveLength(1) - expect(accounts[0].address).toBe('REKEYED_FROM_EXPLICIT') - expect(accounts[0].rekeyAddress).toBe('EXPLICIT_ADDRESS') + expect(accounts[0].account.address).toBe('REKEYED_FROM_EXPLICIT') + expect(accounts[0].authAddress).toBe('EXPLICIT_ADDRESS') }) }) diff --git a/packages/accounts/src/__tests__/utils.test.ts b/packages/accounts/src/__tests__/utils.test.ts index 37e14364e..3bae50d03 100644 --- a/packages/accounts/src/__tests__/utils.test.ts +++ b/packages/accounts/src/__tests__/utils.test.ts @@ -151,14 +151,11 @@ describe('services/accounts/utils - account type checks', () => { ).toBe(false) }) - test('isRekeyedAccount returns true if rekeyAddress is present', () => { - expect(isRekeyedAccount(baseAccount)).toBe(false) - expect( - isRekeyedAccount({ - ...baseAccount, - rekeyAddress: 'ADDR2', - } as any), - ).toBe(true) + test('isRekeyedAccount returns true when authAddress differs from account address', () => { + expect(isRekeyedAccount(baseAccount, null)).toBe(false) + expect(isRekeyedAccount(baseAccount, undefined)).toBe(false) + expect(isRekeyedAccount(baseAccount, baseAccount.address)).toBe(false) + expect(isRekeyedAccount(baseAccount, 'ADDR2')).toBe(true) }) test('isAlgo25Account returns true if type is algo25', () => { @@ -214,7 +211,7 @@ describe('services/accounts/utils - account type checks', () => { }) test('canSignWithAccount returns true for account with keyPairId', () => { - expect(canSignWithAccount(baseAccount, [])).toBe(true) + expect(canSignWithAccount(baseAccount, [], new Map())).toBe(true) }) test('canSignWithAccount returns false for account without keyPairId', () => { @@ -222,6 +219,7 @@ describe('services/accounts/utils - account type checks', () => { canSignWithAccount( { ...baseAccount, keyPairId: undefined } as any, [], + new Map(), ), ).toBe(false) }) @@ -238,10 +236,13 @@ describe('services/accounts/utils - account type checks', () => { id: '3', type: 'watch', address: 'REKEYED_ADDR', - rekeyAddress: 'AUTH_ADDR', } as any - expect(canSignWithAccount(rekeyedAccount, [authAccount])).toBe(true) + const authAddresses = new Map([[rekeyedAccount.address, 'AUTH_ADDR']]) + + expect( + canSignWithAccount(rekeyedAccount, [authAccount], authAddresses), + ).toBe(true) }) test('canSignWithAccount returns false for rekeyed account when auth account has no keys', () => { @@ -255,10 +256,13 @@ describe('services/accounts/utils - account type checks', () => { id: '3', type: 'watch', address: 'REKEYED_ADDR', - rekeyAddress: 'AUTH_ADDR', } as any - expect(canSignWithAccount(rekeyedAccount, [authAccount])).toBe(false) + const authAddresses = new Map([[rekeyedAccount.address, 'AUTH_ADDR']]) + + expect( + canSignWithAccount(rekeyedAccount, [authAccount], authAddresses), + ).toBe(false) }) test('canSignWithAccount returns false for rekeyed account when auth account is not in list', () => { @@ -266,10 +270,11 @@ describe('services/accounts/utils - account type checks', () => { id: '3', type: 'watch', address: 'REKEYED_ADDR', - rekeyAddress: 'AUTH_ADDR', } as any - - expect(canSignWithAccount(rekeyedAccount, [])).toBe(false) + const authAddresses = new Map([[rekeyedAccount.address, 'AUTH_ADDR']]) + expect(canSignWithAccount(rekeyedAccount, [], authAddresses)).toBe( + false, + ) }) test('canSignWithAccount handles rekey chain', () => { @@ -284,18 +289,48 @@ describe('services/accounts/utils - account type checks', () => { id: '2', type: 'watch', address: 'MIDDLE_ADDR', - rekeyAddress: 'ROOT_ADDR', } as any const leafAccount = { id: '3', type: 'watch', address: 'LEAF_ADDR', - rekeyAddress: 'MIDDLE_ADDR', } as any const accounts = [rootAccount, middleAccount, leafAccount] - expect(canSignWithAccount(leafAccount, accounts)).toBe(true) + const authAddresses = new Map([ + [leafAccount.address, 'MIDDLE_ADDR'], + [middleAccount.address, 'ROOT_ADDR'], + ]) + expect(canSignWithAccount(leafAccount, accounts, authAddresses)).toBe( + true, + ) + }) + + test('canSignWithAccount handles circular rekey chain without stack overflow', () => { + const accountA = { + id: '1', + type: 'watch', + address: 'ADDR_A', + } as any + + const accountB = { + id: '2', + type: 'watch', + address: 'ADDR_B', + } as any + + const accounts = [accountA, accountB] + const authAddresses = new Map([ + [accountA.address, 'ADDR_B'], + [accountB.address, 'ADDR_A'], + ]) + expect(canSignWithAccount(accountA, accounts, authAddresses)).toBe( + false, + ) + expect(canSignWithAccount(accountB, accounts, authAddresses)).toBe( + false, + ) }) }) @@ -306,7 +341,7 @@ describe('services/accounts/utils - resolveAccountStatus', () => { address: 'ADDR', keyPairId: 'pk1', } as any - expect(resolveAccountStatus(account, [])).toBe('standard') + expect(resolveAccountStatus(account, [], null)).toBe('standard') }) test('returns hardware for hardware wallet account', () => { @@ -320,12 +355,12 @@ describe('services/accounts/utils - resolveAccountStatus', () => { accountIndex: 0, }, } as any - expect(resolveAccountStatus(account, [])).toBe('hardware') + expect(resolveAccountStatus(account, [], null)).toBe('hardware') }) test('returns watch for watch account', () => { const account = { type: 'watch', address: 'ADDR' } as any - expect(resolveAccountStatus(account, [])).toBe('watch') + expect(resolveAccountStatus(account, [], null)).toBe('watch') }) test('returns hdWallet for hdWallet account', () => { @@ -334,22 +369,21 @@ describe('services/accounts/utils - resolveAccountStatus', () => { address: 'ADDR', keyPairId: 'pk1', } as any - expect(resolveAccountStatus(account, [])).toBe('hdWallet') + expect(resolveAccountStatus(account, [], null)).toBe('hdWallet') }) test('returns multisig for multisig account', () => { const account = { type: 'multisig', address: 'ADDR' } as any - expect(resolveAccountStatus(account, [])).toBe('multisig') + expect(resolveAccountStatus(account, [], null)).toBe('multisig') }) test('returns noAuth for rekeyed account when auth account is not in wallet', () => { const account = { type: 'algo25', address: 'ADDR', - rekeyAddress: 'UNKNOWN', keyPairId: 'pk1', } as any - expect(resolveAccountStatus(account, [])).toBe('noAuth') + expect(resolveAccountStatus(account, [], 'UNKNOWN')).toBe('noAuth') }) test('returns rekeyedStandard for rekeyed account when auth is algo25', () => { @@ -361,10 +395,9 @@ describe('services/accounts/utils - resolveAccountStatus', () => { const account = { type: 'algo25', address: 'ADDR', - rekeyAddress: 'AUTH', keyPairId: 'pk1', } as any - expect(resolveAccountStatus(account, [authAccount])).toBe( + expect(resolveAccountStatus(account, [authAccount], 'AUTH')).toBe( 'rekeyedStandard', ) }) @@ -383,28 +416,41 @@ describe('services/accounts/utils - resolveAccountStatus', () => { const account = { type: 'algo25', address: 'ADDR', - rekeyAddress: 'AUTH', keyPairId: 'pk1', } as any - expect(resolveAccountStatus(account, [authAccount])).toBe( + expect(resolveAccountStatus(account, [authAccount], 'AUTH')).toBe( 'rekeyedHardware', ) }) + + test('watch account with auth address in wallet upgrades to rekeyedStandard', () => { + const authAccount = { + type: 'algo25', + address: 'AUTH', + keyPairId: 'pk1', + } as any + const watchAccount = { + type: 'watch', + address: 'WATCH_ADDR', + } as any + expect(resolveAccountStatus(watchAccount, [authAccount], 'AUTH')).toBe( + 'rekeyedStandard', + ) + }) }) describe('services/accounts/utils - isSigningAccount', () => { test('returns false for true watch account', () => { const account = { type: 'watch', address: 'ADDR' } as any - expect(isSigningAccount(account, [])).toBe(false) + expect(isSigningAccount(account, [], null)).toBe(false) }) test('returns false for rekeyed account without auth in wallet (noAuth)', () => { const account = { type: 'watch', address: 'ADDR', - rekeyAddress: 'MISSING_AUTH', } as any - expect(isSigningAccount(account, [])).toBe(false) + expect(isSigningAccount(account, [], 'MISSING_AUTH')).toBe(false) }) test('returns true for rekeyed account with auth present (rekeyedStandard)', () => { @@ -416,9 +462,8 @@ describe('services/accounts/utils - isSigningAccount', () => { const account = { type: 'watch', address: 'ADDR', - rekeyAddress: 'AUTH', } as any - expect(isSigningAccount(account, [authAccount])).toBe(true) + expect(isSigningAccount(account, [authAccount], 'AUTH')).toBe(true) }) test('returns true for rekeyed account with ledger auth present (rekeyedLedger)', () => { @@ -435,9 +480,8 @@ describe('services/accounts/utils - isSigningAccount', () => { const account = { type: 'watch', address: 'ADDR', - rekeyAddress: 'AUTH', } as any - expect(isSigningAccount(account, [authAccount])).toBe(true) + expect(isSigningAccount(account, [authAccount], 'AUTH')).toBe(true) }) test('returns true for standard account', () => { @@ -446,7 +490,7 @@ describe('services/accounts/utils - isSigningAccount', () => { address: 'ADDR', keyPairId: 'pk1', } as any - expect(isSigningAccount(account, [])).toBe(true) + expect(isSigningAccount(account, [], null)).toBe(true) }) }) diff --git a/packages/accounts/src/account-discovery.ts b/packages/accounts/src/account-discovery.ts index b1c529e9b..95c79e966 100644 --- a/packages/accounts/src/account-discovery.ts +++ b/packages/accounts/src/account-discovery.ts @@ -218,6 +218,11 @@ export async function discoverAccounts({ }) } +export type DiscoveredRekeyedAccount = { + account: WalletAccount + authAddress: string +} + async function checkRekeyed( algorandClient: AlgorandClient, address: string, @@ -248,8 +253,8 @@ async function scanRekeyedKeys({ session, derivationType, algorandClient, -}: ScanRekeyedKeysParams): Promise { - const foundAccounts: WalletAccount[] = [] +}: ScanRekeyedKeysParams): Promise { + const foundAccounts: DiscoveredRekeyedAccount[] = [] let keyGap = 0 let keyIdx = 0 @@ -272,11 +277,15 @@ async function scanRekeyedKeys({ ) return rekeyedAccounts.map( - (account: { address: string }): WatchAccount => ({ - id: generateOrderedUniqueId(), - address: account.address, - type: AccountTypes.watch, - rekeyAddress: address, + (account: { + address: string + }): DiscoveredRekeyedAccount => ({ + account: { + id: generateOrderedUniqueId(), + address: account.address, + type: AccountTypes.watch, + } as WatchAccount, + authAddress: address, }), ) }) @@ -308,7 +317,7 @@ export async function discoverRekeyedAccounts({ accountGapLimit = ACCOUNT_GAP_LIMIT, keyIndexGapLimit = KEY_INDEX_GAP_LIMIT, accountAddresses, -}: DiscoverAccountsParams): Promise { +}: DiscoverAccountsParams): Promise { const algorandClient = getAlgorandClient() if (accountAddresses && accountAddresses.length > 0) { @@ -316,11 +325,13 @@ export async function discoverRekeyedAccounts({ const rekeyedAccounts = await checkRekeyed(algorandClient, address) return rekeyedAccounts.map( - (account: { address: string }): WalletAccount => ({ - id: generateOrderedUniqueId(), - address: account.address, - type: AccountTypes.watch, - rekeyAddress: address, + (account: { address: string }): DiscoveredRekeyedAccount => ({ + account: { + id: generateOrderedUniqueId(), + address: account.address, + type: AccountTypes.watch, + } as WatchAccount, + authAddress: address, }), ) }) @@ -329,7 +340,7 @@ export async function discoverRekeyedAccounts({ return results.flat() } - const foundAccounts: WalletAccount[] = [] + const foundAccounts: DiscoveredRekeyedAccount[] = [] let accountGap = 0 let accountIndex = 0 diff --git a/packages/accounts/src/db/index.ts b/packages/accounts/src/db/index.ts index 3f894a7bd..7f25652c6 100644 --- a/packages/accounts/src/db/index.ts +++ b/packages/accounts/src/db/index.ts @@ -17,6 +17,7 @@ export { insertAssetHolding, deleteAssetHoldings, upsertAccountBalance, + seedAuthAddress, getAccountBalance, getAllAccountBalances, getAllAssetIdsForNetwork, diff --git a/packages/accounts/src/db/repository.ts b/packages/accounts/src/db/repository.ts index 8f7cb86a8..bfbaebcb9 100644 --- a/packages/accounts/src/db/repository.ts +++ b/packages/accounts/src/db/repository.ts @@ -283,6 +283,56 @@ export async function upsertAccountBalance({ .run() } +type SeedAuthAddressParams = { + db?: Database + accountAddress: string + network: string + authAddress: string +} + +/** + * Seed an auth address into the DB without a full on-chain fetch. + * Used during import when the auth address is already known from + * discovery. If a row already exists, only the auth_address column + * is updated; otherwise a minimal placeholder row is inserted so + * the auth address is available before the background sync fills + * the rest. + */ +export async function seedAuthAddress({ + db = getDatabase(), + accountAddress, + network, + authAddress, +}: SeedAuthAddressParams): Promise { + const now = Date.now() + + await db + .insert(AccountBalancesSchema) + .values({ + accountAddress, + network, + algoBalance: new Decimal(0), + totalAssetsOptedIn: 0, + totalCreatedAssets: 0, + totalAppsOptedIn: 0, + minBalance: new Decimal(0), + status: 'Offline', + authAddress, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: [ + AccountBalancesSchema.accountAddress, + AccountBalancesSchema.network, + ], + set: { + authAddress, + updatedAt: now, + }, + }) + .run() +} + type GetAccountBalanceParams = { db?: Database accountAddress: string diff --git a/packages/accounts/src/hooks/__tests__/useSigningAccounts.test.ts b/packages/accounts/src/hooks/__tests__/useSigningAccounts.test.ts index fb3cdd39c..8ffe9df70 100644 --- a/packages/accounts/src/hooks/__tests__/useSigningAccounts.test.ts +++ b/packages/accounts/src/hooks/__tests__/useSigningAccounts.test.ts @@ -19,6 +19,14 @@ vi.mock('../../store', () => ({ useAccountsStore: vi.fn(), })) +vi.mock('../useAccountAuthAddresses', () => ({ + useAccountAuthAddresses: () => ({ + authAddresses: new Map(), + isPending: false, + isFetched: true, + }), +})) + describe('useSigningAccounts', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/packages/accounts/src/hooks/index.ts b/packages/accounts/src/hooks/index.ts index 90415fa73..75ccd0f23 100644 --- a/packages/accounts/src/hooks/index.ts +++ b/packages/accounts/src/hooks/index.ts @@ -10,6 +10,7 @@ limitations under the License */ +export * from './useAccountAuthAddresses' export * from './useAccountBalancesQuery' export * from './useAccountInformationQuery' export * from './usePortfolioTotals' diff --git a/packages/accounts/src/hooks/querykeys.ts b/packages/accounts/src/hooks/querykeys.ts index 71efe439e..531ddb02c 100644 --- a/packages/accounts/src/hooks/querykeys.ts +++ b/packages/accounts/src/hooks/querykeys.ts @@ -34,6 +34,12 @@ export const getAccountBalancesQueryKey = ( return [MODULE_PREFIX, 'balance', { address, network, filters }] } +export const getAccountAuthAddressesQueryKey = (network: Network) => [ + MODULE_PREFIX, + 'auth-addresses', + { network }, +] + export const getAccountBalancesHistoryQueryKey = ( addresses: AccountAddress[], period: HistoryPeriod, @@ -56,7 +62,8 @@ export const getAccountAssetBalanceHistoryQueryKey = ( export const getInvalidateAccountBalancesPredicate = (query: Query) => query.queryKey.length >= 2 && query.queryKey.at(0) === MODULE_PREFIX && - query.queryKey.at(1) === 'balance' + (query.queryKey.at(1) === 'balance' || + query.queryKey.at(1) === 'auth-addresses') export function invalidateAccountQueries(queryClient: QueryClient): void { void queryClient.invalidateQueries({ diff --git a/packages/accounts/src/hooks/useAccountAuthAddresses.ts b/packages/accounts/src/hooks/useAccountAuthAddresses.ts new file mode 100644 index 000000000..38578fa60 --- /dev/null +++ b/packages/accounts/src/hooks/useAccountAuthAddresses.ts @@ -0,0 +1,51 @@ +/* + Copyright 2022-2025 Pera Wallet, LDA + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License + */ + +import { useQuery } from '@tanstack/react-query' +import { useNetwork } from '@perawallet/wallet-core-blockchain' +import { getAllAccountBalances } from '../db' +import type { AuthAddressLookup } from '../utils' +import { getAccountAuthAddressesQueryKey } from './querykeys' +import { useAllAccounts } from './useAllAccounts' + +type UseAccountAuthAddressesResult = { + authAddresses: AuthAddressLookup + isPending: boolean + isFetched: boolean +} + +export const useAccountAuthAddresses = (): UseAccountAuthAddressesResult => { + const { network } = useNetwork() + const accounts = useAllAccounts() + + const addresses = accounts.map(a => a.address) + const addressKey = [...addresses].sort().join(',') + + const query = useQuery({ + queryKey: [...getAccountAuthAddressesQueryKey(network), { addressKey }], + queryFn: async (): Promise => { + if (addresses.length === 0) return new Map() + const rows = await getAllAccountBalances({ + accountAddresses: addresses, + network, + }) + return new Map(rows.map(r => [r.accountAddress, r.authAddress])) + }, + staleTime: Infinity, + }) + + return { + authAddresses: query.data ?? new Map(), + isPending: query.isPending, + isFetched: query.isFetched, + } +} diff --git a/packages/accounts/src/hooks/useAccountDiscovery.ts b/packages/accounts/src/hooks/useAccountDiscovery.ts index 518ec12f0..68d2c932f 100644 --- a/packages/accounts/src/hooks/useAccountDiscovery.ts +++ b/packages/accounts/src/hooks/useAccountDiscovery.ts @@ -15,6 +15,7 @@ import { useKMS } from '@perawallet/wallet-core-kms' import { discoverAccounts as baseDiscoverAccounts, discoverRekeyedAccounts as baseDiscoverRekeyedAccounts, + type DiscoveredRekeyedAccount, } from '../account-discovery' import { BIP32DerivationType } from '@algorandfoundation/xhd-wallet-api' import { KEY_DOMAIN } from '../constants' @@ -47,7 +48,7 @@ export const useAccountDiscovery = () => { accountGapLimit?: number keyIndexGapLimit?: number accountAddresses?: string[] - }) => { + }): Promise => { // When account addresses are provided (e.g. Algo25 imports), // no HD session is needed — just check rekeyed accounts by address. if (params.accountAddresses && params.accountAddresses.length > 0) { diff --git a/packages/accounts/src/hooks/useSigningAccounts.ts b/packages/accounts/src/hooks/useSigningAccounts.ts index 1c140a992..81672d569 100644 --- a/packages/accounts/src/hooks/useSigningAccounts.ts +++ b/packages/accounts/src/hooks/useSigningAccounts.ts @@ -13,11 +13,20 @@ import { useMemo } from 'react' import { useAccountsStore } from '../store' import { isSigningAccount } from '../utils' +import { useAccountAuthAddresses } from './useAccountAuthAddresses' export const useSigningAccounts = () => { const accounts = useAccountsStore(state => state.accounts) + const { authAddresses } = useAccountAuthAddresses() return useMemo( - () => accounts.filter(account => isSigningAccount(account, accounts)), - [accounts], + () => + accounts.filter(account => + isSigningAccount( + account, + accounts, + authAddresses.get(account.address), + ), + ), + [accounts, authAddresses], ) } diff --git a/packages/accounts/src/models/accounts.ts b/packages/accounts/src/models/accounts.ts index ad2362a18..53f571e82 100644 --- a/packages/accounts/src/models/accounts.ts +++ b/packages/accounts/src/models/accounts.ts @@ -68,7 +68,6 @@ export type BaseWalletAccount = { type: AccountType address: string keyPairId?: string - rekeyAddress?: string } export type Algo25Account = BaseWalletAccount & { diff --git a/packages/accounts/src/store/store.ts b/packages/accounts/src/store/store.ts index 0c46adac2..279404eb7 100644 --- a/packages/accounts/src/store/store.ts +++ b/packages/accounts/src/store/store.ts @@ -89,7 +89,7 @@ export const useAccountsStore: UseBoundStore< { name: STORE_NAME, storage: createJSONStorage(() => getProvider().keyValueStorage), - version: 2, + version: 3, partialize: state => ({ accounts: state.accounts, selectedAccountAddress: state.selectedAccountAddress, @@ -103,6 +103,20 @@ export const useAccountsStore: UseBoundStore< state.sortMode = 'manual' state.manualAccountOrder = accounts.map(a => a.address) } + if (version < 3) { + // v3: rekeyAddress is no longer persisted on the account + // record. The dynamic auth address from the chain is now + // the single source of truth, stored in the SQLite + // account_balances row and refreshed by the background + // sync service. Drop the stale static field. + const accounts = (state.accounts ?? []) as Array< + Record + > + state.accounts = accounts.map(acc => { + const { rekeyAddress: _ignored, ...rest } = acc + return rest + }) + } return state as AccountsState }, }, diff --git a/packages/accounts/src/utils.ts b/packages/accounts/src/utils.ts index e6279d6e1..b9f337424 100644 --- a/packages/accounts/src/utils.ts +++ b/packages/accounts/src/utils.ts @@ -52,8 +52,11 @@ export const isLedgerAccount = ( ) } -export const isRekeyedAccount = (account: WalletAccount) => { - return !!account.rekeyAddress +export const isRekeyedAccount = ( + account: WalletAccount, + authAddress: string | null | undefined, +): boolean => { + return !!authAddress && authAddress !== account.address } export const isAlgo25Account = ( @@ -78,16 +81,32 @@ export const hasSigningKeys = (account: WalletAccount): boolean => { return !!account.keyPairId } +export type AuthAddressLookup = ReadonlyMap + export const canSignWithAccount = ( account: WalletAccount, accounts: WalletAccount[], + authAddresses: AuthAddressLookup, + visited: Set = new Set(), ): boolean => { if (hasSigningKeys(account)) return true - if (account.rekeyAddress) { - const authAccount = accounts.find( - a => a.address === account.rekeyAddress, - ) - if (authAccount) return canSignWithAccount(authAccount, accounts) + const authAddress = authAddresses.get(account.address) + if ( + authAddress && + authAddress !== account.address && + !visited.has(authAddress) + ) { + const authAccount = accounts.find(a => a.address === authAddress) + if (authAccount) { + const nextVisited = new Set(visited) + nextVisited.add(account.address) + return canSignWithAccount( + authAccount, + accounts, + authAddresses, + nextVisited, + ) + } } return false } @@ -105,11 +124,10 @@ export type AccountStatus = export const resolveAccountStatus = ( account: WalletAccount, accounts: WalletAccount[], + authAddress: string | null | undefined, ): AccountStatus => { - if (isRekeyedAccount(account)) { - const authAccount = accounts.find( - a => a.address === account.rekeyAddress, - ) + if (isRekeyedAccount(account, authAddress)) { + const authAccount = accounts.find(a => a.address === authAddress) if (!authAccount) return 'noAuth' if (isHardwareWalletAccount(authAccount)) return 'rekeyedHardware' return 'rekeyedStandard' @@ -130,30 +148,32 @@ export const resolveAccountStatus = ( export const isSigningAccount = ( account: WalletAccount, accounts: WalletAccount[], + authAddress: string | null | undefined, ): boolean => { - const status = resolveAccountStatus(account, accounts) + const status = resolveAccountStatus(account, accounts, authAddress) return status !== 'watch' && status !== 'noAuth' } /** * Resolve the auth account for a given account. - * If the account is rekeyed, returns the rekey target. - * Only follows one level to prevent circular references. + * If the account is rekeyed (chain auth address differs), returns the + * rekey target from the wallet. Only follows one level to prevent + * circular references. Returns the account itself when not rekeyed. + * Throws RekeyTargetNotFoundError if the auth address is not in the wallet. */ export const resolveAuthAccount = ( account: WalletAccount, allAccounts: WalletAccount[], + authAddress: string | null | undefined, ): WalletAccount => { - if (!account.rekeyAddress) { + if (!authAddress || authAddress === account.address) { return account } - const rekeyTarget = allAccounts.find( - a => a.address === account.rekeyAddress, - ) + const rekeyTarget = allAccounts.find(a => a.address === authAddress) if (!rekeyTarget) { - throw new RekeyTargetNotFoundError(account.rekeyAddress) + throw new RekeyTargetNotFoundError(authAddress) } return rekeyTarget diff --git a/packages/signing/src/hooks/__tests__/useArbitraryDataSigner.spec.ts b/packages/signing/src/hooks/__tests__/useArbitraryDataSigner.spec.ts index 5012b711e..63c19dfe3 100644 --- a/packages/signing/src/hooks/__tests__/useArbitraryDataSigner.spec.ts +++ b/packages/signing/src/hooks/__tests__/useArbitraryDataSigner.spec.ts @@ -30,9 +30,15 @@ vi.mock('@perawallet/wallet-core-kms', () => ({ const mockIsHDWalletAccount = vi.fn() const mockIsAlgo25Account = vi.fn() let mockAccounts: WalletAccount[] = [] +let mockAuthAddresses: Map = new Map() vi.mock('@perawallet/wallet-core-accounts', () => ({ useAccountsStore: (selector: any) => selector({ accounts: mockAccounts }), + useAccountAuthAddresses: () => ({ + authAddresses: mockAuthAddresses, + isPending: false, + isFetched: true, + }), isHDWalletAccount: (...args: any[]) => mockIsHDWalletAccount(...args), isAlgo25Account: (...args: any[]) => mockIsAlgo25Account(...args), })) @@ -66,6 +72,7 @@ describe('useArbitraryDataSigner', () => { beforeEach(() => { vi.clearAllMocks() mockAccounts = [] + mockAuthAddresses = new Map() mockGetKeyOrThrow.mockReturnValue(mockKey) mockIsHDWalletAccount.mockReturnValue(false) mockIsAlgo25Account.mockReturnValue(false) @@ -237,10 +244,10 @@ describe('useArbitraryDataSigner', () => { const originalAccount = { ...algo25Account, address: 'ORIGINAL_ADDR', - rekeyAddress: 'REKEYED_ADDR', } as unknown as WalletAccount mockAccounts = [rekeyedAccount as unknown as WalletAccount] + mockAuthAddresses = new Map([['ORIGINAL_ADDR', 'REKEYED_ADDR']]) mockIsAlgo25Account.mockReturnValue(true) mockWithAlgo25Session.mockResolvedValue([new Uint8Array([42])]) @@ -257,10 +264,10 @@ describe('useArbitraryDataSigner', () => { const originalAccount = { ...algo25Account, address: 'ORIGINAL_ADDR', - rekeyAddress: 'MISSING_ADDR', } as unknown as WalletAccount mockAccounts = [] + mockAuthAddresses = new Map([['ORIGINAL_ADDR', 'MISSING_ADDR']]) const { result } = renderHook(() => useArbitraryDataSigner()) diff --git a/packages/signing/src/hooks/__tests__/useSigningRequest.spec.ts b/packages/signing/src/hooks/__tests__/useSigningRequest.spec.ts index 804787513..d2079c8a8 100644 --- a/packages/signing/src/hooks/__tests__/useSigningRequest.spec.ts +++ b/packages/signing/src/hooks/__tests__/useSigningRequest.spec.ts @@ -55,6 +55,11 @@ vi.mock('@perawallet/wallet-core-accounts', () => ({ { address: 'ADDR1', type: 'algo25' }, { address: 'ADDR2', type: 'algo25' }, ]), + useAccountAuthAddresses: vi.fn(() => ({ + authAddresses: new Map(), + isPending: false, + isFetched: true, + })), })) vi.mock('@perawallet/wallet-core-blockchain', async importOriginal => { diff --git a/packages/signing/src/hooks/useArbitraryDataSigner.ts b/packages/signing/src/hooks/useArbitraryDataSigner.ts index 37293c8ef..60b02abb2 100644 --- a/packages/signing/src/hooks/useArbitraryDataSigner.ts +++ b/packages/signing/src/hooks/useArbitraryDataSigner.ts @@ -10,7 +10,10 @@ limitations under the License */ -import { useAccountsStore } from '@perawallet/wallet-core-accounts' +import { + useAccountsStore, + useAccountAuthAddresses, +} from '@perawallet/wallet-core-accounts' import { useKMS } from '@perawallet/wallet-core-kms' import { useCallback } from 'react' import { @@ -27,6 +30,7 @@ import { SIGNING_KEY_DOMAIN } from '../constants' export const useArbitraryDataSigner = () => { const accounts = useAccountsStore(state => state.accounts) + const { authAddresses } = useAccountAuthAddresses() const { getKeyOrThrow, withHDSession, withAlgo25Session } = useKMS() const signHDWalletArbitraryData = useCallback( @@ -96,16 +100,31 @@ export const useArbitraryDataSigner = () => { account: WalletAccount, data: string | string[], ): Promise => { - if (account.rekeyAddress) { + const authAddress = authAddresses.get(account.address) + if (authAddress && authAddress !== account.address) { const rekeyedAccount = - accounts.find(a => a.address === account.rekeyAddress) ?? - null + accounts.find(a => a.address === authAddress) ?? null if (!rekeyedAccount) { return Promise.reject( - `No rekeyed account found for ${account.rekeyAddress}`, + `No rekeyed account found for ${authAddress}`, + ) + } + + if (isHDWalletAccount(rekeyedAccount)) { + return signHDWalletArbitraryData( + rekeyedAccount as HDWalletAccount, + data, + ) + } + if (isAlgo25Account(rekeyedAccount)) { + return signAlgo25ArbitraryData( + rekeyedAccount as Algo25Account, + data, ) } - return signArbitraryData(rekeyedAccount, data) + return Promise.reject( + `Unsupported auth account type ${rekeyedAccount.type} for ${rekeyedAccount.address}`, + ) } if (isHDWalletAccount(account)) { @@ -123,7 +142,12 @@ export const useArbitraryDataSigner = () => { `Unsupported account type ${account.type} for ${account.address}`, ) }, - [accounts, signHDWalletArbitraryData, signAlgo25ArbitraryData], + [ + accounts, + authAddresses, + signHDWalletArbitraryData, + signAlgo25ArbitraryData, + ], ) return { diff --git a/packages/signing/src/hooks/useSigningActorLifecycle.ts b/packages/signing/src/hooks/useSigningActorLifecycle.ts index 8d28ce268..1b13cf965 100644 --- a/packages/signing/src/hooks/useSigningActorLifecycle.ts +++ b/packages/signing/src/hooks/useSigningActorLifecycle.ts @@ -18,7 +18,10 @@ import { useTransactionEncoder, useNetwork, } from '@perawallet/wallet-core-blockchain' -import { useAllAccounts } from '@perawallet/wallet-core-accounts' +import { + useAllAccounts, + useAccountAuthAddresses, +} from '@perawallet/wallet-core-accounts' import { getProvider } from '@perawallet/wallet-extension-provider' import { useTransactionSigner } from './useTransactionSigner' import { useSigningStore } from '../store' @@ -84,6 +87,7 @@ export const useSigningActorLifecycle = (): UseSigningActorLifecycleResult => { const algokit = useAlgorandClient() const { network } = useNetwork() const allAccounts = useAllAccounts() + const { authAddresses } = useAccountAuthAddresses() // Actor refs stored in a Map ref — ephemeral, not persisted, no re-renders const actorRefsMap = useRef(new Map()) @@ -122,6 +126,7 @@ export const useSigningActorLifecycle = (): UseSigningActorLifecycleResult => { const actor = createSigningMachine( request, allAccounts, + authAddresses, buildDeps(), ) @@ -163,7 +168,7 @@ export const useSigningActorLifecycle = (): UseSigningActorLifecycleResult => { actor.start() actorRefsMap.current.set(request.id, actor) }, - [allAccounts, buildDeps], + [allAccounts, authAddresses, buildDeps], ) // Ref so the effect always has the latest createActorForRequest diff --git a/packages/signing/src/hooks/useSigningPipeline.ts b/packages/signing/src/hooks/useSigningPipeline.ts index a05fcaad5..ae1fa620f 100644 --- a/packages/signing/src/hooks/useSigningPipeline.ts +++ b/packages/signing/src/hooks/useSigningPipeline.ts @@ -17,6 +17,7 @@ import { mapToDisplayableTransaction } from '@perawallet/wallet-core-blockchain' import { canSignWithAccount, useAllAccounts, + useAccountAuthAddresses, } from '@perawallet/wallet-core-accounts' import type { PipelineStage, TransactionSignRequest } from '../models' import { @@ -62,6 +63,7 @@ export const useSigningPipeline = ( } = useSigningRequest() const accounts = useAllAccounts() + const { authAddresses } = useAccountAuthAddresses() // ------------------------------------------------------------------------- // Display data — computed from the request's transaction payload. @@ -82,7 +84,7 @@ export const useSigningPipeline = ( const signableAddresses = new Set( accounts - .filter(a => canSignWithAccount(a, accounts)) + .filter(a => canSignWithAccount(a, accounts, authAddresses)) .map(a => a.address), ) @@ -111,7 +113,7 @@ export const useSigningPipeline = ( distinctWarnings, requestStructure, } - }, [txRequest?.txs, accounts]) + }, [txRequest?.txs, accounts, authAddresses]) // ------------------------------------------------------------------------- // Machine state — derived from actor subscription diff --git a/packages/signing/src/hooks/useTransactionSigner.ts b/packages/signing/src/hooks/useTransactionSigner.ts index c7c0a90a0..ef1ce1f52 100644 --- a/packages/signing/src/hooks/useTransactionSigner.ts +++ b/packages/signing/src/hooks/useTransactionSigner.ts @@ -10,7 +10,10 @@ limitations under the License */ -import { useAccountsStore } from '@perawallet/wallet-core-accounts' +import { + useAccountsStore, + useAccountAuthAddresses, +} from '@perawallet/wallet-core-accounts' import { useKMS } from '@perawallet/wallet-core-kms' import { useCallback } from 'react' import { @@ -34,6 +37,7 @@ import { SIGNING_KEY_DOMAIN } from '../constants' export const useTransactionSigner = () => { const accounts = useAccountsStore(state => state.accounts) + const { authAddresses } = useAccountAuthAddresses() const { getKeyOrThrow, withHDSession, withAlgo25Session } = useKMS() const { encodeTransaction } = useTransactionEncoder() @@ -122,13 +126,14 @@ export const useTransactionSigner = () => { txns: PeraTransactionGroup, dontFollowRekey?: boolean, ): Promise => { - if (account.rekeyAddress && !dontFollowRekey) { + const authAddress = authAddresses.get(account.address) + const isRekeyed = !!authAddress && authAddress !== account.address + if (isRekeyed && !dontFollowRekey) { const rekeyedAccount = - accounts.find(a => a.address === account.rekeyAddress) ?? - null + accounts.find(a => a.address === authAddress) ?? null if (!rekeyedAccount) { return Promise.reject( - `No rekeyed account found for ${account.rekeyAddress}`, + `No rekeyed account found for ${authAddress}`, ) } //rekeys don't chain, so only follow rekeys for one level @@ -152,7 +157,12 @@ export const useTransactionSigner = () => { `Unsupported account type ${account.type} for ${account.address}`, ) }, - [accounts, signHDWalletTransactions, signAlgo25Transactions], + [ + accounts, + authAddresses, + signHDWalletTransactions, + signAlgo25Transactions, + ], ) const signTransactions = useCallback( diff --git a/packages/signing/src/machine/__tests__/signingMachine.spec.ts b/packages/signing/src/machine/__tests__/signingMachine.spec.ts index 80370bf18..c0387d9ef 100644 --- a/packages/signing/src/machine/__tests__/signingMachine.spec.ts +++ b/packages/signing/src/machine/__tests__/signingMachine.spec.ts @@ -90,6 +90,7 @@ const makeInput = ( ): SigningMachineInput => ({ request: mockRequest, allAccounts: [mockAlgo25Account], + authAddresses: new Map(), ...mockDeps, ...overrides, }) diff --git a/packages/signing/src/machine/actions.ts b/packages/signing/src/machine/actions.ts index 0abf58b1d..1597b278f 100644 --- a/packages/signing/src/machine/actions.ts +++ b/packages/signing/src/machine/actions.ts @@ -10,7 +10,10 @@ limitations under the License */ -import type { WalletAccount } from '@perawallet/wallet-core-accounts' +import type { + WalletAccount, + AuthAddressLookup, +} from '@perawallet/wallet-core-accounts' import { hasSigningKeys, isHardwareWalletAccount, @@ -79,6 +82,7 @@ const determineSignerType = ( const buildGroupSignerTypeMap = ( groups: SignableGroup[], allAccounts: WalletAccount[], + authAddresses: AuthAddressLookup, ): GroupSignerTypeMap => { const map: GroupSignerTypeMap = new Map() for (const group of groups) { @@ -89,7 +93,11 @@ const buildGroupSignerTypeMap = ( if (!signerAccount) { throw new Error(`Signer account not found: ${group.signerAddress}`) } - const authAccount = resolveAuthAccount(signerAccount, allAccounts) + const authAccount = resolveAuthAccount( + signerAccount, + allAccounts, + authAddresses.get(signerAccount.address), + ) map.set( group.signerAddress, determineSignerType(signerAccount, authAccount), @@ -273,7 +281,7 @@ const extractDeps = (input: SigningMachineInput): SigningMachineDeps => ({ export const resolveInitialContext = ( input: SigningMachineInput, ): SigningMachineContext => { - const { request, allAccounts } = input + const { request, allAccounts, authAddresses } = input const signableGroups = buildSignableGroups(request, allAccounts) @@ -288,6 +296,7 @@ export const resolveInitialContext = ( const groupSignerTypes = buildGroupSignerTypeMap( signableGroups, allAccounts, + authAddresses, ) const hasHardwareSigners = [...groupSignerTypes.values()].includes( @@ -300,6 +309,7 @@ export const resolveInitialContext = ( return { request, allAccounts, + authAddresses, signerAddress, groupSignerTypes, completedSignerTypes: [], @@ -323,6 +333,7 @@ export const makeFailedContext = ( ): SigningMachineContext => ({ request: input.request, allAccounts: input.allAccounts, + authAddresses: input.authAddresses, signerAddress: null, groupSignerTypes: null, completedSignerTypes: [], diff --git a/packages/signing/src/machine/actors/signers/__tests__/localKeySignerActor.spec.ts b/packages/signing/src/machine/actors/signers/__tests__/localKeySignerActor.spec.ts index 7cbb2a0c0..e67ec535d 100644 --- a/packages/signing/src/machine/actors/signers/__tests__/localKeySignerActor.spec.ts +++ b/packages/signing/src/machine/actors/signers/__tests__/localKeySignerActor.spec.ts @@ -53,6 +53,7 @@ describe('localKeySignerActor', () => { const input: LocalKeySignerActorInput = { groups: [mockGroup], allAccounts: [mockAlgo25Account], + authAddresses: new Map(), signTransactions, } @@ -83,6 +84,7 @@ describe('localKeySignerActor', () => { const input: LocalKeySignerActorInput = { groups: [mockGroup], allAccounts: [mockAlgo25Account], + authAddresses: new Map(), signTransactions, } @@ -109,6 +111,7 @@ describe('localKeySignerActor', () => { const input: LocalKeySignerActorInput = { groups: [groupForMultisig], allAccounts: [accountWithoutKeys], + authAddresses: new Map(), signTransactions, } @@ -124,6 +127,7 @@ describe('localKeySignerActor', () => { const input: LocalKeySignerActorInput = { groups: [mockGroup], allAccounts: [], // no matching account + authAddresses: new Map(), signTransactions, } diff --git a/packages/signing/src/machine/actors/signers/localKeySignerActor.ts b/packages/signing/src/machine/actors/signers/localKeySignerActor.ts index a2c81682e..58e6a9b43 100644 --- a/packages/signing/src/machine/actors/signers/localKeySignerActor.ts +++ b/packages/signing/src/machine/actors/signers/localKeySignerActor.ts @@ -11,7 +11,10 @@ */ import { fromPromise } from 'xstate' -import type { WalletAccount } from '@perawallet/wallet-core-accounts' +import type { + WalletAccount, + AuthAddressLookup, +} from '@perawallet/wallet-core-accounts' import type { AnalyzedSignableGroup, SigningResult, @@ -26,6 +29,7 @@ import { CannotSignError } from '../../../pipeline/errors' export type LocalKeySignerActorInput = { groups: AnalyzedSignableGroup[] allAccounts: WalletAccount[] + authAddresses: AuthAddressLookup signTransactions: LocalSigningFunction } @@ -39,7 +43,7 @@ export const localKeySignerActor = fromPromise< SigningResult[], LocalKeySignerActorInput >(async ({ input }) => { - const { groups, allAccounts, signTransactions } = input + const { groups, allAccounts, authAddresses, signTransactions } = input const strategy = createLocalKeyStrategy(signTransactions) return Promise.all( @@ -53,7 +57,11 @@ export const localKeySignerActor = fromPromise< 'Account not found in allAccounts', ) } - const authAccount = resolveAuthAccount(signerAccount, allAccounts) + const authAccount = resolveAuthAccount( + signerAccount, + allAccounts, + authAddresses.get(signerAccount.address), + ) return strategy.sign(group, authAccount) }), ) diff --git a/packages/signing/src/machine/context.ts b/packages/signing/src/machine/context.ts index 4370b9e22..a1451bc87 100644 --- a/packages/signing/src/machine/context.ts +++ b/packages/signing/src/machine/context.ts @@ -10,7 +10,10 @@ limitations under the License */ -import type { WalletAccount } from '@perawallet/wallet-core-accounts' +import type { + WalletAccount, + AuthAddressLookup, +} from '@perawallet/wallet-core-accounts' import type { Network } from '@perawallet/wallet-core-shared' import type { DataTransport, @@ -102,6 +105,14 @@ export type SigningMachineContext = { */ allAccounts: WalletAccount[] + /** + * Snapshot of the dynamic on-chain auth address for every account, + * captured at the moment the signing request was created. Used to + * resolve the auth account for rekeyed signers without touching the + * persisted `WalletAccount` record. + */ + authAddresses: AuthAddressLookup + /** * The primary signer address for this request (from the first group's sender). * Used for transport routing (e.g. detecting multisig propose vs algod). @@ -197,4 +208,5 @@ export type SigningMachineEvent = export type SigningMachineInput = { request: SignRequest allAccounts: WalletAccount[] + authAddresses: AuthAddressLookup } & SigningMachineDeps diff --git a/packages/signing/src/machine/createSigningMachine.ts b/packages/signing/src/machine/createSigningMachine.ts index 7fe0da999..46014997d 100644 --- a/packages/signing/src/machine/createSigningMachine.ts +++ b/packages/signing/src/machine/createSigningMachine.ts @@ -11,7 +11,10 @@ */ import { createActor } from 'xstate' -import type { WalletAccount } from '@perawallet/wallet-core-accounts' +import type { + WalletAccount, + AuthAddressLookup, +} from '@perawallet/wallet-core-accounts' import type { SignRequest } from '../models' import type { SigningMachineDeps } from './context' import { signingMachine } from './signingMachine' @@ -22,16 +25,18 @@ import { signingMachine } from './signingMachine' * Called from useSigningRequest (Phase 6) where all runtime deps * (KMS functions, AlgoKit client, etc.) are available via React hooks. * - * @param request The sign request to process - * @param allAccounts All user accounts (for signer resolution and analysis) - * @param deps Runtime dependencies injected from the React hook layer + * @param request The sign request to process + * @param allAccounts All user accounts (for signer resolution and analysis) + * @param authAddresses On-chain auth-address lookup captured at request time + * @param deps Runtime dependencies injected from the React hook layer */ export const createSigningMachine = ( request: SignRequest, allAccounts: WalletAccount[], + authAddresses: AuthAddressLookup, deps: SigningMachineDeps, ) => createActor(signingMachine, { id: request.id, - input: { request, allAccounts, ...deps }, + input: { request, allAccounts, authAddresses, ...deps }, }) diff --git a/packages/signing/src/machine/signingMachine.ts b/packages/signing/src/machine/signingMachine.ts index 887e2ba34..0bf9a25b7 100644 --- a/packages/signing/src/machine/signingMachine.ts +++ b/packages/signing/src/machine/signingMachine.ts @@ -304,6 +304,7 @@ export const signingMachine = setup({ 'localKey', ), allAccounts: context.allAccounts, + authAddresses: context.authAddresses, signTransactions: context.deps.signTransactions, }), onDone: { diff --git a/packages/signing/src/pipeline/signing/__tests__/createMultisigStrategy.spec.ts b/packages/signing/src/pipeline/signing/__tests__/createMultisigStrategy.spec.ts index 543053c69..5f471a594 100644 --- a/packages/signing/src/pipeline/signing/__tests__/createMultisigStrategy.spec.ts +++ b/packages/signing/src/pipeline/signing/__tests__/createMultisigStrategy.spec.ts @@ -79,6 +79,7 @@ describe('createMultisigStrategy', () => { getLocalParticipants: () => localParticipants, getStrategyForParticipant: () => mockParticipantStrategy, getAllAccounts: () => allAccounts, + getAuthAddresses: () => new Map(), }), mockParticipantStrategy, } diff --git a/packages/signing/src/pipeline/signing/createMultisigStrategy.ts b/packages/signing/src/pipeline/signing/createMultisigStrategy.ts index 0529bc3c0..d5d110d97 100644 --- a/packages/signing/src/pipeline/signing/createMultisigStrategy.ts +++ b/packages/signing/src/pipeline/signing/createMultisigStrategy.ts @@ -10,7 +10,10 @@ limitations under the License */ -import type { WalletAccount } from '@perawallet/wallet-core-accounts' +import type { + WalletAccount, + AuthAddressLookup, +} from '@perawallet/wallet-core-accounts' import { isMultisigAccount } from '@perawallet/wallet-core-accounts' import type { SigningStrategy, SigningResult, SignerInfo } from '../types' import { NoLocalParticipantsError } from '../errors' @@ -20,16 +23,21 @@ export interface CreateMultisigStrategyOptions { getLocalParticipants: ( account: WalletAccount, allAccounts: WalletAccount[], + authAddresses: AuthAddressLookup, ) => WalletAccount[] /** Get signing strategy for an individual participant */ getStrategyForParticipant: ( participant: WalletAccount, allAccounts: WalletAccount[], + authAddresses: AuthAddressLookup, ) => SigningStrategy /** Get all user accounts */ getAllAccounts: () => WalletAccount[] + + /** Get the current on-chain auth-address lookup */ + getAuthAddresses: () => AuthAddressLookup } /** @@ -39,8 +47,12 @@ export interface CreateMultisigStrategyOptions { export const createMultisigStrategy = ( options: CreateMultisigStrategyOptions, ): SigningStrategy => { - const { getLocalParticipants, getStrategyForParticipant, getAllAccounts } = - options + const { + getLocalParticipants, + getStrategyForParticipant, + getAllAccounts, + getAuthAddresses, + } = options return { canSign: (account: WalletAccount): boolean => { @@ -49,7 +61,12 @@ export const createMultisigStrategy = ( sign: async (group, account, callbacks) => { const allAccounts = getAllAccounts() - const localParticipants = getLocalParticipants(account, allAccounts) + const authAddresses = getAuthAddresses() + const localParticipants = getLocalParticipants( + account, + allAccounts, + authAddresses, + ) if (localParticipants.length === 0) { throw new NoLocalParticipantsError(account.address) @@ -61,6 +78,7 @@ export const createMultisigStrategy = ( const strategy = getStrategyForParticipant( participant, allAccounts, + authAddresses, ) return strategy.sign(group, participant, callbacks) }), diff --git a/packages/signing/src/pipeline/signing/getSigningStrategy.ts b/packages/signing/src/pipeline/signing/getSigningStrategy.ts index 6294e42c0..7b592fc57 100644 --- a/packages/signing/src/pipeline/signing/getSigningStrategy.ts +++ b/packages/signing/src/pipeline/signing/getSigningStrategy.ts @@ -10,7 +10,10 @@ limitations under the License */ -import type { WalletAccount } from '@perawallet/wallet-core-accounts' +import type { + WalletAccount, + AuthAddressLookup, +} from '@perawallet/wallet-core-accounts' import { hasSigningKeys, isHardwareWalletAccount, @@ -41,11 +44,15 @@ export interface GetSigningStrategyOptions { getLocalParticipants: ( account: WalletAccount, allAccounts: WalletAccount[], + authAddresses: AuthAddressLookup, ) => WalletAccount[] /** Get all user accounts */ getAllAccounts: () => WalletAccount[] + /** Get the current on-chain auth-address lookup */ + getAuthAddresses: () => AuthAddressLookup + /** Transaction encoder for hardware wallet signing */ encodeTransaction: EncodeTransactionFunction @@ -62,6 +69,7 @@ export const createSigningStrategySelector = ( ): (( account: WalletAccount, allAccounts: WalletAccount[], + authAddresses: AuthAddressLookup, ) => SigningStrategy) => { const localStrategy = createLocalKeyStrategy(options.signTransactions) const hardwareStrategy = createHardwareStrategy({ @@ -73,14 +81,16 @@ export const createSigningStrategySelector = ( // participant via a lazy callback, avoiding circular init issues. const multisigStrategy = createMultisigStrategy({ getLocalParticipants: options.getLocalParticipants, - getStrategyForParticipant: (participant, allAccounts) => - selectStrategy(participant, allAccounts), + getStrategyForParticipant: (participant, allAccounts, authAddresses) => + selectStrategy(participant, allAccounts, authAddresses), getAllAccounts: options.getAllAccounts, + getAuthAddresses: options.getAuthAddresses, }) const selectStrategy = ( account: WalletAccount, allAccounts: WalletAccount[], + authAddresses: AuthAddressLookup, ): SigningStrategy => { // Multisig accounts are handled by the multisig strategy if (isMultisigAccount(account)) { @@ -88,7 +98,11 @@ export const createSigningStrategySelector = ( } // Follow rekey chain to find actual signer - const actualSigner = resolveAuthAccount(account, allAccounts) + const actualSigner = resolveAuthAccount( + account, + allAccounts, + authAddresses.get(account.address), + ) // Select strategy based on actual signer type if (isHardwareWalletAccount(actualSigner)) { diff --git a/packages/signing/src/pipeline/signing/utils.ts b/packages/signing/src/pipeline/signing/utils.ts index 9b4c72a7a..7ec0d6578 100644 --- a/packages/signing/src/pipeline/signing/utils.ts +++ b/packages/signing/src/pipeline/signing/utils.ts @@ -13,6 +13,7 @@ import type { WalletAccount, MultiSigAccount, + AuthAddressLookup, } from '@perawallet/wallet-core-accounts' import { isMultisigAccount, @@ -31,6 +32,7 @@ import { export const getLocalParticipants = ( account: WalletAccount, allAccounts: WalletAccount[], + authAddresses: AuthAddressLookup, ): WalletAccount[] => { if (!isMultisigAccount(account)) { return [] @@ -50,7 +52,7 @@ export const getLocalParticipants = ( } // Check if this account can sign (has keys or is rekeyed to an account with keys) - return canSignWithAccount(localAccount, allAccounts) + return canSignWithAccount(localAccount, allAccounts, authAddresses) }) } @@ -64,13 +66,18 @@ export const getLocalParticipants = ( export const canMeetThresholdLocally = ( account: WalletAccount, allAccounts: WalletAccount[], + authAddresses: AuthAddressLookup, ): boolean => { if (!isMultisigAccount(account)) { return false } const multisigAccount = account as MultiSigAccount - const localParticipants = getLocalParticipants(account, allAccounts) + const localParticipants = getLocalParticipants( + account, + allAccounts, + authAddresses, + ) return localParticipants.length >= multisigAccount.multisigDetails.threshold } diff --git a/packages/walletconnect/src/hooks/__tests__/useWalletConnectHandlers.test.ts b/packages/walletconnect/src/hooks/__tests__/useWalletConnectHandlers.test.ts index 293a2aaf7..ae3868fb1 100644 --- a/packages/walletconnect/src/hooks/__tests__/useWalletConnectHandlers.test.ts +++ b/packages/walletconnect/src/hooks/__tests__/useWalletConnectHandlers.test.ts @@ -96,6 +96,11 @@ vi.mock('@perawallet/wallet-core-accounts', () => ({ useAllAccounts: vi.fn(() => [ { address: 'addr1', name: 'Account 1', type: 'standard' }, ]), + useAccountAuthAddresses: vi.fn(() => ({ + authAddresses: new Map(), + isPending: false, + isFetched: true, + })), useSigningAccounts: vi.fn(() => [ { address: 'addr1', name: 'Account 1', type: 'standard' }, ]), diff --git a/packages/walletconnect/src/hooks/useWalletConnectHandlers.ts b/packages/walletconnect/src/hooks/useWalletConnectHandlers.ts index 3b6bd3ea5..f52b0824d 100644 --- a/packages/walletconnect/src/hooks/useWalletConnectHandlers.ts +++ b/packages/walletconnect/src/hooks/useWalletConnectHandlers.ts @@ -48,8 +48,10 @@ import { canSignWithAccount, isHardwareWalletAccount, useAllAccounts, + useAccountAuthAddresses, useSigningAccounts, WalletAccount, + type AuthAddressLookup, } from '@perawallet/wallet-core-accounts' const validateRequest = ( @@ -102,6 +104,7 @@ const validateRequest = ( const validateDataSignRequest = ( connector: WalletConnect, accounts: WalletAccount[], + authAddresses: AuthAddressLookup, connections: WalletConnectConnection[], network: Network, data: PeraArbitraryDataMessage[], @@ -141,7 +144,7 @@ const validateDataSignRequest = ( const account = accounts.find( account => account.address === item.signer, ) - if (!account || !canSignWithAccount(account, accounts)) { + if (!account || !canSignWithAccount(account, accounts, authAddresses)) { throw new WalletConnectInvalidSessionError('Invalid signer') } @@ -165,6 +168,7 @@ export const useWalletConnectHandlers = () => { const { encodeSignedTransactions, decodeTransactions } = useTransactionEncoder() const accounts = useAllAccounts() + const { authAddresses } = useAccountAuthAddresses() const signingAccounts = useSigningAccounts() //TODO handle ARC-60 sign requests @@ -182,6 +186,7 @@ export const useWalletConnectHandlers = () => { validateDataSignRequest( connector, accounts, + authAddresses, connections, network, params, @@ -226,7 +231,7 @@ export const useWalletConnectHandlers = () => { }, } as ArbitraryDataSignRequest) }, - [connections, accounts, addSignRequest], + [connections, accounts, authAddresses, addSignRequest], ) const handleSignTransaction = useCallback(