From b903214e606a43bf28f2af908e36b9d8baced208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Vytick=20Vytrhl=C3=ADk?= Date: Mon, 30 Sep 2024 22:28:18 +0200 Subject: [PATCH] feat(suite-native): make token selectors network agnostic --- .../AccountsList/AccountsListTokenItem.tsx | 6 +- .../assets/src/components/AssetItem.tsx | 4 +- .../components/TransactionEventTooltip.tsx | 9 +- .../components/AccountDetailGraphHeader.tsx | 4 +- .../src/components/TransactionListHeader.tsx | 6 +- .../screens/AccountDetailContentScreen.tsx | 4 +- .../src/components/TokenReceiveCard.tsx | 12 +- .../src/screens/ReceiveModalScreen.tsx | 8 +- suite-native/state/package.json | 1 + suite-native/state/src/extraDependencies.ts | 3 +- suite-native/state/tsconfig.json | 1 + suite-native/tokens/package.json | 1 - .../tokens/src/ethereumTokensSelectors.ts | 216 -------------- suite-native/tokens/src/index.ts | 1 - suite-native/tokens/src/tokensSelectors.ts | 265 +++++++++++++++--- suite-native/tokens/src/types.ts | 6 +- suite-native/tokens/src/utils.ts | 4 +- suite-native/tokens/tsconfig.json | 3 - .../TokenTransactionDetailSummary.tsx | 4 +- .../TransactionDetailData.tsx | 4 +- .../TransactionDetailHeader.tsx | 4 +- .../TransactionDetailIncludedCoins.tsx | 8 +- .../TransactionDetailListItem.tsx | 4 +- .../TransactionDetailSummary.tsx | 4 +- .../TokenTransferListItem.tsx | 6 +- .../TransactionsList/TransactionList.tsx | 18 +- .../TransactionListItemContainer.tsx | 4 +- .../src/screens/TransactionDetailScreen.tsx | 6 +- suite-native/transactions/src/selectors.ts | 6 +- yarn.lock | 2 +- 30 files changed, 293 insertions(+), 331 deletions(-) delete mode 100644 suite-native/tokens/src/ethereumTokensSelectors.ts diff --git a/suite-native/accounts/src/components/AccountsList/AccountsListTokenItem.tsx b/suite-native/accounts/src/components/AccountsList/AccountsListTokenItem.tsx index 90927f6867a..a26556a20d7 100644 --- a/suite-native/accounts/src/components/AccountsList/AccountsListTokenItem.tsx +++ b/suite-native/accounts/src/components/AccountsList/AccountsListTokenItem.tsx @@ -7,7 +7,7 @@ import { EthereumTokenAmountFormatter, EthereumTokenToFiatAmountFormatter, } from '@suite-native/formatters'; -import { getEthereumTokenName, selectEthereumAccountTokenSymbol } from '@suite-native/tokens'; +import { getTokenName, selectAccountTokenSymbol } from '@suite-native/tokens'; import { AccountsListItemBase } from './AccountsListItemBase'; @@ -30,7 +30,7 @@ export const AccountsListTokenItem = ({ isLast, }: AccountListTokenItemProps) => { const tokenSymbol = useSelector((state: AccountsRootState) => - selectEthereumAccountTokenSymbol(state, account.key, token.contract), + selectAccountTokenSymbol(state, account.key, token.contract), ); const balance = token.balance ?? '0'; @@ -41,7 +41,7 @@ export const AccountsListTokenItem = ({ isLast={isLast} onPress={onSelectAccount} icon={} - title={getEthereumTokenName(token.name)} + title={getTokenName(token.name)} mainValue={ } diff --git a/suite-native/assets/src/components/AssetItem.tsx b/suite-native/assets/src/components/AssetItem.tsx index 3c41c163fe6..6bf7c91c8ed 100644 --- a/suite-native/assets/src/components/AssetItem.tsx +++ b/suite-native/assets/src/components/AssetItem.tsx @@ -20,11 +20,11 @@ import { RootStackRoutes, TabToStackCompositeNavigationProp, } from '@suite-native/navigation'; -import { selectHasDeviceAnyTokens } from '@suite-native/tokens'; import { selectHasAnyDeviceAccountsWithStaking, NativeStakingRootState, } from '@suite-native/staking'; +import { selectHasDeviceAnyTokensForNetwork } from '@suite-native/tokens'; import { AssetsRootState, @@ -91,7 +91,7 @@ export const AssetItem = React.memo(({ cryptoCurrencySymbol, onPress }: AssetIte const accountsPerAsset = accountsKeysForNetworkSymbol.length; const hasAnyTokens = useSelector( (state: AccountsRootState & DeviceRootState & TokenDefinitionsRootState) => - selectHasDeviceAnyTokens(state, cryptoCurrencySymbol), + selectHasDeviceAnyTokensForNetwork(state, cryptoCurrencySymbol), ); const hasAnyAccountsWithStaking = useSelector((state: NativeStakingRootState) => selectHasAnyDeviceAccountsWithStaking(state, cryptoCurrencySymbol), diff --git a/suite-native/graph/src/components/TransactionEventTooltip.tsx b/suite-native/graph/src/components/TransactionEventTooltip.tsx index 0892ec9046a..e6f63892dea 100644 --- a/suite-native/graph/src/components/TransactionEventTooltip.tsx +++ b/suite-native/graph/src/components/TransactionEventTooltip.tsx @@ -17,10 +17,7 @@ import { SignValue } from '@suite-common/suite-types'; import { NetworkSymbol, getNetworkType } from '@suite-common/wallet-config'; import { AccountKey, TokenAddress } from '@suite-common/wallet-types'; import { AccountsRootState } from '@suite-common/wallet-core'; -import { - selectEthereumAccountTokenInfo, - selectEthereumAccountTokenSymbol, -} from '@suite-native/tokens'; +import { selectAccountTokenInfo, selectAccountTokenSymbol } from '@suite-native/tokens'; export type TransactionEventTooltipProps = EventTooltipComponentProps; @@ -71,10 +68,10 @@ const TokenAmountTooltipFormatter = ({ value: number; }) => { const tokenInfo = useSelector((state: AccountsRootState) => - selectEthereumAccountTokenInfo(state, accountKey, tokenAddress), + selectAccountTokenInfo(state, accountKey, tokenAddress), ); const tokenSymbol = useSelector((state: AccountsRootState) => - selectEthereumAccountTokenSymbol(state, accountKey, tokenAddress), + selectAccountTokenSymbol(state, accountKey, tokenAddress), ); const tokenDecimals = tokenInfo?.decimals; diff --git a/suite-native/module-accounts-management/src/components/AccountDetailGraphHeader.tsx b/suite-native/module-accounts-management/src/components/AccountDetailGraphHeader.tsx index 7d558ec9729..6e7750d172a 100644 --- a/suite-native/module-accounts-management/src/components/AccountDetailGraphHeader.tsx +++ b/suite-native/module-accounts-management/src/components/AccountDetailGraphHeader.tsx @@ -7,7 +7,7 @@ import { NetworkSymbol } from '@suite-common/wallet-config'; import { AccountsRootState, selectAccountByKey } from '@suite-common/wallet-core'; import { AccountKey, TokenAddress, TokenSymbol } from '@suite-common/wallet-types'; import { DiscreetTextTrigger, VStack } from '@suite-native/atoms'; -import { selectEthereumAccountTokenSymbol } from '@suite-native/tokens'; +import { selectAccountTokenSymbol } from '@suite-native/tokens'; import { GraphFiatBalance } from '@suite-native/graph'; import { AccountDetailCryptoValue } from './AccountDetailCryptoValue'; @@ -52,7 +52,7 @@ export const AccountDetailGraphHeader = ({ accountKey, tokenAddress }: AccountBa selectAccountByKey(state, accountKey), ); const tokenSymbol = useSelector((state: AccountsRootState) => - selectEthereumAccountTokenSymbol(state, accountKey, tokenAddress), + selectAccountTokenSymbol(state, accountKey, tokenAddress), ); const setPoint = useSetAtom(selectedPointAtom); diff --git a/suite-native/module-accounts-management/src/components/TransactionListHeader.tsx b/suite-native/module-accounts-management/src/components/TransactionListHeader.tsx index bafbbdadb29..8c97f371f47 100644 --- a/suite-native/module-accounts-management/src/components/TransactionListHeader.tsx +++ b/suite-native/module-accounts-management/src/components/TransactionListHeader.tsx @@ -12,7 +12,6 @@ import { selectAccountByKey, selectIsPortfolioTrackerDevice, } from '@suite-common/wallet-core'; -import { isEthereumAccountSymbol } from '@suite-common/wallet-utils'; import { AppTabsParamList, RootStackParamList, @@ -23,6 +22,7 @@ import { import { Translation } from '@suite-native/intl'; import { FeatureFlag, useFeatureFlag } from '@suite-native/feature-flags'; import { NetworkSymbol } from '@suite-common/wallet-config'; +import { isCoinWithTokens } from '@suite-native/tokens'; import { AccountDetailGraph } from './AccountDetailGraph'; import { AccountDetailCryptoValue } from './AccountDetailCryptoValue'; @@ -132,7 +132,7 @@ export const TransactionListHeader = memo( }; const isTokenDetail = !!tokenContract; - const isEthereumAccountDetail = !isTokenDetail && isEthereumAccountSymbol(account.symbol); + const canHaveTokens = !isTokenDetail && isCoinWithTokens(account.symbol); const isPriceCardDisplayed = !isTestnetAccount && !isTokenDetail; const isSendButtonDisplayed = @@ -181,7 +181,7 @@ export const TransactionListHeader = memo( )} - {isEthereumAccountDetail && accountHasTransactions && ( + {canHaveTokens && accountHasTransactions && ( - selectEthereumAccountTokenInfo(state, accountKey, tokenContract), + selectAccountTokenInfo(state, accountKey, tokenContract), ); useEffect(() => { diff --git a/suite-native/receive/src/components/TokenReceiveCard.tsx b/suite-native/receive/src/components/TokenReceiveCard.tsx index 9d5fb56a593..7ddfff37d23 100644 --- a/suite-native/receive/src/components/TokenReceiveCard.tsx +++ b/suite-native/receive/src/components/TokenReceiveCard.tsx @@ -9,9 +9,9 @@ import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; import { AccountKey, TokenAddress } from '@suite-common/wallet-types'; import { AccountsRootState, selectAccountLabel } from '@suite-common/wallet-core'; import { - getEthereumTokenName, - selectEthereumAccountTokenInfo, - selectEthereumAccountTokenSymbol, + getTokenName, + selectAccountTokenInfo, + selectAccountTokenSymbol, } from '@suite-native/tokens'; type TokenReceiveCardProps = { @@ -38,16 +38,16 @@ export const TokenReceiveCard = ({ contract, accountKey }: TokenReceiveCardProps ); const token = useSelector((state: AccountsRootState) => - selectEthereumAccountTokenInfo(state, accountKey, contract), + selectAccountTokenInfo(state, accountKey, contract), ); const tokenSymbol = useSelector((state: AccountsRootState) => - selectEthereumAccountTokenSymbol(state, accountKey, contract), + selectAccountTokenSymbol(state, accountKey, contract), ); if (!token) return ; - const tokenName = getEthereumTokenName(token.name); + const tokenName = getTokenName(token.name); return ( diff --git a/suite-native/receive/src/screens/ReceiveModalScreen.tsx b/suite-native/receive/src/screens/ReceiveModalScreen.tsx index ccdd17af879..a2ab574dd93 100644 --- a/suite-native/receive/src/screens/ReceiveModalScreen.tsx +++ b/suite-native/receive/src/screens/ReceiveModalScreen.tsx @@ -24,7 +24,7 @@ import { selectAccountNetworkSymbol, selectDeviceAccountKeyForNetworkSymbolAndAccountTypeWithIndex, } from '@suite-common/wallet-core'; -import { selectEthereumAccountTokenSymbol } from '@suite-native/tokens'; +import { selectAccountTokenSymbol } from '@suite-native/tokens'; import { CryptoIcon } from '@suite-common/icons-deprecated'; import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; @@ -70,8 +70,8 @@ const ReceiveModalScreenSubHeader = ({ accountKey, tokenContract }: ScreenSubHea const networkSymbol = useSelector((state: AccountsRootState) => selectAccountNetworkSymbol(state, accountKey), ); - const ethereumTokenSymbol = useSelector((state: AccountsRootState) => - selectEthereumAccountTokenSymbol(state, accountKey, tokenContract), + const tokenSymbol = useSelector((state: AccountsRootState) => + selectAccountTokenSymbol(state, accountKey, tokenContract), ); useEffect(() => { @@ -98,7 +98,7 @@ const ReceiveModalScreenSubHeader = ({ accountKey, tokenContract }: ScreenSubHea {accountLabel && ( {accountLabel} - {ethereumTokenSymbol && ` - ${ethereumTokenSymbol}`} + {tokenSymbol && ` - ${tokenSymbol}`} )} diff --git a/suite-native/state/package.json b/suite-native/state/package.json index 11c3caa2771..8dfe99fb9b9 100644 --- a/suite-native/state/package.json +++ b/suite-native/state/package.json @@ -31,6 +31,7 @@ "@suite-native/module-send": "workspace:*", "@suite-native/settings": "workspace:*", "@suite-native/storage": "workspace:*", + "@suite-native/tokens": "workspace:*", "@trezor/transport-native": "workspace:*", "@trezor/utils": "workspace:*", "expo-device": "6.0.2", diff --git a/suite-native/state/src/extraDependencies.ts b/suite-native/state/src/extraDependencies.ts index 9e3547d52b2..3f912485ab1 100644 --- a/suite-native/state/src/extraDependencies.ts +++ b/suite-native/state/src/extraDependencies.ts @@ -14,6 +14,7 @@ import { import { mergeDeepObject } from '@trezor/utils'; import { NativeUsbTransport } from '@trezor/transport-native'; import { selectEnabledDiscoveryNetworkSymbols } from '@suite-native/discovery'; +import { NETWORK_SYMBOLS_WITH_TOKENS } from '@suite-native/tokens'; const deviceType = Device.isDevice ? 'device' : 'emulator'; @@ -33,7 +34,7 @@ export const extraDependencies: ExtraDependencies = mergeDeepObject(extraDepende // otherwise disableAccountsThunk might erase accounts not supported by current device selectEnabledNetworks: selectEnabledDiscoveryNetworkSymbols, // todo: this is temporary solution to make token definitions work on native in portfolio tracker - selectTokenDefinitionsEnabledNetworks: () => ['eth'], + selectTokenDefinitionsEnabledNetworks: () => NETWORK_SYMBOLS_WITH_TOKENS, selectBitcoinAmountUnit: selectBitcoinUnits, selectAreSatsAmountUnit, selectLocalCurrency: selectFiatCurrencyCode, diff --git a/suite-native/state/tsconfig.json b/suite-native/state/tsconfig.json index 8da15fa87d8..ee52877d676 100644 --- a/suite-native/state/tsconfig.json +++ b/suite-native/state/tsconfig.json @@ -34,6 +34,7 @@ { "path": "../module-send" }, { "path": "../settings" }, { "path": "../storage" }, + { "path": "../tokens" }, { "path": "../../packages/transport-native" }, diff --git a/suite-native/tokens/package.json b/suite-native/tokens/package.json index 27e54180bb0..08c597a37c4 100644 --- a/suite-native/tokens/package.json +++ b/suite-native/tokens/package.json @@ -16,7 +16,6 @@ "@suite-common/wallet-config": "workspace:*", "@suite-common/wallet-core": "workspace:*", "@suite-common/wallet-types": "workspace:*", - "@suite-common/wallet-utils": "workspace:*", "@trezor/blockchain-link": "workspace:*", "proxy-memoize": "2.0.2", "react": "18.2.0" diff --git a/suite-native/tokens/src/ethereumTokensSelectors.ts b/suite-native/tokens/src/ethereumTokensSelectors.ts deleted file mode 100644 index 37529609473..00000000000 --- a/suite-native/tokens/src/ethereumTokensSelectors.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { A, G, pipe } from '@mobily/ts-belt'; -import { memoizeWithArgs } from 'proxy-memoize'; - -import { - AccountsRootState, - DeviceRootState, - selectAccountByKey, - selectAccountTransactions, - selectVisibleDeviceAccounts, - TransactionsRootState, -} from '@suite-common/wallet-core'; -import { - AccountKey, - TokenAddress, - TokenInfoBranded, - TokenSymbol, -} from '@suite-common/wallet-types'; -import { isEthereumAccountSymbol } from '@suite-common/wallet-utils'; -import { TokenInfo, TokenTransfer } from '@trezor/blockchain-link'; -import { - selectFilterKnownTokens, - selectIsSpecificCoinDefinitionKnown, - TokenDefinitionsRootState, -} from '@suite-common/token-definitions'; - -import { EthereumTokenTransfer, WalletAccountTransaction } from './types'; - -export const selectEthereumAccountTokenInfo = memoizeWithArgs( - ( - state: AccountsRootState, - accountKey?: AccountKey, - tokenAddress?: TokenAddress, - ): TokenInfoBranded | null => { - const account = selectAccountByKey(state, accountKey); - if (!account || !account.tokens) return null; - - return ( - (A.find( - account.tokens, - (token: TokenInfo) => token.contract === tokenAddress, - ) as TokenInfoBranded) ?? null - ); - }, - // 100 is a reasonable size for the cache - { size: 100 }, -); - -export const selectEthereumAccountTokenSymbol = ( - state: AccountsRootState, - accountKey?: AccountKey, - tokenAddress?: TokenAddress, -): TokenSymbol | null => { - const tokenInfo = selectEthereumAccountTokenInfo(state, accountKey, tokenAddress); - - if (!tokenInfo) return null; - - // FIXME: This is the only place in the codebase where we change case of token symbol. - // The `toUpperCase()` operation is necessary because we are receiving wrongly formatted token symbol from connect. - // Can be removed at the moment when desktop issue https://github.com/trezor/trezor-suite/issues/8037 is resolved. - return tokenInfo.symbol.toUpperCase() as TokenSymbol; -}; - -export const selectEthereumAccountTokenTransactions = memoizeWithArgs( - ( - state: TransactionsRootState, - accountKey: AccountKey, - tokenAddress: TokenAddress, - ): WalletAccountTransaction[] => - pipe( - selectAccountTransactions(state, accountKey), - A.map(transaction => ({ - ...transaction, - tokens: transaction.tokens.map((tokenTransfer: TokenTransfer) => ({ - ...tokenTransfer, - symbol: tokenTransfer.symbol, - })), - })), - A.filter(transaction => - A.some( - transaction.tokens, - tokenTransfer => tokenTransfer.contract === tokenAddress, - ), - ), - ) as WalletAccountTransaction[], - { size: 500 }, -); - -const selectAllAccountTokens = ( - state: AccountsRootState, - accountKey: AccountKey, -): TokenInfoBranded[] => { - const account = selectAccountByKey(state, accountKey); - if (!account || !account.tokens) return []; - - return account.tokens as TokenInfoBranded[]; -}; - -export const selectAnyOfTokensIsKnown = ( - state: TokenDefinitionsRootState & AccountsRootState, - ethereumAccountKey: AccountKey, -): boolean => { - // It may be temping to reuse selectEthereumAccountsKnownTokens.length but this is faster - const tokens = selectAllAccountTokens(state, ethereumAccountKey); - - const result = A.any(tokens, token => { - const isKnown = selectIsSpecificCoinDefinitionKnown(state, 'eth', token.contract); - - return isKnown; - }); - - return result; -}; - -const isNotZeroAmountTranfer = (tokenTranfer: TokenTransfer) => - tokenTranfer.amount !== '' && tokenTranfer.amount !== '0'; - -/** Is not a transaction with zero amount and no internal transfers. */ -const isNotEmptyTransaction = (transaction: WalletAccountTransaction) => - transaction.amount !== '0' || - (G.isArray(transaction.internalTransfers) && A.isNotEmpty(transaction.internalTransfers)); - -const isTransactionWithTokenTransfers = (transaction: WalletAccountTransaction) => - G.isArray(transaction.tokens) && A.isNotEmpty(transaction.tokens); - -const selectAccountTransactionsWithTokensWithFiatRates = memoizeWithArgs( - ( - state: TransactionsRootState & TokenDefinitionsRootState, - accountKey: AccountKey, - areTokenOnlyTransactionsIncluded: boolean, - ): WalletAccountTransaction[] => - pipe( - selectAccountTransactions(state, accountKey), - A.map(transaction => ({ - ...transaction, - tokens: pipe( - transaction?.tokens ?? [], - A.filter(isNotZeroAmountTranfer), - A.filter(token => - selectIsSpecificCoinDefinitionKnown( - state, - transaction.symbol, - token.contract as TokenAddress, - ), - ), - A.map((tokenTransfer: TokenTransfer) => ({ - ...tokenTransfer, - symbol: tokenTransfer.symbol, - })), - ) as EthereumTokenTransfer[], - })), - A.filter( - transaction => - isNotEmptyTransaction(transaction) || - (areTokenOnlyTransactionsIncluded && - isTransactionWithTokenTransfers(transaction)), - ), - ) as WalletAccountTransaction[], - { size: 500 }, -); - -export const selectAccountOrTokenAccountTransactions = ( - state: TransactionsRootState & TokenDefinitionsRootState, - accountKey: AccountKey, - tokenAddress: TokenAddress | null, - areTokenOnlyTransactionsIncluded: boolean, -): WalletAccountTransaction[] => { - if (tokenAddress) { - return selectEthereumAccountTokenTransactions(state, accountKey, tokenAddress); - } - - return selectAccountTransactionsWithTokensWithFiatRates( - state, - accountKey, - areTokenOnlyTransactionsIncluded, - ) as WalletAccountTransaction[]; -}; - -export const selectEthereumAccountsKnownTokens = memoizeWithArgs( - ( - state: AccountsRootState & TokenDefinitionsRootState, - ethereumAccountKey: AccountKey, - ): TokenInfoBranded[] => { - const account = selectAccountByKey(state, ethereumAccountKey); - if (!account || !isEthereumAccountSymbol(account.symbol)) return []; - - return selectFilterKnownTokens( - state, - account.symbol, - account.tokens ?? [], - ) as TokenInfoBranded[]; - }, - { size: 50 }, -); - -export const selectNumberOfEthereumAccountTokensWithFiatRates = ( - state: TokenDefinitionsRootState & AccountsRootState, - ethereumAccountKey: AccountKey, -): number => { - const tokens = selectEthereumAccountsKnownTokens(state, ethereumAccountKey); - - return tokens.length; -}; - -export const selectHasDeviceAnyEthereumTokens = ( - state: AccountsRootState & DeviceRootState & TokenDefinitionsRootState, -) => { - const ethereumAccounts = selectVisibleDeviceAccounts(state).filter(account => - isEthereumAccountSymbol(account.symbol), - ); - - return A.any(ethereumAccounts, account => { - const result = selectAnyOfTokensIsKnown(state, account.key); - - return result; - }); -}; diff --git a/suite-native/tokens/src/index.ts b/suite-native/tokens/src/index.ts index f906cb3d9d9..51a1c8ec9eb 100644 --- a/suite-native/tokens/src/index.ts +++ b/suite-native/tokens/src/index.ts @@ -1,4 +1,3 @@ export * from './types'; export * from './utils'; -export * from './ethereumTokensSelectors'; export * from './tokensSelectors'; diff --git a/suite-native/tokens/src/tokensSelectors.ts b/suite-native/tokens/src/tokensSelectors.ts index 7405709e47d..0ed6cf34edc 100644 --- a/suite-native/tokens/src/tokensSelectors.ts +++ b/suite-native/tokens/src/tokensSelectors.ts @@ -1,70 +1,249 @@ -import { TokenDefinitionsRootState } from '@suite-common/token-definitions'; -import { NetworkSymbol } from '@suite-common/wallet-config'; -import { AccountsRootState, DeviceRootState, selectAccountByKey } from '@suite-common/wallet-core'; +import { A, G, pipe } from '@mobily/ts-belt'; +import { memoizeWithArgs } from 'proxy-memoize'; import { - selectAnyOfTokensIsKnown, - selectHasDeviceAnyEthereumTokens, - selectNumberOfEthereumAccountTokensWithFiatRates, -} from './ethereumTokensSelectors'; + selectFilterKnownTokens, + selectIsSpecificCoinDefinitionKnown, + TokenDefinitionsRootState, +} from '@suite-common/token-definitions'; +import { + AccountsRootState, + DeviceRootState, + selectAccountByKey, + selectAccountTransactions, + selectVisibleDeviceAccountsByNetworkSymbol, + TransactionsRootState, +} from '@suite-common/wallet-core'; +import { + AccountKey, + TokenAddress, + TokenInfoBranded, + TokenSymbol, +} from '@suite-common/wallet-types'; +import { TokenInfo, TokenTransfer } from '@trezor/blockchain-link'; +import { NetworkSymbol } from '@suite-common/wallet-config'; + import { isCoinWithTokens } from './utils'; +import { TypedTokenTransfer, WalletAccountTransaction } from './types'; export type TokensRootState = AccountsRootState & DeviceRootState & TokenDefinitionsRootState; -export const selectHasDeviceAnyTokens = (state: TokensRootState, coin: NetworkSymbol) => { - if (!isCoinWithTokens(coin)) return false; +export const selectAccountTokenInfo = memoizeWithArgs( + ( + state: AccountsRootState, + accountKey?: AccountKey, + tokenAddress?: TokenAddress, + ): TokenInfoBranded | null => { + const account = selectAccountByKey(state, accountKey); + if (!account || !account.tokens) { + return null; + } + + return ( + (A.find( + account.tokens, + (token: TokenInfo) => token.contract === tokenAddress, + ) as TokenInfoBranded) ?? null + ); + }, + // 100 is a reasonable size for the cache + { size: 100 }, +); + +export const selectAccountTokenSymbol = ( + state: AccountsRootState, + accountKey?: AccountKey, + tokenAddress?: TokenAddress, +): TokenSymbol | null => { + const tokenInfo = selectAccountTokenInfo(state, accountKey, tokenAddress); + + if (!tokenInfo) { + return null; + } + + // FIXME: This is the only place in the codebase where we change case of token symbol. + // The `toUpperCase()` operation is necessary because we are receiving wrongly formatted token symbol from connect. + // Can be removed at the moment when desktop issue https://github.com/trezor/trezor-suite/issues/8037 is resolved. + return tokenInfo.symbol.toUpperCase() as TokenSymbol; +}; + +export const selectAccountTokenTransactions = memoizeWithArgs( + ( + state: TransactionsRootState, + accountKey: AccountKey, + tokenAddress: TokenAddress, + ): WalletAccountTransaction[] => + pipe( + selectAccountTransactions(state, accountKey), + A.map(transaction => ({ + ...transaction, + tokens: transaction.tokens.map((tokenTransfer: TokenTransfer) => ({ + ...tokenTransfer, + symbol: tokenTransfer.symbol, + })), + })), + A.filter(transaction => + A.some( + transaction.tokens, + tokenTransfer => tokenTransfer.contract === tokenAddress, + ), + ), + ) as WalletAccountTransaction[], + { size: 50 }, +); + +const selectAllAccountTokens = ( + state: AccountsRootState, + accountKey: AccountKey, +): TokenInfoBranded[] => { + const account = selectAccountByKey(state, accountKey); + if (!account || !account.tokens) { + return []; + } + + return account.tokens as TokenInfoBranded[]; +}; + +export const selectAnyOfTokensIsKnown = ( + state: TokenDefinitionsRootState & AccountsRootState, + accountKey: AccountKey, +): boolean => { + // It may be temping to reuse selectAccountsKnownTokens.length but this is faster + const tokens = selectAllAccountTokens(state, accountKey); + const account = selectAccountByKey(state, accountKey); + + if (!account?.symbol) { + return false; + } + const result = A.any(tokens, token => { + const isKnown = selectIsSpecificCoinDefinitionKnown(state, account.symbol, token.contract); - switch (coin) { - case 'eth': - const hasAnyTokens = selectHasDeviceAnyEthereumTokens(state); + return isKnown; + }); - return hasAnyTokens; - default: - // Exhaustive check, all coin types in NETWORKS_WITH_TOKENS should be handled above - coin satisfies never; + return result; +}; - return false; +const isNotZeroAmountTransfer = (tokenTransfer: TokenTransfer) => + tokenTransfer.amount !== '' && tokenTransfer.amount !== '0'; + +/** Is not a transaction with zero amount and no internal transfers. */ +const isNotEmptyTransaction = (transaction: WalletAccountTransaction) => + transaction.amount !== '0' || + (G.isArray(transaction.internalTransfers) && A.isNotEmpty(transaction.internalTransfers)); + +const isTransactionWithTokenTransfers = (transaction: WalletAccountTransaction) => + G.isArray(transaction.tokens) && A.isNotEmpty(transaction.tokens); + +const selectAccountTransactionsWithTokensWithFiatRates = memoizeWithArgs( + ( + state: TransactionsRootState & TokenDefinitionsRootState, + accountKey: AccountKey, + areTokenOnlyTransactionsIncluded: boolean, + ): WalletAccountTransaction[] => + pipe( + selectAccountTransactions(state, accountKey), + A.map(transaction => ({ + ...transaction, + tokens: pipe( + transaction?.tokens ?? [], + A.filter(isNotZeroAmountTransfer), + A.filter(token => + selectIsSpecificCoinDefinitionKnown( + state, + transaction.symbol, + token.contract as TokenAddress, + ), + ), + A.map((tokenTransfer: TokenTransfer) => ({ + ...tokenTransfer, + symbol: tokenTransfer.symbol, + })), + ) as TypedTokenTransfer[], + })), + A.filter( + transaction => + isNotEmptyTransaction(transaction) || + (areTokenOnlyTransactionsIncluded && + isTransactionWithTokenTransfers(transaction)), + ), + ) as WalletAccountTransaction[], + { size: 50 }, +); + +export const selectAccountOrTokenTransactions = ( + state: TransactionsRootState & TokenDefinitionsRootState, + accountKey: AccountKey, + tokenAddress: TokenAddress | null, + areTokenOnlyTransactionsIncluded: boolean, +): WalletAccountTransaction[] => { + if (tokenAddress) { + return selectAccountTokenTransactions(state, accountKey, tokenAddress); } + + return selectAccountTransactionsWithTokensWithFiatRates( + state, + accountKey, + areTokenOnlyTransactionsIncluded, + ) as WalletAccountTransaction[]; }; +export const selectAccountsKnownTokens = memoizeWithArgs( + ( + state: AccountsRootState & TokenDefinitionsRootState, + accountKey: AccountKey, + ): TokenInfoBranded[] => { + const account = selectAccountByKey(state, accountKey); + if (!account || !isCoinWithTokens(account.symbol)) { + return []; + } + + return selectFilterKnownTokens( + state, + account.symbol, + account.tokens ?? [], + ) as TokenInfoBranded[]; + }, + { size: 50 }, +); + export const selectNumberOfAccountTokensWithFiatRates = ( - state: TokensRootState, - accountKey: string, -) => { + state: TokenDefinitionsRootState & AccountsRootState, + accountKey: AccountKey, +): number => { const account = selectAccountByKey(state, accountKey); - if (!account || !isCoinWithTokens(account.symbol)) return 0; + if (!account || !isCoinWithTokens(account.symbol)) { + return 0; + } - switch (account.symbol) { - case 'eth': - const tokensNumber = selectNumberOfEthereumAccountTokensWithFiatRates( - state, - accountKey, - ); + const tokens = selectAccountsKnownTokens(state, accountKey); - return tokensNumber; - default: - // Exhaustive check, all coin types in NETWORKS_WITH_TOKENS should be handled above - account.symbol satisfies never; + return tokens.length; +}; - return 0; +export const selectHasDeviceAnyTokensForNetwork = (state: TokensRootState, coin: NetworkSymbol) => { + if (!isCoinWithTokens(coin)) { + return false; } + + const accounts = selectVisibleDeviceAccountsByNetworkSymbol(state, coin); + + return A.any(accounts, account => { + const result = selectAnyOfTokensIsKnown(state, account.key); + + return result; + }); }; export const selectAccountHasAnyKnownToken = (state: TokensRootState, accountKey: string) => { const account = selectAccountByKey(state, accountKey); - if (!account || !isCoinWithTokens(account.symbol)) return false; - - switch (account.symbol) { - case 'eth': - const anyOfTokensIsKnown = selectAnyOfTokensIsKnown(state, accountKey); + if (!account || !isCoinWithTokens(account.symbol)) { + return false; + } - return anyOfTokensIsKnown; - default: - // Exhaustive check, all coin types in NETWORKS_WITH_TOKENS should be handled above - account.symbol satisfies never; + const anyOfTokensIsKnown = selectAnyOfTokensIsKnown(state, accountKey); - return false; - } + return anyOfTokensIsKnown; }; diff --git a/suite-native/tokens/src/types.ts b/suite-native/tokens/src/types.ts index 8af55e5893a..8c8ec9daed1 100644 --- a/suite-native/tokens/src/types.ts +++ b/suite-native/tokens/src/types.ts @@ -1,15 +1,15 @@ -import { TokenTransfer } from '@trezor/blockchain-link'; +import { TokenTransfer as BlockchainLinkTokenTransfer } from '@trezor/blockchain-link'; import { TokenAddress, TokenSymbol, WalletAccountTransaction as CommonWalletAccountTransaction, } from '@suite-common/wallet-types'; -export type EthereumTokenTransfer = Omit & { +export type TypedTokenTransfer = Omit & { symbol: TokenSymbol; contract: TokenAddress; }; export type WalletAccountTransaction = Omit & { - tokens: EthereumTokenTransfer[]; + tokens: TypedTokenTransfer[]; }; diff --git a/suite-native/tokens/src/utils.ts b/suite-native/tokens/src/utils.ts index 50a8bceea54..d2ce592d703 100644 --- a/suite-native/tokens/src/utils.ts +++ b/suite-native/tokens/src/utils.ts @@ -2,13 +2,13 @@ import { G, S } from '@mobily/ts-belt'; import { NetworkSymbol } from '@suite-common/wallet-config'; -export const getEthereumTokenName = (tokenName?: string) => { +export const getTokenName = (tokenName?: string) => { if (G.isNullable(tokenName) || S.isEmpty(tokenName)) return 'Unknown token'; return tokenName; }; -export const NETWORK_SYMBOLS_WITH_TOKENS = ['eth'] satisfies Array; +export const NETWORK_SYMBOLS_WITH_TOKENS = ['eth', 'pol', 'bnb'] satisfies Array; export type NetworkSymbolWithTokens = (typeof NETWORK_SYMBOLS_WITH_TOKENS)[number]; export const isCoinWithTokens = (symbol: NetworkSymbol): symbol is NetworkSymbolWithTokens => { diff --git a/suite-native/tokens/tsconfig.json b/suite-native/tokens/tsconfig.json index 48a4af432a3..3c5c8d737c9 100644 --- a/suite-native/tokens/tsconfig.json +++ b/suite-native/tokens/tsconfig.json @@ -14,9 +14,6 @@ { "path": "../../suite-common/wallet-types" }, - { - "path": "../../suite-common/wallet-utils" - }, { "path": "../../packages/blockchain-link" } diff --git a/suite-native/transactions/src/components/TransactionDetail/TokenTransactionDetailSummary.tsx b/suite-native/transactions/src/components/TransactionDetail/TokenTransactionDetailSummary.tsx index 5c6d9524c62..cbc2e63f164 100644 --- a/suite-native/transactions/src/components/TransactionDetail/TokenTransactionDetailSummary.tsx +++ b/suite-native/transactions/src/components/TransactionDetail/TokenTransactionDetailSummary.tsx @@ -1,5 +1,5 @@ import { AccountKey } from '@suite-common/wallet-types'; -import { EthereumTokenTransfer } from '@suite-native/tokens'; +import { TypedTokenTransfer } from '@suite-native/tokens'; import { VStack } from '@suite-native/atoms'; import { TransactionDetailAddressesSection } from './TransactionDetailAddressesSection'; @@ -12,7 +12,7 @@ export const TokenTransactionDetailSummary = ({ }: { accountKey: AccountKey; txid: string; - tokenTransfer: EthereumTokenTransfer; + tokenTransfer: TypedTokenTransfer; onShowMore: () => void; }) => { // Token transfer has always only one address, so we need to wrap it to an array. diff --git a/suite-native/transactions/src/components/TransactionDetail/TransactionDetailData.tsx b/suite-native/transactions/src/components/TransactionDetail/TransactionDetailData.tsx index 851f2cecab2..8d190a361e0 100644 --- a/suite-native/transactions/src/components/TransactionDetail/TransactionDetailData.tsx +++ b/suite-native/transactions/src/components/TransactionDetail/TransactionDetailData.tsx @@ -12,7 +12,7 @@ import { selectTransactionBlockTimeById, } from '@suite-common/wallet-core'; import { getFiatRateKey } from '@suite-common/wallet-utils'; -import { EthereumTokenTransfer, WalletAccountTransaction } from '@suite-native/tokens'; +import { TypedTokenTransfer, WalletAccountTransaction } from '@suite-native/tokens'; import { Translation, useTranslate } from '@suite-native/intl'; import { Link } from '@suite-native/link'; import { TokenDefinitionsRootState } from '@suite-common/token-definitions'; @@ -27,7 +27,7 @@ import { TransactionDetailSheets } from './TransactionDetailSheets'; type TransactionDetailDataProps = { transaction: WalletAccountTransaction; accountKey: AccountKey; - tokenTransfer?: EthereumTokenTransfer; + tokenTransfer?: TypedTokenTransfer; }; export const TransactionDetailData = ({ diff --git a/suite-native/transactions/src/components/TransactionDetail/TransactionDetailHeader.tsx b/suite-native/transactions/src/components/TransactionDetail/TransactionDetailHeader.tsx index fbc8c41becc..e52f690f854 100644 --- a/suite-native/transactions/src/components/TransactionDetail/TransactionDetailHeader.tsx +++ b/suite-native/transactions/src/components/TransactionDetail/TransactionDetailHeader.tsx @@ -8,7 +8,7 @@ import { EthereumTokenToFiatAmountFormatter, SignValueFormatter, } from '@suite-native/formatters'; -import { EthereumTokenTransfer, WalletAccountTransaction } from '@suite-native/tokens'; +import { TypedTokenTransfer, WalletAccountTransaction } from '@suite-native/tokens'; import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; import { Translation } from '@suite-native/intl'; @@ -18,7 +18,7 @@ import { getTransactionValueSign } from '../../utils'; type TransactionDetailHeaderProps = { transaction: WalletAccountTransaction; - tokenTransfer?: EthereumTokenTransfer; + tokenTransfer?: TypedTokenTransfer; accountKey: AccountKey; }; diff --git a/suite-native/transactions/src/components/TransactionDetail/TransactionDetailIncludedCoins.tsx b/suite-native/transactions/src/components/TransactionDetail/TransactionDetailIncludedCoins.tsx index b179cbf5b00..b21abdac90b 100644 --- a/suite-native/transactions/src/components/TransactionDetail/TransactionDetailIncludedCoins.tsx +++ b/suite-native/transactions/src/components/TransactionDetail/TransactionDetailIncludedCoins.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { TouchableOpacity } from 'react-native'; import { BottomSheet, Box, Card, RoundedIcon, Text } from '@suite-native/atoms'; -import { EthereumTokenTransfer, WalletAccountTransaction } from '@suite-native/tokens'; +import { TypedTokenTransfer, WalletAccountTransaction } from '@suite-native/tokens'; import { AccountKey } from '@suite-common/wallet-types'; import { Icon } from '@suite-common/icons-deprecated'; import { useNativeStyles } from '@trezor/styles'; @@ -13,12 +13,12 @@ import { cardStyle } from './TransactionDetailSummary'; type TransactionDetailIncludedCoinsProps = { accountKey: AccountKey; transaction: WalletAccountTransaction; - tokenTransfer?: EthereumTokenTransfer; + tokenTransfer?: TypedTokenTransfer; }; const isSameTokenTransfer = ( - tokenTransferA: EthereumTokenTransfer, - tokenTransferB: EthereumTokenTransfer, + tokenTransferA: TypedTokenTransfer, + tokenTransferB: TypedTokenTransfer, ) => tokenTransferA.from === tokenTransferB.from && tokenTransferA.to === tokenTransferB.to && diff --git a/suite-native/transactions/src/components/TransactionDetail/TransactionDetailListItem.tsx b/suite-native/transactions/src/components/TransactionDetail/TransactionDetailListItem.tsx index fadfee59263..86cc0a3aad9 100644 --- a/suite-native/transactions/src/components/TransactionDetail/TransactionDetailListItem.tsx +++ b/suite-native/transactions/src/components/TransactionDetail/TransactionDetailListItem.tsx @@ -4,7 +4,7 @@ import { useNavigation } from '@react-navigation/native'; import { Box, RoundedIcon, Text } from '@suite-native/atoms'; import { AccountKey } from '@suite-common/wallet-types'; -import { EthereumTokenTransfer, WalletAccountTransaction } from '@suite-native/tokens'; +import { TypedTokenTransfer, WalletAccountTransaction } from '@suite-native/tokens'; import { StackNavigationProps, RootStackParamList, @@ -22,7 +22,7 @@ import { type TransactionDetailListItemProps = { accountKey: AccountKey; transaction: WalletAccountTransaction; - tokenTransfer?: EthereumTokenTransfer; + tokenTransfer?: TypedTokenTransfer; isFirst?: boolean; isLast?: boolean; onPress: () => void; diff --git a/suite-native/transactions/src/components/TransactionDetail/TransactionDetailSummary.tsx b/suite-native/transactions/src/components/TransactionDetail/TransactionDetailSummary.tsx index 7eee149c046..ff29f4a45fd 100644 --- a/suite-native/transactions/src/components/TransactionDetail/TransactionDetailSummary.tsx +++ b/suite-native/transactions/src/components/TransactionDetail/TransactionDetailSummary.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { Card } from '@suite-native/atoms'; import { AccountKey } from '@suite-common/wallet-types'; -import { EthereumTokenTransfer } from '@suite-native/tokens'; +import { TypedTokenTransfer } from '@suite-native/tokens'; import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; import { TransactionDetailAddressesSheet } from './TransactionDetailAddressesSheet'; @@ -12,7 +12,7 @@ import { TokenTransactionDetailSummary } from './TokenTransactionDetailSummary'; type TransactionDetailSummaryProps = { txid: string; accountKey: AccountKey; - tokenTransfer?: EthereumTokenTransfer; + tokenTransfer?: TypedTokenTransfer; }; export const cardStyle = prepareNativeStyle(utils => ({ diff --git a/suite-native/transactions/src/components/TransactionsList/TokenTransferListItem.tsx b/suite-native/transactions/src/components/TransactionsList/TokenTransferListItem.tsx index e465b0f5c42..d5fcdd8027a 100644 --- a/suite-native/transactions/src/components/TransactionsList/TokenTransferListItem.tsx +++ b/suite-native/transactions/src/components/TransactionsList/TokenTransferListItem.tsx @@ -5,7 +5,7 @@ import { EthereumTokenAmountFormatter, EthereumTokenToFiatAmountFormatter, } from '@suite-native/formatters'; -import { EthereumTokenTransfer, WalletAccountTransaction } from '@suite-native/tokens'; +import { TypedTokenTransfer, WalletAccountTransaction } from '@suite-native/tokens'; import { selectIsPhishingTransaction, TransactionsRootState } from '@suite-common/wallet-core'; import { TokenDefinitionsRootState } from '@suite-common/token-definitions'; @@ -15,7 +15,7 @@ import { getTransactionValueSign } from '../../utils'; type TokenTransferListItemProps = { txid: string; - tokenTransfer: EthereumTokenTransfer; + tokenTransfer: TypedTokenTransfer; transaction: WalletAccountTransaction; accountKey: AccountKey; includedCoinsCount?: number; @@ -28,7 +28,7 @@ export const TokenTransferListItemValues = ({ transaction, accountKey, }: { - tokenTransfer: EthereumTokenTransfer; + tokenTransfer: TypedTokenTransfer; transaction: WalletAccountTransaction; accountKey: AccountKey; }) => { diff --git a/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx b/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx index a48e9e0e325..f1950a70a26 100644 --- a/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx +++ b/suite-native/transactions/src/components/TransactionsList/TransactionList.tsx @@ -15,8 +15,8 @@ import { AccountKey, TokenAddress } from '@suite-common/wallet-types'; import { groupTransactionsByDate, isPending, MonthKey } from '@suite-common/wallet-utils'; import { Box, Loader } from '@suite-native/atoms'; import { - EthereumTokenTransfer, - selectAccountOrTokenAccountTransactions, + TypedTokenTransfer, + selectAccountOrTokenTransactions, WalletAccountTransaction, } from '@suite-native/tokens'; import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; @@ -49,7 +49,7 @@ type RenderTransactionItemParams = { isLast: boolean; }; -type EthereumTokenTransferWithTx = EthereumTokenTransfer & { +type TypedTokenTransferWithTx = TypedTokenTransfer & { originalTransaction: WalletAccountTransaction; }; @@ -57,12 +57,12 @@ type RenderTokenTransferItemParams = Omit< RenderTransactionItemParams, 'areTokensIncluded' | 'item' > & { - item: EthereumTokenTransferWithTx; + item: TypedTokenTransferWithTx; txid: string; }; type TransactionListItem = - | (EthereumTokenTransferWithTx | MonthKey) + | (TypedTokenTransferWithTx | MonthKey) | (WalletAccountTransaction | MonthKey); const sectionListStyle = prepareNativeStyle(utils => ({ @@ -150,7 +150,7 @@ export const TransactionList = ({ ); const transactions = useSelector((state: TransactionsRootState & TokenDefinitionsRootState) => - selectAccountOrTokenAccountTransactions( + selectAccountOrTokenTransactions( state, accountKey, tokenContract ?? null, @@ -232,7 +232,7 @@ export const TransactionList = ({ ({ ...tokenTransfer, originalTransaction: transaction, - }) as EthereumTokenTransferWithTx, + }) as TypedTokenTransferWithTx, ), ), ]); @@ -247,7 +247,7 @@ export const TransactionList = ({ const renderItem = useCallback( ({ item, index }: { item: TransactionListItem; index: number }) => { if (typeof item === 'string') { - return renderSectionHeader({ section: { monthKey: item } }); + return renderSectionHeader({ section: { monthKey: item as MonthKey } }); } const isFirstInSection = typeof data.at(index - 1) === 'string'; const isLastInSection = @@ -255,7 +255,7 @@ export const TransactionList = ({ const getIsTokenTransfer = ( itemForCheck: TransactionListItem, - ): itemForCheck is EthereumTokenTransferWithTx => 'originalTransaction' in itemForCheck; + ): itemForCheck is TypedTokenTransferWithTx => 'originalTransaction' in itemForCheck; return getIsTokenTransfer(item) ? renderTokenTransferItem({ diff --git a/suite-native/transactions/src/components/TransactionsList/TransactionListItemContainer.tsx b/suite-native/transactions/src/components/TransactionsList/TransactionListItemContainer.tsx index 16001cf8b26..34a99c5f38a 100644 --- a/suite-native/transactions/src/components/TransactionsList/TransactionListItemContainer.tsx +++ b/suite-native/transactions/src/components/TransactionsList/TransactionListItemContainer.tsx @@ -21,7 +21,7 @@ import { TransactionsRootState, } from '@suite-common/wallet-core'; import { NetworkSymbol } from '@suite-common/wallet-config'; -import { EthereumTokenTransfer } from '@suite-native/tokens'; +import { TypedTokenTransfer } from '@suite-native/tokens'; import { Color } from '@trezor/theme'; import { Translation } from '@suite-native/intl'; import { TokenDefinitionsRootState } from '@suite-common/token-definitions'; @@ -37,7 +37,7 @@ type TransactionListItemContainerProps = RequireExactlyOne< isFirst?: boolean; isLast?: boolean; networkSymbol: NetworkSymbol; - tokenTransfer: EthereumTokenTransfer; + tokenTransfer: TypedTokenTransfer; transactionType: TransactionType; }, 'networkSymbol' | 'tokenTransfer' diff --git a/suite-native/transactions/src/screens/TransactionDetailScreen.tsx b/suite-native/transactions/src/screens/TransactionDetailScreen.tsx index 8996a735d57..0db7ea50bf8 100644 --- a/suite-native/transactions/src/screens/TransactionDetailScreen.tsx +++ b/suite-native/transactions/src/screens/TransactionDetailScreen.tsx @@ -18,7 +18,7 @@ import { StackProps, } from '@suite-native/navigation'; import { useNativeStyles } from '@trezor/styles'; -import { EthereumTokenTransfer, WalletAccountTransaction } from '@suite-native/tokens'; +import { TypedTokenTransfer, WalletAccountTransaction } from '@suite-native/tokens'; import { TokenAddress, TokenSymbol, TransactionType } from '@suite-common/wallet-types'; import { Translation } from '@suite-native/intl'; @@ -92,13 +92,13 @@ export const TransactionDetailScreen = ({