Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,15 @@ const mockFindContacts = vi.fn(() => [] as unknown[])

vi.mock('@perawallet/wallet-core-accounts', () => ({
useAllAccounts: () => mockUseAllAccounts(),
useAccountLogicalType: () => 'Algo25',
useCanSignWith: () => true,
useRekeyAccount: () => null,
useSignerFor: () => null,
AccountTypes: {
software: 'software',
algo25: 'algo25',
hdWallet: 'hdWallet',
hardware: 'hardware',
},
AccountLogicalTypes: {
Algo25: 'Algo25',
HdKey: 'HdKey',
LedgerBle: 'LedgerBle',
Multisig: 'Multisig',
Rekeyed: 'Rekeyed',
RekeyedAuth: 'RekeyedAuth',
NoAuth: 'NoAuth',
multisig: 'multisig',
watch: 'watch',
},
isMultisigAccount: () => false,
}))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ import { useCallback, useEffect, useMemo } from 'react'
import { ParamListBase, useNavigation } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import {
isSigningLogicalType,
useAccountBalancesQuery,
useAccountLogicalType,
useCanSignWith,
useSortedAssetBalances,
WalletAccount,
AssetWithAccountBalance,
Expand Down Expand Up @@ -152,8 +151,7 @@ export const useAccountAssetList = ({
return sortedBalances.filter(b => matchingAssetIds.has(b.assetId))
}, [sortedBalances, searchFilter, matchingAssetIds])

const logicalType = useAccountLogicalType(account.address)
const isWatch = !logicalType || !isSigningLogicalType(logicalType)
const isWatch = !useCanSignWith(account)

const goToAssetScreen = useCallback(
(item: AssetWithAccountBalance) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import { IconName, PWIcon, PWView } from '@components/core'
import { useMemo } from 'react'
import {
AccountTypes,
useAccountLogicalType,
type AccountLogicalType,
useCanSignWith,
useRekeyAccount,
type AccountType,
type WalletAccount,
} from '@perawallet/wallet-core-accounts'
import { useIsDarkMode } from '@hooks/useIsDarkMode'
Expand All @@ -29,51 +30,51 @@ const FALLBACK_ASSET = `accounts/${THEME_TOKEN}/unknown-account`
export type AccountIconProps = {
account?: WalletAccount
size?: AccountIconSize
logicalTypeOverride?: AccountLogicalType
/**
* Override the resolved logical type. Useful when projecting a different
* state than what's currently stored — e.g. showing a post-undo-rekey
* preview where the source should appear as its base type.
* When true, render the icon for the account's base `type` and ignore
* its rekey state. Used by the undo-rekey preview to show what the
* source would look like once the rekey is undone.
*/
ignoreRekey?: boolean
} & SvgProps

const iconNames = {
Algo25: `accounts/${THEME_TOKEN}/algo25-account`,
HdKey: `accounts/${THEME_TOKEN}/hdwallet-account`,
LedgerBle: `accounts/${THEME_TOKEN}/ledger-account`,
Multisig: `accounts/${THEME_TOKEN}/multisig-account`,
Rekeyed: `accounts/${THEME_TOKEN}/noauth-account`,
RekeyedAuth: `accounts/${THEME_TOKEN}/rekeyed-standard`,
NoAuth: `accounts/${THEME_TOKEN}/watch-account`,
} satisfies Record<AccountLogicalType, string>
const BASE_ICON: Record<AccountType, string> = {
[AccountTypes.algo25]: `accounts/${THEME_TOKEN}/algo25-account`,
[AccountTypes.hdWallet]: `accounts/${THEME_TOKEN}/hdwallet-account`,
[AccountTypes.hardware]: `accounts/${THEME_TOKEN}/ledger-account`,
[AccountTypes.multisig]: `accounts/${THEME_TOKEN}/multisig-account`,
[AccountTypes.watch]: `accounts/${THEME_TOKEN}/watch-account`,
}

const resolveIconAsset = (
logicalType: AccountLogicalType,
account: WalletAccount,
): string => {
if (logicalType === 'RekeyedAuth') {
if (account.type === AccountTypes.hardware) {
return `accounts/${THEME_TOKEN}/rekeyed-ledger`
}
if (account.type === AccountTypes.multisig) {
return `accounts/${THEME_TOKEN}/rekeyed-multisig`
}
}
return iconNames[logicalType] ?? FALLBACK_ASSET
const REKEYED_SIGNABLE_ICON: Partial<Record<AccountType, string>> = {
[AccountTypes.hardware]: `accounts/${THEME_TOKEN}/rekeyed-ledger`,
[AccountTypes.multisig]: `accounts/${THEME_TOKEN}/rekeyed-multisig`,
}
const REKEYED_SIGNABLE_DEFAULT = `accounts/${THEME_TOKEN}/rekeyed-standard`
const REKEYED_UNSIGNABLE_ICON = `accounts/${THEME_TOKEN}/noauth-account`

export const AccountIcon = (props: AccountIconProps) => {
const { account, size = 'md', logicalTypeOverride, ...rest } = props
const { account, size = 'md', ignoreRekey, ...rest } = props
const darkmode = useIsDarkMode()
const resolvedType = useAccountLogicalType(account?.address)
const logicalType = logicalTypeOverride ?? resolvedType
const rekeyAccount = useRekeyAccount(account?.address)
const canSign = useCanSignWith(account)
const styles = useStyles({ size })

const icon = useMemo(() => {
if (!account || !logicalType) return null
if (!account) return null

const isRekeyed = !ignoreRekey && !!account.rekeyAddress
let asset: string
if (isRekeyed && canSign) {
asset =
REKEYED_SIGNABLE_ICON[account.type] ?? REKEYED_SIGNABLE_DEFAULT
} else if (isRekeyed && !canSign) {
asset = REKEYED_UNSIGNABLE_ICON
} else {
asset = BASE_ICON[account.type] ?? FALLBACK_ASSET
}

const theme = darkmode ? 'dark' : 'light'
const asset = resolveIconAsset(logicalType, account)
const iconName: IconName = asset.replaceAll(
THEME_TOKEN,
theme,
Expand All @@ -85,7 +86,10 @@ export const AccountIcon = (props: AccountIconProps) => {
size={size}
/>
)
}, [account, logicalType, darkmode, rest, size])
// rekeyAccount keeps the memo invalidating when the auth account
// changes (which can flip canSign).
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [account, ignoreRekey, canSign, rekeyAccount, darkmode, size, rest])

if (!icon) return <></>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useAccountTypeInfo } from '../useAccountTypeInfo'
import type {
AccountLogicalType,
RekeyTransition,
WalletAccount,
} from '@perawallet/wallet-core-accounts'
Expand Down Expand Up @@ -47,7 +46,7 @@ vi.mock('@perawallet/wallet-core-config', () => ({
},
}))

const mockUseAccountLogicalType = vi.fn<() => AccountLogicalType | null>()
const mockUseCanSignWith = vi.fn<() => boolean>()
const mockUseRekeyTransition = vi.fn<() => RekeyTransition | null>()
vi.mock('@perawallet/wallet-core-accounts', async importOriginal => {
const actual =
Expand All @@ -56,28 +55,32 @@ vi.mock('@perawallet/wallet-core-accounts', async importOriginal => {
>()
return {
...actual,
useAccountLogicalType: () => mockUseAccountLogicalType(),
useCanSignWith: () => mockUseCanSignWith(),
useRekeyTransition: () => mockUseRekeyTransition(),
}
})

const algo25Account: WalletAccount = {
type: 'algo25',
address: 'ALGO25ADDR',
keyPairId: 'key-1',
}
const accountOfType = (
type: WalletAccount['type'],
rekeyAddress?: string,
): WalletAccount =>
({
type,
address: `${type.toUpperCase()}_ADDR`,
keyPairId: 'key-1',
rekeyAddress,
}) as WalletAccount

describe('useAccountTypeInfo', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseAccountLogicalType.mockReturnValue('Algo25')
mockUseCanSignWith.mockReturnValue(true)
mockUseRekeyTransition.mockReturnValue(null)
})

it('resolves Algo25 account type', () => {
mockUseAccountLogicalType.mockReturnValue('Algo25')
it('resolves algo25 account type', () => {
const { result } = renderHook(() =>
useAccountTypeInfo({ account: algo25Account }),
useAccountTypeInfo({ account: accountOfType('algo25') }),
)

expect(result.current.title).toBe('account_type_info.standard_title')
Expand All @@ -86,10 +89,9 @@ describe('useAccountTypeInfo', () => {
)
})

it('resolves LedgerBle account type', () => {
mockUseAccountLogicalType.mockReturnValue('LedgerBle')
it('resolves hardware account type', () => {
const { result } = renderHook(() =>
useAccountTypeInfo({ account: algo25Account }),
useAccountTypeInfo({ account: accountOfType('hardware') }),
)

expect(result.current.title).toBe('account_type_info.ledger_title')
Expand All @@ -98,10 +100,9 @@ describe('useAccountTypeInfo', () => {
)
})

it('resolves HdKey account type', () => {
mockUseAccountLogicalType.mockReturnValue('HdKey')
it('resolves hdWallet account type', () => {
const { result } = renderHook(() =>
useAccountTypeInfo({ account: algo25Account }),
useAccountTypeInfo({ account: accountOfType('hdWallet') }),
)

expect(result.current.title).toBe('account_type_info.hd_wallet_title')
Expand All @@ -110,10 +111,9 @@ describe('useAccountTypeInfo', () => {
)
})

it('resolves Multisig account type', () => {
mockUseAccountLogicalType.mockReturnValue('Multisig')
it('resolves multisig account type', () => {
const { result } = renderHook(() =>
useAccountTypeInfo({ account: algo25Account }),
useAccountTypeInfo({ account: accountOfType('multisig') }),
)

expect(result.current.title).toBe('account_type_info.multisig_title')
Expand All @@ -122,11 +122,11 @@ describe('useAccountTypeInfo', () => {
)
})

it('resolves RekeyedAuth account type without a known auth account as generic rekeyed', () => {
mockUseAccountLogicalType.mockReturnValue('RekeyedAuth')
it('resolves signable rekeyed account without a known auth as generic rekeyed', () => {
mockUseCanSignWith.mockReturnValue(true)
mockUseRekeyTransition.mockReturnValue(null)
const { result } = renderHook(() =>
useAccountTypeInfo({ account: algo25Account }),
useAccountTypeInfo({ account: accountOfType('algo25', 'AUTH') }),
)

expect(result.current.title).toBe(
Expand All @@ -138,13 +138,13 @@ describe('useAccountTypeInfo', () => {
})

it('resolves a standard-to-ledger rekey with the split transition title', () => {
mockUseAccountLogicalType.mockReturnValue('RekeyedAuth')
mockUseCanSignWith.mockReturnValue(true)
mockUseRekeyTransition.mockReturnValue({
from: 'Algo25',
to: 'LedgerBle',
from: 'algo25',
to: 'hardware',
})
const { result } = renderHook(() =>
useAccountTypeInfo({ account: algo25Account }),
useAccountTypeInfo({ account: accountOfType('algo25', 'AUTH') }),
)

expect(result.current.title).toBe('Rekeyed')
Expand All @@ -155,33 +155,33 @@ describe('useAccountTypeInfo', () => {
})

it('resolves a ledger-to-ledger rekey with the ledger-to-ledger description', () => {
mockUseAccountLogicalType.mockReturnValue('RekeyedAuth')
mockUseCanSignWith.mockReturnValue(true)
mockUseRekeyTransition.mockReturnValue({
from: 'LedgerBle',
to: 'LedgerBle',
from: 'hardware',
to: 'hardware',
})
const { result } = renderHook(() =>
useAccountTypeInfo({ account: algo25Account }),
useAccountTypeInfo({ account: accountOfType('hardware', 'AUTH') }),
)

expect(result.current.description).toBe(
'account_type_info.rekeyed_ledger_to_ledger_description',
)
})

it('resolves Rekeyed account type as No Auth (locked-out)', () => {
mockUseAccountLogicalType.mockReturnValue('Rekeyed')
it('resolves an unsignable rekeyed account as No Auth', () => {
mockUseCanSignWith.mockReturnValue(false)
mockUseRekeyTransition.mockReturnValue(null)
const { result } = renderHook(() =>
useAccountTypeInfo({ account: algo25Account }),
useAccountTypeInfo({ account: accountOfType('algo25', 'AUTH') }),
)

expect(result.current.title).toBe('account_type_info.no_auth_title')
})

it('resolves NoAuth account type as Watch', () => {
mockUseAccountLogicalType.mockReturnValue('NoAuth')
it('resolves watch account type', () => {
const { result } = renderHook(() =>
useAccountTypeInfo({ account: algo25Account }),
useAccountTypeInfo({ account: accountOfType('watch') }),
)

expect(result.current.title).toBe('account_type_info.watch_title')
Expand All @@ -191,9 +191,8 @@ describe('useAccountTypeInfo', () => {
})

it('opens webview with support URL when learn more is pressed', () => {
mockUseAccountLogicalType.mockReturnValue('Algo25')
const { result } = renderHook(() =>
useAccountTypeInfo({ account: algo25Account }),
useAccountTypeInfo({ account: accountOfType('algo25') }),
)

act(() => {
Expand All @@ -206,9 +205,8 @@ describe('useAccountTypeInfo', () => {
})

it('opens webview with rekey article when learn more is pressed for Ledger', () => {
mockUseAccountLogicalType.mockReturnValue('LedgerBle')
const { result } = renderHook(() =>
useAccountTypeInfo({ account: algo25Account }),
useAccountTypeInfo({ account: accountOfType('hardware') }),
)

act(() => {
Expand Down
Loading
Loading