From 06f4bfa5f591bbedf5417521e26e3959b4a3c0c1 Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Thu, 23 Jan 2025 13:02:36 -0800 Subject: [PATCH 01/18] chore: Create consolidateTokenBalances utility function --- .../app/assets/token-list/token-list.tsx | 67 +++------------- .../assets/util/calculateTokenFiatAmount.ts | 2 +- .../assets/util/consolidateTokenBalances.ts | 76 +++++++++++++++++++ ui/pages/asset/util.ts | 64 ++++++++++++++++ 4 files changed, 151 insertions(+), 58 deletions(-) create mode 100644 ui/components/app/assets/util/consolidateTokenBalances.ts diff --git a/ui/components/app/assets/token-list/token-list.tsx b/ui/components/app/assets/token-list/token-list.tsx index 5555c8782f8a..1504e89242a2 100644 --- a/ui/components/app/assets/token-list/token-list.tsx +++ b/ui/components/app/assets/token-list/token-list.tsx @@ -23,13 +23,12 @@ import { } from '../../../../selectors'; import { getConversionRate } from '../../../../ducks/metamask/metamask'; import { filterAssets } from '../util/filter'; -import { calculateTokenBalance } from '../util/calculateTokenBalance'; -import { calculateTokenFiatAmount } from '../util/calculateTokenFiatAmount'; import { endTrace, TraceName } from '../../../../../shared/lib/trace'; import { useTokenBalances } from '../../../../hooks/useTokenBalances'; import { setTokenNetworkFilter } from '../../../../store/actions'; import { useMultichainSelector } from '../../../../hooks/useMultichainSelector'; import { getMultichainShouldShowFiat } from '../../../../selectors/multichain'; +import { consolidateTokenBalances } from '../util/consolidateTokenBalances'; type TokenListProps = { onTokenClick: (chainId: string, address: string) => void; @@ -130,62 +129,16 @@ export default function TokenList({ } }, [Object.keys(allNetworks).length]); - const consolidatedBalances = () => { - const tokensWithBalance: TokenWithFiatAmount[] = []; - Object.entries(selectedAccountTokensChains).forEach( - ([stringChainKey, tokens]) => { - const chainId = stringChainKey as Hex; - tokens.forEach((token: Token) => { - const { isNative, address, decimals } = token; - const balance = - calculateTokenBalance({ - isNative, - chainId, - address, - decimals, - nativeBalances, - selectedAccountTokenBalancesAcrossChains, - }) || '0'; - - const tokenFiatAmount = calculateTokenFiatAmount({ - token, - chainId, - balance, - marketData, - currencyRates, - }); - - // Respect the "hide zero balance" setting (when true): - // - Native tokens should always display with zero balance when on the current network filter. - // - Native tokens should not display with zero balance when on all networks filter - // - ERC20 tokens with zero balances should respect the setting on both the current and all networks. - - // Respect the "hide zero balance" setting (when false): - // - Native tokens should always display with zero balance when on the current network filter. - // - Native tokens should always display with zero balance when on all networks filter - // - ERC20 tokens always display with zero balance on both the current and all networks filter. - if ( - !hideZeroBalanceTokens || - balance !== '0' || - (token.isNative && isOnCurrentNetwork) - ) { - tokensWithBalance.push({ - ...token, - balance, - tokenFiatAmount, - chainId, - string: String(balance), - }); - } - }); - }, - ); - - return tokensWithBalance; - }; - const sortedFilteredTokens = useMemo(() => { - const consolidatedTokensWithBalances = consolidatedBalances(); + const consolidatedTokensWithBalances = consolidateTokenBalances( + selectedAccountTokensChains, + nativeBalances, + selectedAccountTokenBalancesAcrossChains, + marketData, + currencyRates, + hideZeroBalanceTokens, + isOnCurrentNetwork, + ); const filteredAssets = filterAssets(consolidatedTokensWithBalances, [ { key: 'chainId', diff --git a/ui/components/app/assets/util/calculateTokenFiatAmount.ts b/ui/components/app/assets/util/calculateTokenFiatAmount.ts index 279fae37f582..5155c7b1ca94 100644 --- a/ui/components/app/assets/util/calculateTokenFiatAmount.ts +++ b/ui/components/app/assets/util/calculateTokenFiatAmount.ts @@ -1,7 +1,7 @@ import { Hex } from '@metamask/utils'; import { ChainAddressMarketData, Token } from '../token-list/token-list'; -type SymbolCurrencyRateMapping = Record>; +export type SymbolCurrencyRateMapping = Record>; type CalculateTokenFiatAmountParams = { token: Token; diff --git a/ui/components/app/assets/util/consolidateTokenBalances.ts b/ui/components/app/assets/util/consolidateTokenBalances.ts new file mode 100644 index 000000000000..6b46235c8cad --- /dev/null +++ b/ui/components/app/assets/util/consolidateTokenBalances.ts @@ -0,0 +1,76 @@ +import { Hex } from '@metamask/utils'; +import { + ChainAddressMarketData, + Token, + TokenWithFiatAmount, +} from '../token-list/token-list'; +import { calculateTokenBalance } from './calculateTokenBalance'; +import { + SymbolCurrencyRateMapping, + calculateTokenFiatAmount, +} from './calculateTokenFiatAmount'; + +export const consolidateTokenBalances = ( + selectedAccountTokensChains: Record, + nativeBalances: Record, + selectedAccountTokenBalancesAcrossChains: Record< + `0x${string}`, + Record<`0x${string}`, `0x${string}`> + >, + marketData: ChainAddressMarketData, + currencyRates: SymbolCurrencyRateMapping, + hideZeroBalanceTokens: boolean, + isOnCurrentNetwork: boolean, +) => { + const tokensWithBalance: TokenWithFiatAmount[] = []; + Object.entries(selectedAccountTokensChains).forEach( + ([stringChainKey, tokens]) => { + const chainId = stringChainKey as Hex; + tokens.forEach((token: Token) => { + const { isNative, address, decimals } = token; + const balance = + calculateTokenBalance({ + isNative, + chainId, + address, + decimals, + nativeBalances, + selectedAccountTokenBalancesAcrossChains, + }) || '0'; + + const tokenFiatAmount = calculateTokenFiatAmount({ + token, + chainId, + balance, + marketData, + currencyRates, + }); + + // Respect the "hide zero balance" setting (when true): + // - Native tokens should always display with zero balance when on the current network filter. + // - Native tokens should not display with zero balance when on all networks filter + // - ERC20 tokens with zero balances should respect the setting on both the current and all networks. + + // Respect the "hide zero balance" setting (when false): + // - Native tokens should always display with zero balance when on the current network filter. + // - Native tokens should always display with zero balance when on all networks filter + // - ERC20 tokens always display with zero balance on both the current and all networks filter. + if ( + !hideZeroBalanceTokens || + balance !== '0' || + (token.isNative && isOnCurrentNetwork) + ) { + tokensWithBalance.push({ + ...token, + balance, + tokenFiatAmount, + chainId, + string: String(balance), + }); + } + }); + }, + ); + + return tokensWithBalance; +}; diff --git a/ui/pages/asset/util.ts b/ui/pages/asset/util.ts index 2f4a41df6cc9..0d413fc6be8d 100644 --- a/ui/pages/asset/util.ts +++ b/ui/pages/asset/util.ts @@ -92,3 +92,67 @@ export const findAssetByAddress = ( token.address && token.address.toLowerCase() === address.toLowerCase(), ); }; + +type ParsedAssetId = { + namespace: string; // Namespace (e.g., eip155, solana, bip122) + chainId: string; // Full chain ID (namespace + blockchain ID) + assetNamespace: string; // Asset namespace (e.g., slip44, erc20, token, ordinal) + assetReference: string; // Asset reference (on-chain address, token identifier, etc.) +}; + +const parseAssetId = (assetId: string): ParsedAssetId => { + // Split the assetId into chain_id and asset details + const [chainId, assetDetails] = assetId.split('/'); + + if (!chainId || !assetDetails) { + throw new Error( + 'Invalid assetId format. Must include both chainId and asset details.', + ); + } + + // Split asset details into namespace and reference + const [assetNamespace, assetReference] = assetDetails.split(':'); + + if (!assetNamespace || !assetReference) { + throw new Error( + 'Invalid asset details format. Must include both assetNamespace and assetReference.', + ); + } + + // Validate the chainId format (namespace:blockchainId) + const [namespace, blockchainId] = chainId.split(':'); + if (!namespace || !blockchainId) { + throw new Error( + 'Invalid chainId format. Must include both namespace and blockchain ID.', + ); + } + + // Validate assetNamespace (must match [-a-z0-9]{3,8}) + // https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md#syntax + const assetNamespaceRegex = /^[-a-z0-9]{3,8}$/u; + if (!assetNamespaceRegex.test(assetNamespace)) { + throw new Error( + `Invalid assetNamespace format: "${assetNamespace}". Must be 3-8 characters, containing only lowercase letters, numbers, or dashes.`, + ); + } + + // Validate assetReference (must match [-.%a-zA-Z0-9]{1,128}) + // https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md#syntax + const assetReferenceRegex = /^[-.%a-zA-Z0-9]{1,128}$/u; + if (!assetReferenceRegex.test(assetReference)) { + throw new Error( + `Invalid assetReference format: "${assetReference}". Must be 1-128 characters, containing only alphanumerics, dashes, dots, or percent signs.`, + ); + } + + // Ensure assetReference is URL-decoded if necessary + // https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md#syntax + const decodedAssetReference = decodeURIComponent(assetReference); + + return { + namespace, + chainId, + assetNamespace, + assetReference: decodedAssetReference, + }; +}; From 2eecfcf2d1b703b309348e379eb50c5dd98a1353 Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Thu, 23 Jan 2025 13:49:59 -0800 Subject: [PATCH 02/18] fix: Continue to break out utility files --- .../app/assets/asset-list/asset-list.tsx | 94 ++++--------------- .../app/assets/auto-detect-token/index.scss | 10 -- .../assets/util/importAllDetectedTokens.ts | 90 ++++++++++++++++++ 3 files changed, 110 insertions(+), 84 deletions(-) delete mode 100644 ui/components/app/assets/auto-detect-token/index.scss create mode 100644 ui/components/app/assets/util/importAllDetectedTokens.ts diff --git a/ui/components/app/assets/asset-list/asset-list.tsx b/ui/components/app/assets/asset-list/asset-list.tsx index 5ac7a84e0b29..3fd5bbbd4369 100644 --- a/ui/components/app/assets/asset-list/asset-list.tsx +++ b/ui/components/app/assets/asset-list/asset-list.tsx @@ -1,6 +1,6 @@ import React, { useContext, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { Token } from '@metamask/assets-controllers'; + import { NetworkConfiguration } from '@metamask/network-controller'; import TokenList from '../token-list'; import { PRIMARY } from '../../../../helpers/constants/common'; @@ -26,7 +26,6 @@ import { MetaMetricsContext } from '../../../../contexts/metametrics'; import { MetaMetricsEventCategory, MetaMetricsEventName, - MetaMetricsTokenEventSource, } from '../../../../../shared/constants/metametrics'; import DetectedToken from '../../detected-token/detected-token'; import { ReceiveModal } from '../../../multichain'; @@ -45,10 +44,8 @@ import { getSelectedNetworkClientId, } from '../../../../../shared/modules/selectors/networks'; import { addImportedTokens } from '../../../../store/actions'; -import { - AssetType, - TokenStandard, -} from '../../../../../shared/constants/transaction'; +import { Token } from '../token-list/token-list'; +import { importAllDetectedTokens } from '../util/importAllDetectedTokens'; import AssetListControlBar from './asset-list-control-bar'; import NativeToken from './native-token'; @@ -90,10 +87,6 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork) || []; - const isTokenNetworkFilterEqualCurrentNetwork = useSelector( - getIsTokenNetworkFilterEqualCurrentNetwork, - ); - const allNetworks: Record<`0x${string}`, NetworkConfiguration> = useSelector( getNetworkConfigurationsByChainId, ); @@ -101,6 +94,9 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { const selectedAddress = useSelector(getSelectedAddress); const useTokenDetection = useSelector(getUseTokenDetection); const currentChainId = useSelector(getCurrentChainId); + const isOnCurrentNetwork = useSelector( + getIsTokenNetworkFilterEqualCurrentNetwork, + ); const [showFundingMethodModal, setShowFundingMethodModal] = useState(false); const [showReceiveModal, setShowReceiveModal] = useState(false); @@ -125,7 +121,7 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { const shouldShowTokensLinks = showTokensLinks ?? isEvm; const detectedTokensMultichain: { - [key: `0x${string}`]: Token[]; + [key: string]: Token[]; } = useSelector(getAllDetectedTokensForSelectedAddress); const multichainDetectedTokensLength = Object.values( @@ -134,70 +130,20 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { // Add detected tokens to sate useEffect(() => { - const importAllDetectedTokens = async () => { - // If autodetect tokens toggle is OFF, return - if (!useTokenDetection) { - return; - } - // TODO add event for MetaMetricsEventName.TokenAdded - - if ( - process.env.PORTFOLIO_VIEW && - !isTokenNetworkFilterEqualCurrentNetwork - ) { - const importPromises = Object.entries(detectedTokensMultichain).map( - async ([networkId, tokens]) => { - const chainConfig = allNetworks[networkId as `0x${string}`]; - const { defaultRpcEndpointIndex } = chainConfig; - const { networkClientId: networkInstanceId } = - chainConfig.rpcEndpoints[defaultRpcEndpointIndex]; - - await dispatch( - addImportedTokens(tokens as Token[], networkInstanceId), - ); - tokens.forEach((importedToken) => { - trackEvent({ - event: MetaMetricsEventName.TokenAdded, - category: MetaMetricsEventCategory.Wallet, - sensitiveProperties: { - token_symbol: importedToken.symbol, - token_contract_address: importedToken.address, - token_decimal_precision: importedToken.decimals, - source: MetaMetricsTokenEventSource.Detected, - token_standard: TokenStandard.ERC20, - asset_type: AssetType.token, - token_added_type: 'detected', - chain_id: chainConfig.chainId, - }, - }); - }); - }, - ); - - await Promise.all(importPromises); - } else if (detectedTokens.length > 0) { - await dispatch(addImportedTokens(detectedTokens, networkClientId)); - detectedTokens.forEach((importedToken: Token) => { - trackEvent({ - event: MetaMetricsEventName.TokenAdded, - category: MetaMetricsEventCategory.Wallet, - sensitiveProperties: { - token_symbol: importedToken.symbol, - token_contract_address: importedToken.address, - token_decimal_precision: importedToken.decimals, - source: MetaMetricsTokenEventSource.Detected, - token_standard: TokenStandard.ERC20, - asset_type: AssetType.token, - token_added_type: 'detected', - chain_id: currentChainId, - }, - }); - }); - } - }; - importAllDetectedTokens(); + importAllDetectedTokens( + useTokenDetection, + isOnCurrentNetwork, + detectedTokensMultichain, + allNetworks, + networkClientId, + addImportedTokens, + currentChainId, + trackEvent, + detectedTokens, + dispatch, + ); }, [ - isTokenNetworkFilterEqualCurrentNetwork, + isOnCurrentNetwork, selectedAddress, networkClientId, detectedTokens.length, diff --git a/ui/components/app/assets/auto-detect-token/index.scss b/ui/components/app/assets/auto-detect-token/index.scss deleted file mode 100644 index 5714d957f0f9..000000000000 --- a/ui/components/app/assets/auto-detect-token/index.scss +++ /dev/null @@ -1,10 +0,0 @@ -.auto-detect-in-modal { - &__benefit { - flex: 1; - } - - &__dialog { - background-position: -80px 16px; - background-repeat: no-repeat; - } -} diff --git a/ui/components/app/assets/util/importAllDetectedTokens.ts b/ui/components/app/assets/util/importAllDetectedTokens.ts new file mode 100644 index 000000000000..2ee69587b34c --- /dev/null +++ b/ui/components/app/assets/util/importAllDetectedTokens.ts @@ -0,0 +1,90 @@ +import { Hex } from '@metamask/utils'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, + MetaMetricsTokenEventSource, +} from '../../../../../shared/constants/metametrics'; +import { + AssetType, + TokenStandard, +} from '../../../../../shared/constants/transaction'; +import { Token } from '../token-list/token-list'; +import { + NetworkClientId, + NetworkConfiguration, +} from '@metamask/network-controller'; + +export const importAllDetectedTokens = async ( + useTokenDetection: boolean, + isOnCurrentNetwork: boolean, + detectedTokensMultichain: { + [key: string]: Token[]; + }, + allNetworks: Record, + networkClientId: NetworkClientId, + addImportedTokens: ( + tokensToImport: Token[], + networkClientId?: NetworkClientId, + ) => void, + currentChainId: string, + trackEvent: any, + detectedTokens: any, + dispatch: any, +) => { + // If autodetect tokens toggle is OFF, return + if (!useTokenDetection) { + return; + } + // TODO add event for MetaMetricsEventName.TokenAdded + + if (process.env.PORTFOLIO_VIEW && !isOnCurrentNetwork) { + const importPromises = Object.entries(detectedTokensMultichain).map( + async ([networkId, tokens]) => { + const chainConfig = allNetworks[networkId]; + const { defaultRpcEndpointIndex } = chainConfig; + const { networkClientId: networkInstanceId } = + chainConfig.rpcEndpoints[defaultRpcEndpointIndex]; + + await dispatch(addImportedTokens(tokens as Token[], networkInstanceId)); + tokens.forEach((importedToken) => { + // when multichain is fully integrated, we should change these event signatures for analytics + trackEvent({ + event: MetaMetricsEventName.TokenAdded, + category: MetaMetricsEventCategory.Wallet, + sensitiveProperties: { + token_symbol: importedToken.symbol, + token_contract_address: importedToken.address, + token_decimal_precision: importedToken.decimals, + source: MetaMetricsTokenEventSource.Detected, + token_standard: TokenStandard.ERC20, + asset_type: AssetType.token, + token_added_type: 'detected', + chain_id: chainConfig.chainId, + }, + }); + }); + }, + ); + + await Promise.all(importPromises); + } else if (detectedTokens.length > 0) { + await dispatch(addImportedTokens(detectedTokens, networkClientId)); + detectedTokens.forEach((importedToken: Token) => { + // when multichain is fully integrated, we should change these event signatures for analytics + trackEvent({ + event: MetaMetricsEventName.TokenAdded, + category: MetaMetricsEventCategory.Wallet, + sensitiveProperties: { + token_symbol: importedToken.symbol, + token_contract_address: importedToken.address, + token_decimal_precision: importedToken.decimals, + source: MetaMetricsTokenEventSource.Detected, + token_standard: TokenStandard.ERC20, + asset_type: AssetType.token, + token_added_type: 'detected', + chain_id: currentChainId, + }, + }); + }); + } +}; From 09a8195ffe9a0129246163f48327653f6381ffec Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Wed, 5 Feb 2025 14:29:51 -0800 Subject: [PATCH 03/18] refactor: consolidateBalances to getTokenBalancesEvm selector --- .../app/assets/token-list/token-list.tsx | 23 +++-- ui/selectors/multichain.ts | 86 +++++++++++++++++++ 2 files changed, 101 insertions(+), 8 deletions(-) diff --git a/ui/components/app/assets/token-list/token-list.tsx b/ui/components/app/assets/token-list/token-list.tsx index 1504e89242a2..844b067126b0 100644 --- a/ui/components/app/assets/token-list/token-list.tsx +++ b/ui/components/app/assets/token-list/token-list.tsx @@ -27,7 +27,10 @@ import { endTrace, TraceName } from '../../../../../shared/lib/trace'; import { useTokenBalances } from '../../../../hooks/useTokenBalances'; import { setTokenNetworkFilter } from '../../../../store/actions'; import { useMultichainSelector } from '../../../../hooks/useMultichainSelector'; -import { getMultichainShouldShowFiat } from '../../../../selectors/multichain'; +import { + getMultichainShouldShowFiat, + getTokenBalancesEvm, +} from '../../../../selectors/multichain'; import { consolidateTokenBalances } from '../util/consolidateTokenBalances'; type TokenListProps = { @@ -102,12 +105,16 @@ export default function TokenList({ getIsTokenNetworkFilterEqualCurrentNetwork, ); + // EVM specific tokenBalance polling const { tokenBalances } = useTokenBalances({ chainIds: chainIdsToPoll as Hex[], }); const selectedAccountTokenBalancesAcrossChains = tokenBalances[selectedAccount.address]; + // const evmBalances = useSelector(getTokenBalancesEvm); + // console.log('evmBalances', evmBalances); + const marketData: ChainAddressMarketData = useSelector( getMarketData, ) as ChainAddressMarketData; @@ -131,13 +138,13 @@ export default function TokenList({ const sortedFilteredTokens = useMemo(() => { const consolidatedTokensWithBalances = consolidateTokenBalances( - selectedAccountTokensChains, - nativeBalances, - selectedAccountTokenBalancesAcrossChains, - marketData, - currencyRates, - hideZeroBalanceTokens, - isOnCurrentNetwork, + selectedAccountTokensChains, // done + nativeBalances, // done + selectedAccountTokenBalancesAcrossChains, // can't bc needs polling + marketData, // done + currencyRates, // done + hideZeroBalanceTokens, // done (getPreferences) + isOnCurrentNetwork, // done ); const filteredAssets = filterAssets(consolidatedTokensWithBalances, [ { diff --git a/ui/selectors/multichain.ts b/ui/selectors/multichain.ts index a21c2d84015c..20984a403828 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -38,13 +38,26 @@ import { } from '../../shared/modules/selectors/networks'; import { AccountsState, getSelectedInternalAccount } from './accounts'; import { + getCurrencyRates, getIsMainnet, + getIsTokenNetworkFilterEqualCurrentNetwork, + getMarketData, getMaybeSelectedInternalAccount, getNativeCurrencyImage, + getPreferences, getSelectedAccountCachedBalance, + getSelectedAccountNativeTokenCachedBalanceByChainId, + getSelectedAccountTokensAcrossChains, getShouldShowFiat, getShowFiatInTestnets, } from './selectors'; +import { createDeepEqualSelector } from '../../shared/modules/selectors/util'; +import { + Token, + TokenWithFiatAmount, +} from '../components/app/assets/token-list/token-list'; +import { calculateTokenBalance } from '../components/app/assets/util/calculateTokenBalance'; +import { calculateTokenFiatAmount } from '../components/app/assets/util/calculateTokenFiatAmount'; export type RatesState = { metamask: RatesControllerState; @@ -376,6 +389,79 @@ export function getMultichainBalances( return state.metamask.balances; } +// consolidateTokenBalances +export const getTokenBalancesEvm = createDeepEqualSelector( + getSelectedAccountTokensAcrossChains, // TODO: useFilteredAccountTokens, we need to filter Testnets + getSelectedAccountNativeTokenCachedBalanceByChainId, + getMarketData, + getCurrencyRates, + getPreferences, + getIsTokenNetworkFilterEqualCurrentNetwork, + ( + selectedAccountTokensChains, + nativeBalances, + marketData, + currencyRates, + preferences, + isOnCurrentNetwork, + ) => { + const { hideZeroBalanceTokens } = preferences; + + const tokensWithBalance: TokenWithFiatAmount[] = []; + Object.entries(selectedAccountTokensChains).forEach( + ([stringChainKey, tokens]) => { + const chainId = stringChainKey as Hex; + tokens.forEach((token: Token) => { + const { isNative, address, decimals } = token; + const balance = + calculateTokenBalance({ + isNative, + chainId, + address, + decimals, + // @ts-ignore + nativeBalances, + // @ts-ignore + selectedAccountTokenBalancesAcrossChains, + }) || '0'; + + const tokenFiatAmount = calculateTokenFiatAmount({ + token, + chainId, + balance, + marketData, + currencyRates, + }); + + // Respect the "hide zero balance" setting (when true): + // - Native tokens should always display with zero balance when on the current network filter. + // - Native tokens should not display with zero balance when on all networks filter + // - ERC20 tokens with zero balances should respect the setting on both the current and all networks. + + // Respect the "hide zero balance" setting (when false): + // - Native tokens should always display with zero balance when on the current network filter. + // - Native tokens should always display with zero balance when on all networks filter + // - ERC20 tokens always display with zero balance on both the current and all networks filter. + if ( + !hideZeroBalanceTokens || + balance !== '0' || + (token.isNative && isOnCurrentNetwork) + ) { + tokensWithBalance.push({ + ...token, + balance, + tokenFiatAmount, + chainId, + string: String(balance), + }); + } + }); + }, + ); + return tokensWithBalance; + }, +); + export function getMultichainTransactions( state: MultichainState, ): TransactionsState['metamask']['nonEvmTransactions'] { From b2febcc3b69add082ab15c974e7a18d2c0446dbe Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Wed, 5 Feb 2025 15:44:56 -0800 Subject: [PATCH 04/18] chore: Remove consolidatedBalances util in favor of fleshed out selector --- .../app/assets/token-list/token-list.tsx | 46 +---- .../assets/util/consolidateTokenBalances.ts | 76 -------- ui/selectors/multichain.ts | 181 +++++++++--------- ui/selectors/selectors.js | 85 ++++++++ 4 files changed, 187 insertions(+), 201 deletions(-) delete mode 100644 ui/components/app/assets/util/consolidateTokenBalances.ts diff --git a/ui/components/app/assets/token-list/token-list.tsx b/ui/components/app/assets/token-list/token-list.tsx index 844b067126b0..34d7add43315 100644 --- a/ui/components/app/assets/token-list/token-list.tsx +++ b/ui/components/app/assets/token-list/token-list.tsx @@ -6,20 +6,17 @@ import { TEST_CHAINS } from '../../../../../shared/constants/network'; import { sortAssets } from '../util/sort'; import { getChainIdsToPoll, - getCurrencyRates, getCurrentNetwork, getIsTestnet, - getIsTokenNetworkFilterEqualCurrentNetwork, - getMarketData, getNetworkConfigurationIdByChainId, getNewTokensImported, getPreferences, getSelectedAccount, - getSelectedAccountNativeTokenCachedBalanceByChainId, getSelectedAccountTokensAcrossChains, getShowFiatInTestnets, getTokenExchangeRates, getTokenNetworkFilter, + getTokenBalancesEvm, } from '../../../../selectors'; import { getConversionRate } from '../../../../ducks/metamask/metamask'; import { filterAssets } from '../util/filter'; @@ -27,11 +24,7 @@ import { endTrace, TraceName } from '../../../../../shared/lib/trace'; import { useTokenBalances } from '../../../../hooks/useTokenBalances'; import { setTokenNetworkFilter } from '../../../../store/actions'; import { useMultichainSelector } from '../../../../hooks/useMultichainSelector'; -import { - getMultichainShouldShowFiat, - getTokenBalancesEvm, -} from '../../../../selectors/multichain'; -import { consolidateTokenBalances } from '../util/consolidateTokenBalances'; +import { getMultichainShouldShowFiat } from '../../../../selectors/multichain'; type TokenListProps = { onTokenClick: (chainId: string, address: string) => void; @@ -89,8 +82,7 @@ export default function TokenList({ const dispatch = useDispatch(); const currentNetwork = useSelector(getCurrentNetwork); const allNetworks = useSelector(getNetworkConfigurationIdByChainId); - const { tokenSortConfig, privacyMode, hideZeroBalanceTokens } = - useSelector(getPreferences); + const { tokenSortConfig, privacyMode } = useSelector(getPreferences); const tokenNetworkFilter = useSelector(getTokenNetworkFilter); const selectedAccount = useSelector(getSelectedAccount); const conversionRate = useSelector(getConversionRate); @@ -101,28 +93,13 @@ export default function TokenList({ ); const newTokensImported = useSelector(getNewTokensImported); const selectedAccountTokensChains = useFilteredAccountTokens(currentNetwork); - const isOnCurrentNetwork = useSelector( - getIsTokenNetworkFilterEqualCurrentNetwork, - ); - // EVM specific tokenBalance polling - const { tokenBalances } = useTokenBalances({ + // EVM specific tokenBalance polling, updates state via polling loop per chainId + useTokenBalances({ chainIds: chainIdsToPoll as Hex[], }); - const selectedAccountTokenBalancesAcrossChains = - tokenBalances[selectedAccount.address]; - // const evmBalances = useSelector(getTokenBalancesEvm); - // console.log('evmBalances', evmBalances); - - const marketData: ChainAddressMarketData = useSelector( - getMarketData, - ) as ChainAddressMarketData; - - const currencyRates = useSelector(getCurrencyRates); - const nativeBalances: Record = useSelector( - getSelectedAccountNativeTokenCachedBalanceByChainId, - ) as Record; + const evmBalances = useSelector(getTokenBalancesEvm); const isTestnet = useSelector(getIsTestnet); // Ensure newly added networks are included in the tokenNetworkFilter useEffect(() => { @@ -137,16 +114,7 @@ export default function TokenList({ }, [Object.keys(allNetworks).length]); const sortedFilteredTokens = useMemo(() => { - const consolidatedTokensWithBalances = consolidateTokenBalances( - selectedAccountTokensChains, // done - nativeBalances, // done - selectedAccountTokenBalancesAcrossChains, // can't bc needs polling - marketData, // done - currencyRates, // done - hideZeroBalanceTokens, // done (getPreferences) - isOnCurrentNetwork, // done - ); - const filteredAssets = filterAssets(consolidatedTokensWithBalances, [ + const filteredAssets = filterAssets(evmBalances, [ { key: 'chainId', opts: tokenNetworkFilter, diff --git a/ui/components/app/assets/util/consolidateTokenBalances.ts b/ui/components/app/assets/util/consolidateTokenBalances.ts deleted file mode 100644 index 6b46235c8cad..000000000000 --- a/ui/components/app/assets/util/consolidateTokenBalances.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Hex } from '@metamask/utils'; -import { - ChainAddressMarketData, - Token, - TokenWithFiatAmount, -} from '../token-list/token-list'; -import { calculateTokenBalance } from './calculateTokenBalance'; -import { - SymbolCurrencyRateMapping, - calculateTokenFiatAmount, -} from './calculateTokenFiatAmount'; - -export const consolidateTokenBalances = ( - selectedAccountTokensChains: Record, - nativeBalances: Record, - selectedAccountTokenBalancesAcrossChains: Record< - `0x${string}`, - Record<`0x${string}`, `0x${string}`> - >, - marketData: ChainAddressMarketData, - currencyRates: SymbolCurrencyRateMapping, - hideZeroBalanceTokens: boolean, - isOnCurrentNetwork: boolean, -) => { - const tokensWithBalance: TokenWithFiatAmount[] = []; - Object.entries(selectedAccountTokensChains).forEach( - ([stringChainKey, tokens]) => { - const chainId = stringChainKey as Hex; - tokens.forEach((token: Token) => { - const { isNative, address, decimals } = token; - const balance = - calculateTokenBalance({ - isNative, - chainId, - address, - decimals, - nativeBalances, - selectedAccountTokenBalancesAcrossChains, - }) || '0'; - - const tokenFiatAmount = calculateTokenFiatAmount({ - token, - chainId, - balance, - marketData, - currencyRates, - }); - - // Respect the "hide zero balance" setting (when true): - // - Native tokens should always display with zero balance when on the current network filter. - // - Native tokens should not display with zero balance when on all networks filter - // - ERC20 tokens with zero balances should respect the setting on both the current and all networks. - - // Respect the "hide zero balance" setting (when false): - // - Native tokens should always display with zero balance when on the current network filter. - // - Native tokens should always display with zero balance when on all networks filter - // - ERC20 tokens always display with zero balance on both the current and all networks filter. - if ( - !hideZeroBalanceTokens || - balance !== '0' || - (token.isNative && isOnCurrentNetwork) - ) { - tokensWithBalance.push({ - ...token, - balance, - tokenFiatAmount, - chainId, - string: String(balance), - }); - } - }); - }, - ); - - return tokensWithBalance; -}; diff --git a/ui/selectors/multichain.ts b/ui/selectors/multichain.ts index 20984a403828..a758e122cae4 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -21,6 +21,7 @@ import { getConversionRate, getNativeCurrency, getCurrentCurrency, + // getTokenBalances, } from '../ducks/metamask/metamask'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths @@ -36,28 +37,29 @@ import { getNetworkConfigurationsByChainId, getCurrentChainId, } from '../../shared/modules/selectors/networks'; +// import { createDeepEqualSelector } from '../../shared/modules/selectors/util'; +// import { +// Token, +// TokenWithFiatAmount, +// } from '../components/app/assets/token-list/token-list'; +// import { calculateTokenBalance } from '../components/app/assets/util/calculateTokenBalance'; +// import { calculateTokenFiatAmount } from '../components/app/assets/util/calculateTokenFiatAmount'; import { AccountsState, getSelectedInternalAccount } from './accounts'; import { - getCurrencyRates, + // getCurrencyRates, getIsMainnet, - getIsTokenNetworkFilterEqualCurrentNetwork, - getMarketData, + // getIsTokenNetworkFilterEqualCurrentNetwork, + // getMarketData, getMaybeSelectedInternalAccount, getNativeCurrencyImage, - getPreferences, + // getPreferences, + // getSelectedAccount, getSelectedAccountCachedBalance, - getSelectedAccountNativeTokenCachedBalanceByChainId, - getSelectedAccountTokensAcrossChains, + // getSelectedAccountNativeTokenCachedBalanceByChainId, + // getSelectedAccountTokensAcrossChains, getShouldShowFiat, getShowFiatInTestnets, } from './selectors'; -import { createDeepEqualSelector } from '../../shared/modules/selectors/util'; -import { - Token, - TokenWithFiatAmount, -} from '../components/app/assets/token-list/token-list'; -import { calculateTokenBalance } from '../components/app/assets/util/calculateTokenBalance'; -import { calculateTokenFiatAmount } from '../components/app/assets/util/calculateTokenFiatAmount'; export type RatesState = { metamask: RatesControllerState; @@ -389,79 +391,6 @@ export function getMultichainBalances( return state.metamask.balances; } -// consolidateTokenBalances -export const getTokenBalancesEvm = createDeepEqualSelector( - getSelectedAccountTokensAcrossChains, // TODO: useFilteredAccountTokens, we need to filter Testnets - getSelectedAccountNativeTokenCachedBalanceByChainId, - getMarketData, - getCurrencyRates, - getPreferences, - getIsTokenNetworkFilterEqualCurrentNetwork, - ( - selectedAccountTokensChains, - nativeBalances, - marketData, - currencyRates, - preferences, - isOnCurrentNetwork, - ) => { - const { hideZeroBalanceTokens } = preferences; - - const tokensWithBalance: TokenWithFiatAmount[] = []; - Object.entries(selectedAccountTokensChains).forEach( - ([stringChainKey, tokens]) => { - const chainId = stringChainKey as Hex; - tokens.forEach((token: Token) => { - const { isNative, address, decimals } = token; - const balance = - calculateTokenBalance({ - isNative, - chainId, - address, - decimals, - // @ts-ignore - nativeBalances, - // @ts-ignore - selectedAccountTokenBalancesAcrossChains, - }) || '0'; - - const tokenFiatAmount = calculateTokenFiatAmount({ - token, - chainId, - balance, - marketData, - currencyRates, - }); - - // Respect the "hide zero balance" setting (when true): - // - Native tokens should always display with zero balance when on the current network filter. - // - Native tokens should not display with zero balance when on all networks filter - // - ERC20 tokens with zero balances should respect the setting on both the current and all networks. - - // Respect the "hide zero balance" setting (when false): - // - Native tokens should always display with zero balance when on the current network filter. - // - Native tokens should always display with zero balance when on all networks filter - // - ERC20 tokens always display with zero balance on both the current and all networks filter. - if ( - !hideZeroBalanceTokens || - balance !== '0' || - (token.isNative && isOnCurrentNetwork) - ) { - tokensWithBalance.push({ - ...token, - balance, - tokenFiatAmount, - chainId, - string: String(balance), - }); - } - }); - }, - ); - return tokensWithBalance; - }, -); - export function getMultichainTransactions( state: MultichainState, ): TransactionsState['metamask']['nonEvmTransactions'] { @@ -554,3 +483,83 @@ export function getMultichainConversionRate( ? getConversionRate(state) : getMultichainCoinRates(state)?.[ticker.toLowerCase()]?.conversionRate; } + +// // consolidateTokenBalances +// export const getTokenBalancesEvm = createDeepEqualSelector( +// getSelectedAccountTokensAcrossChains, // TODO: useFilteredAccountTokens, we need to filter Testnets +// getSelectedAccountNativeTokenCachedBalanceByChainId, +// getTokenBalances, +// (state) => state.metamask.marketData, +// getCurrencyRates, +// getPreferences, +// getIsTokenNetworkFilterEqualCurrentNetwork, +// getSelectedAccount, +// ( +// selectedAccountTokensChains, +// nativeBalances, +// tokenBalances, +// marketData, +// currencyRates, +// preferences, +// isOnCurrentNetwork, +// selectedAccount, +// ) => { +// const { hideZeroBalanceTokens } = preferences; +// const selectedAccountTokenBalancesAcrossChains = +// tokenBalances[selectedAccount.address]; + +// const tokensWithBalance: TokenWithFiatAmount[] = []; +// Object.entries(selectedAccountTokensChains).forEach( +// ([stringChainKey, tokens]) => { +// const chainId = stringChainKey as Hex; +// // @ts-ignore +// tokens.forEach((token: Token) => { +// const { isNative, address, decimals } = token; +// const balance = +// calculateTokenBalance({ +// isNative, +// chainId, +// address, +// decimals, +// // @ts-ignore +// nativeBalances, +// // @ts-ignore +// selectedAccountTokenBalancesAcrossChains, +// }) || '0'; + +// const tokenFiatAmount = calculateTokenFiatAmount({ +// token, +// chainId, +// balance, +// marketData, +// currencyRates, +// }); + +// // Respect the "hide zero balance" setting (when true): +// // - Native tokens should always display with zero balance when on the current network filter. +// // - Native tokens should not display with zero balance when on all networks filter +// // - ERC20 tokens with zero balances should respect the setting on both the current and all networks. + +// // Respect the "hide zero balance" setting (when false): +// // - Native tokens should always display with zero balance when on the current network filter. +// // - Native tokens should always display with zero balance when on all networks filter +// // - ERC20 tokens always display with zero balance on both the current and all networks filter. +// if ( +// !hideZeroBalanceTokens || +// balance !== '0' || +// (token.isNative && isOnCurrentNetwork) +// ) { +// tokensWithBalance.push({ +// ...token, +// balance, +// tokenFiatAmount, +// chainId, +// string: String(balance), +// }); +// } +// }); +// }, +// ); +// return tokensWithBalance; +// }, +// ); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index cf169592151a..4247c99f2929 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -90,6 +90,7 @@ import { isAddressLedger, getIsUnlocked, getCompletedOnboarding, + getTokenBalances, } from '../ducks/metamask/metamask'; import { getLedgerWebHidConnectedStatus, @@ -109,6 +110,8 @@ import { hasTransactionData } from '../../shared/modules/transaction.utils'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { createDeepEqualSelector } from '../../shared/modules/selectors/util'; import { isSnapIgnoredInProd } from '../helpers/utils/snaps'; +import { calculateTokenBalance } from '../components/app/assets/util/calculateTokenBalance'; +import { calculateTokenFiatAmount } from '../components/app/assets/util/calculateTokenFiatAmount'; import { getAllUnapprovedTransactions, getCurrentNetworkTransactions, @@ -564,6 +567,8 @@ export function getCrossChainMetaMaskCachedBalances(state) { return acc; }, {}); } + +export function getMultichainTokenBalances(state) {} /** * Based on the current account address, return the balance for the native token of all chain networks on that account * @@ -2977,3 +2982,83 @@ export function getKeyringSnapAccounts(state) { return keyringAccounts; } ///: END:ONLY_INCLUDE_IF + +// consolidateTokenBalances +export const getTokenBalancesEvm = createDeepEqualSelector( + getSelectedAccountTokensAcrossChains, // TODO: useFilteredAccountTokens, we need to filter Testnets + getSelectedAccountNativeTokenCachedBalanceByChainId, + getTokenBalances, + (state) => state.metamask.marketData, + getCurrencyRates, + getPreferences, + getIsTokenNetworkFilterEqualCurrentNetwork, + getSelectedAccount, + ( + selectedAccountTokensChains, + nativeBalances, + tokenBalances, + marketData, + currencyRates, + preferences, + isOnCurrentNetwork, + selectedAccount, + ) => { + const { hideZeroBalanceTokens } = preferences; + const selectedAccountTokenBalancesAcrossChains = + tokenBalances[selectedAccount.address]; + + const tokensWithBalance = []; + Object.entries(selectedAccountTokensChains).forEach( + ([stringChainKey, tokens]) => { + const chainId = stringChainKey; + // @ts-ignore + tokens.forEach((token) => { + const { isNative, address, decimals } = token; + const balance = + calculateTokenBalance({ + isNative, + chainId, + address, + decimals, + // @ts-ignore + nativeBalances, + // @ts-ignore + selectedAccountTokenBalancesAcrossChains, + }) || '0'; + + const tokenFiatAmount = calculateTokenFiatAmount({ + token, + chainId, + balance, + marketData, + currencyRates, + }); + + // Respect the "hide zero balance" setting (when true): + // - Native tokens should always display with zero balance when on the current network filter. + // - Native tokens should not display with zero balance when on all networks filter + // - ERC20 tokens with zero balances should respect the setting on both the current and all networks. + + // Respect the "hide zero balance" setting (when false): + // - Native tokens should always display with zero balance when on the current network filter. + // - Native tokens should always display with zero balance when on all networks filter + // - ERC20 tokens always display with zero balance on both the current and all networks filter. + if ( + !hideZeroBalanceTokens || + balance !== '0' || + (token.isNative && isOnCurrentNetwork) + ) { + tokensWithBalance.push({ + ...token, + balance, + tokenFiatAmount, + chainId, + string: String(balance), + }); + } + }); + }, + ); + return tokensWithBalance; + }, +); From ec9c6bb83466a165a19a273de699c1f0e4f56cac Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Wed, 5 Feb 2025 15:51:18 -0800 Subject: [PATCH 05/18] chore: cleanup --- ui/selectors/multichain.ts | 95 -------------------------------------- ui/selectors/selectors.js | 2 +- 2 files changed, 1 insertion(+), 96 deletions(-) diff --git a/ui/selectors/multichain.ts b/ui/selectors/multichain.ts index a758e122cae4..a21c2d84015c 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -21,7 +21,6 @@ import { getConversionRate, getNativeCurrency, getCurrentCurrency, - // getTokenBalances, } from '../ducks/metamask/metamask'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths @@ -37,26 +36,12 @@ import { getNetworkConfigurationsByChainId, getCurrentChainId, } from '../../shared/modules/selectors/networks'; -// import { createDeepEqualSelector } from '../../shared/modules/selectors/util'; -// import { -// Token, -// TokenWithFiatAmount, -// } from '../components/app/assets/token-list/token-list'; -// import { calculateTokenBalance } from '../components/app/assets/util/calculateTokenBalance'; -// import { calculateTokenFiatAmount } from '../components/app/assets/util/calculateTokenFiatAmount'; import { AccountsState, getSelectedInternalAccount } from './accounts'; import { - // getCurrencyRates, getIsMainnet, - // getIsTokenNetworkFilterEqualCurrentNetwork, - // getMarketData, getMaybeSelectedInternalAccount, getNativeCurrencyImage, - // getPreferences, - // getSelectedAccount, getSelectedAccountCachedBalance, - // getSelectedAccountNativeTokenCachedBalanceByChainId, - // getSelectedAccountTokensAcrossChains, getShouldShowFiat, getShowFiatInTestnets, } from './selectors'; @@ -483,83 +468,3 @@ export function getMultichainConversionRate( ? getConversionRate(state) : getMultichainCoinRates(state)?.[ticker.toLowerCase()]?.conversionRate; } - -// // consolidateTokenBalances -// export const getTokenBalancesEvm = createDeepEqualSelector( -// getSelectedAccountTokensAcrossChains, // TODO: useFilteredAccountTokens, we need to filter Testnets -// getSelectedAccountNativeTokenCachedBalanceByChainId, -// getTokenBalances, -// (state) => state.metamask.marketData, -// getCurrencyRates, -// getPreferences, -// getIsTokenNetworkFilterEqualCurrentNetwork, -// getSelectedAccount, -// ( -// selectedAccountTokensChains, -// nativeBalances, -// tokenBalances, -// marketData, -// currencyRates, -// preferences, -// isOnCurrentNetwork, -// selectedAccount, -// ) => { -// const { hideZeroBalanceTokens } = preferences; -// const selectedAccountTokenBalancesAcrossChains = -// tokenBalances[selectedAccount.address]; - -// const tokensWithBalance: TokenWithFiatAmount[] = []; -// Object.entries(selectedAccountTokensChains).forEach( -// ([stringChainKey, tokens]) => { -// const chainId = stringChainKey as Hex; -// // @ts-ignore -// tokens.forEach((token: Token) => { -// const { isNative, address, decimals } = token; -// const balance = -// calculateTokenBalance({ -// isNative, -// chainId, -// address, -// decimals, -// // @ts-ignore -// nativeBalances, -// // @ts-ignore -// selectedAccountTokenBalancesAcrossChains, -// }) || '0'; - -// const tokenFiatAmount = calculateTokenFiatAmount({ -// token, -// chainId, -// balance, -// marketData, -// currencyRates, -// }); - -// // Respect the "hide zero balance" setting (when true): -// // - Native tokens should always display with zero balance when on the current network filter. -// // - Native tokens should not display with zero balance when on all networks filter -// // - ERC20 tokens with zero balances should respect the setting on both the current and all networks. - -// // Respect the "hide zero balance" setting (when false): -// // - Native tokens should always display with zero balance when on the current network filter. -// // - Native tokens should always display with zero balance when on all networks filter -// // - ERC20 tokens always display with zero balance on both the current and all networks filter. -// if ( -// !hideZeroBalanceTokens || -// balance !== '0' || -// (token.isNative && isOnCurrentNetwork) -// ) { -// tokensWithBalance.push({ -// ...token, -// balance, -// tokenFiatAmount, -// chainId, -// string: String(balance), -// }); -// } -// }); -// }, -// ); -// return tokensWithBalance; -// }, -// ); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 4247c99f2929..3ef7de4cb15b 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -2988,7 +2988,7 @@ export const getTokenBalancesEvm = createDeepEqualSelector( getSelectedAccountTokensAcrossChains, // TODO: useFilteredAccountTokens, we need to filter Testnets getSelectedAccountNativeTokenCachedBalanceByChainId, getTokenBalances, - (state) => state.metamask.marketData, + getMarketData, getCurrencyRates, getPreferences, getIsTokenNetworkFilterEqualCurrentNetwork, From 746f67d2a864cc23d38c30d4e7de4be86349c478 Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Wed, 5 Feb 2025 16:01:52 -0800 Subject: [PATCH 06/18] chore: Move useFilteredAccountTokens to selector --- .../app/assets/token-list/token-list.tsx | 24 ------------------- ui/selectors/selectors.js | 12 +++++++++- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/ui/components/app/assets/token-list/token-list.tsx b/ui/components/app/assets/token-list/token-list.tsx index 34d7add43315..e79c0ea37aaf 100644 --- a/ui/components/app/assets/token-list/token-list.tsx +++ b/ui/components/app/assets/token-list/token-list.tsx @@ -53,28 +53,6 @@ export type ChainAddressMarketData = Record< Record> >; -const useFilteredAccountTokens = (currentNetwork: { chainId: string }) => { - const isTestNetwork = useMemo(() => { - return (TEST_CHAINS as string[]).includes(currentNetwork.chainId); - }, [currentNetwork.chainId, TEST_CHAINS]); - - const selectedAccountTokensChains: Record = useSelector( - getSelectedAccountTokensAcrossChains, - ) as Record; - - const filteredAccountTokensChains = useMemo(() => { - return Object.fromEntries( - Object.entries(selectedAccountTokensChains).filter(([chainId]) => - isTestNetwork - ? (TEST_CHAINS as string[]).includes(chainId) - : !(TEST_CHAINS as string[]).includes(chainId), - ), - ); - }, [selectedAccountTokensChains, isTestNetwork, TEST_CHAINS]); - - return filteredAccountTokensChains; -}; - export default function TokenList({ onTokenClick, nativeToken, @@ -92,7 +70,6 @@ export default function TokenList({ shallowEqual, ); const newTokensImported = useSelector(getNewTokensImported); - const selectedAccountTokensChains = useFilteredAccountTokens(currentNetwork); // EVM specific tokenBalance polling, updates state via polling loop per chainId useTokenBalances({ @@ -145,7 +122,6 @@ export default function TokenList({ contractExchangeRates, currentNetwork, selectedAccount, - selectedAccountTokensChains, newTokensImported, ]); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 3ef7de4cb15b..1a5014b1c184 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -2993,6 +2993,7 @@ export const getTokenBalancesEvm = createDeepEqualSelector( getPreferences, getIsTokenNetworkFilterEqualCurrentNetwork, getSelectedAccount, + getCurrentNetwork, ( selectedAccountTokensChains, nativeBalances, @@ -3002,13 +3003,22 @@ export const getTokenBalancesEvm = createDeepEqualSelector( preferences, isOnCurrentNetwork, selectedAccount, + currentNetwork, ) => { const { hideZeroBalanceTokens } = preferences; const selectedAccountTokenBalancesAcrossChains = tokenBalances[selectedAccount.address]; + const isTestNetwork = TEST_CHAINS.includes(currentNetwork.chainId); + const filteredAccountTokensChains = Object.fromEntries( + Object.entries(selectedAccountTokensChains).filter(([chainId]) => + isTestNetwork + ? TEST_CHAINS.includes(chainId) + : !TEST_CHAINS.includes(chainId), + ), + ); const tokensWithBalance = []; - Object.entries(selectedAccountTokensChains).forEach( + Object.entries(filteredAccountTokensChains).forEach( ([stringChainKey, tokens]) => { const chainId = stringChainKey; // @ts-ignore From f17ec92a83cb203571a4de99be609c86cafa8055 Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Wed, 5 Feb 2025 16:33:51 -0800 Subject: [PATCH 07/18] refactor: Breakout redundant business logic into reusable hooks --- .../app/assets/hooks/useNetworkFilter.tsx | 30 +++++ .../app/assets/hooks/useShouldShowFiat.tsx | 24 ++++ .../assets/hooks/useSortedFilteredTokens.tsx | 69 +++++++++++ .../app/assets/token-list/token-list.tsx | 107 ++---------------- 4 files changed, 134 insertions(+), 96 deletions(-) create mode 100644 ui/components/app/assets/hooks/useNetworkFilter.tsx create mode 100644 ui/components/app/assets/hooks/useShouldShowFiat.tsx create mode 100644 ui/components/app/assets/hooks/useSortedFilteredTokens.tsx diff --git a/ui/components/app/assets/hooks/useNetworkFilter.tsx b/ui/components/app/assets/hooks/useNetworkFilter.tsx new file mode 100644 index 000000000000..1f8e352156a5 --- /dev/null +++ b/ui/components/app/assets/hooks/useNetworkFilter.tsx @@ -0,0 +1,30 @@ +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { setTokenNetworkFilter } from '../../../../store/actions'; +import { + getNetworkConfigurationIdByChainId, + getTokenNetworkFilter, +} from '../../../../selectors'; + +const useNetworkFilter = () => { + const dispatch = useDispatch(); + + const allNetworks = useSelector(getNetworkConfigurationIdByChainId); + const networkFilter = useSelector(getTokenNetworkFilter); + + useEffect(() => { + if (process.env.PORTFOLIO_VIEW) { + const allNetworkFilters = Object.fromEntries( + Object.keys(allNetworks).map((chainId) => [chainId, true]), + ); + + if (Object.keys(networkFilter).length > 1) { + dispatch(setTokenNetworkFilter(allNetworkFilters)); + } + } + }, [Object.keys(allNetworks).length, networkFilter, dispatch]); + + return { networkFilter }; +}; + +export default useNetworkFilter; diff --git a/ui/components/app/assets/hooks/useShouldShowFiat.tsx b/ui/components/app/assets/hooks/useShouldShowFiat.tsx new file mode 100644 index 000000000000..22b04d3c677b --- /dev/null +++ b/ui/components/app/assets/hooks/useShouldShowFiat.tsx @@ -0,0 +1,24 @@ +import { useSelector } from 'react-redux'; +import { useMultichainSelector } from '../../../../hooks/useMultichainSelector'; +import { + getIsTestnet, + getSelectedAccount, + getShowFiatInTestnets, +} from '../../../../selectors'; +import { getMultichainShouldShowFiat } from '../../../../selectors/multichain'; + +const useShouldShowFiat = () => { + const isTestnet = useSelector(getIsTestnet); + const selectedAccount = useSelector(getSelectedAccount); + const shouldShowFiat = useMultichainSelector( + getMultichainShouldShowFiat, + selectedAccount, + ); + + const isMainnet = !isTestnet; + const showFiatInTestnets = useSelector(getShowFiatInTestnets); + + return shouldShowFiat && (isMainnet || (isTestnet && showFiatInTestnets)); +}; + +export default useShouldShowFiat; diff --git a/ui/components/app/assets/hooks/useSortedFilteredTokens.tsx b/ui/components/app/assets/hooks/useSortedFilteredTokens.tsx new file mode 100644 index 000000000000..42e66addac94 --- /dev/null +++ b/ui/components/app/assets/hooks/useSortedFilteredTokens.tsx @@ -0,0 +1,69 @@ +import { useMemo } from 'react'; +import { sortAssets } from '../util/sort'; +import { filterAssets } from '../util/filter'; +import { TokenWithFiatAmount } from '../token-list/token-list'; +import useNetworkFilter from './useNetworkFilter'; +import { shallowEqual, useSelector } from 'react-redux'; +import { + getCurrentNetwork, + getNewTokensImported, + getPreferences, + getSelectedAccount, + getTokenBalancesEvm, + getTokenExchangeRates, +} from '../../../../selectors'; +import { getConversionRate } from '../../../../ducks/metamask/metamask'; + +const useSortedFilteredTokens = () => { + const currentNetwork = useSelector(getCurrentNetwork); + const { tokenSortConfig } = useSelector(getPreferences); + const selectedAccount = useSelector(getSelectedAccount); + const conversionRate = useSelector(getConversionRate); + const contractExchangeRates = useSelector( + getTokenExchangeRates, + shallowEqual, + ); + const newTokensImported = useSelector(getNewTokensImported); + const evmBalances = useSelector(getTokenBalancesEvm); // TODO: Make this chain agnostic + + // network filter to determine which tokens to show in list + const { networkFilter } = useNetworkFilter(); + + return useMemo(() => { + const filteredAssets = filterAssets(evmBalances, [ + { + key: 'chainId', + opts: networkFilter, + filterCallback: 'inclusive', + }, + ]); + + const { nativeTokens, nonNativeTokens } = filteredAssets.reduce<{ + nativeTokens: TokenWithFiatAmount[]; + nonNativeTokens: TokenWithFiatAmount[]; + }>( + (acc, token) => { + if (token.isNative) { + acc.nativeTokens.push(token); + } else { + acc.nonNativeTokens.push(token); + } + return acc; + }, + { nativeTokens: [], nonNativeTokens: [] }, + ); + + const assets = [...nativeTokens, ...nonNativeTokens]; + return sortAssets(assets, tokenSortConfig); + }, [ + tokenSortConfig, + networkFilter, + conversionRate, + contractExchangeRates, + currentNetwork, + selectedAccount, + newTokensImported, + ]); +}; + +export default useSortedFilteredTokens; diff --git a/ui/components/app/assets/token-list/token-list.tsx b/ui/components/app/assets/token-list/token-list.tsx index e79c0ea37aaf..429dd48326e4 100644 --- a/ui/components/app/assets/token-list/token-list.tsx +++ b/ui/components/app/assets/token-list/token-list.tsx @@ -1,30 +1,12 @@ -import React, { ReactNode, useEffect, useMemo } from 'react'; -import { shallowEqual, useSelector, useDispatch } from 'react-redux'; +import React, { ReactNode, useEffect } from 'react'; +import { useSelector } from 'react-redux'; import { Hex } from '@metamask/utils'; import TokenCell from '../token-cell'; -import { TEST_CHAINS } from '../../../../../shared/constants/network'; -import { sortAssets } from '../util/sort'; -import { - getChainIdsToPoll, - getCurrentNetwork, - getIsTestnet, - getNetworkConfigurationIdByChainId, - getNewTokensImported, - getPreferences, - getSelectedAccount, - getSelectedAccountTokensAcrossChains, - getShowFiatInTestnets, - getTokenExchangeRates, - getTokenNetworkFilter, - getTokenBalancesEvm, -} from '../../../../selectors'; -import { getConversionRate } from '../../../../ducks/metamask/metamask'; -import { filterAssets } from '../util/filter'; +import { getChainIdsToPoll, getPreferences } from '../../../../selectors'; import { endTrace, TraceName } from '../../../../../shared/lib/trace'; -import { useTokenBalances } from '../../../../hooks/useTokenBalances'; -import { setTokenNetworkFilter } from '../../../../store/actions'; -import { useMultichainSelector } from '../../../../hooks/useMultichainSelector'; -import { getMultichainShouldShowFiat } from '../../../../selectors/multichain'; +import { useTokenBalances as pollAndUpdateEvmBalances } from '../../../../hooks/useTokenBalances'; +import useSortedFilteredTokens from '../hooks/useSortedFilteredTokens'; +import useShouldShowFiat from '../hooks/useShouldShowFiat'; type TokenListProps = { onTokenClick: (chainId: string, address: string) => void; @@ -57,73 +39,16 @@ export default function TokenList({ onTokenClick, nativeToken, }: TokenListProps) { - const dispatch = useDispatch(); - const currentNetwork = useSelector(getCurrentNetwork); - const allNetworks = useSelector(getNetworkConfigurationIdByChainId); - const { tokenSortConfig, privacyMode } = useSelector(getPreferences); - const tokenNetworkFilter = useSelector(getTokenNetworkFilter); - const selectedAccount = useSelector(getSelectedAccount); - const conversionRate = useSelector(getConversionRate); + const { privacyMode } = useSelector(getPreferences); const chainIdsToPoll = useSelector(getChainIdsToPoll); - const contractExchangeRates = useSelector( - getTokenExchangeRates, - shallowEqual, - ); - const newTokensImported = useSelector(getNewTokensImported); // EVM specific tokenBalance polling, updates state via polling loop per chainId - useTokenBalances({ + pollAndUpdateEvmBalances({ chainIds: chainIdsToPoll as Hex[], }); - const evmBalances = useSelector(getTokenBalancesEvm); - const isTestnet = useSelector(getIsTestnet); - // Ensure newly added networks are included in the tokenNetworkFilter - useEffect(() => { - if (process.env.PORTFOLIO_VIEW) { - const allNetworkFilters = Object.fromEntries( - Object.keys(allNetworks).map((chainId) => [chainId, true]), - ); - if (Object.keys(tokenNetworkFilter).length > 1) { - dispatch(setTokenNetworkFilter(allNetworkFilters)); - } - } - }, [Object.keys(allNetworks).length]); - - const sortedFilteredTokens = useMemo(() => { - const filteredAssets = filterAssets(evmBalances, [ - { - key: 'chainId', - opts: tokenNetworkFilter, - filterCallback: 'inclusive', - }, - ]); - - const { nativeTokens, nonNativeTokens } = filteredAssets.reduce<{ - nativeTokens: TokenWithFiatAmount[]; - nonNativeTokens: TokenWithFiatAmount[]; - }>( - (acc, token) => { - if (token.isNative) { - acc.nativeTokens.push(token); - } else { - acc.nonNativeTokens.push(token); - } - return acc; - }, - { nativeTokens: [], nonNativeTokens: [] }, - ); - const assets = [...nativeTokens, ...nonNativeTokens]; - return sortAssets(assets, tokenSortConfig); - }, [ - tokenSortConfig, - tokenNetworkFilter, - conversionRate, - contractExchangeRates, - currentNetwork, - selectedAccount, - newTokensImported, - ]); + const sortedFilteredTokens = useSortedFilteredTokens(); + const shouldShowFiat = useShouldShowFiat(); useEffect(() => { if (sortedFilteredTokens) { @@ -136,16 +61,6 @@ export default function TokenList({ return React.cloneElement(nativeToken as React.ReactElement); } - const shouldShowFiat = useMultichainSelector( - getMultichainShouldShowFiat, - selectedAccount, - ); - const isMainnet = !isTestnet; - // Check if show conversion is enabled - const showFiatInTestnets = useSelector(getShowFiatInTestnets); - const showFiat = - shouldShowFiat && (isMainnet || (isTestnet && showFiatInTestnets)); - return (
{sortedFilteredTokens.map((tokenData) => ( @@ -154,7 +69,7 @@ export default function TokenList({ chainId={tokenData.chainId} address={tokenData.address} symbol={tokenData.symbol} - tokenFiatAmount={showFiat ? tokenData.tokenFiatAmount : null} + tokenFiatAmount={shouldShowFiat ? tokenData.tokenFiatAmount : null} image={tokenData?.image} isNative={tokenData.isNative} string={tokenData.string} From e7c3ab8b18b66e37547167a17731fcfd7ba5de56 Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Wed, 5 Feb 2025 17:23:49 -0800 Subject: [PATCH 08/18] refactor: Intorudce useTokenDetection hook and AssetListFundingModals --- .../asset-list-funding-modals.tsx | 74 ++++++++ .../asset-list-funding-modals/index.ts | 1 + .../app/assets/asset-list/asset-list.tsx | 158 ++---------------- .../hooks/useAssetListTokenDetection.tsx | 70 ++++++++ .../app/assets/hooks/useFundingModals.tsx | 13 ++ .../hooks/usePrimaryCurrencyProperties.tsx | 27 +++ 6 files changed, 195 insertions(+), 148 deletions(-) create mode 100644 ui/components/app/assets/asset-list/asset-list-funding-modals/asset-list-funding-modals.tsx create mode 100644 ui/components/app/assets/asset-list/asset-list-funding-modals/index.ts create mode 100644 ui/components/app/assets/hooks/useAssetListTokenDetection.tsx create mode 100644 ui/components/app/assets/hooks/useFundingModals.tsx create mode 100644 ui/components/app/assets/hooks/usePrimaryCurrencyProperties.tsx diff --git a/ui/components/app/assets/asset-list/asset-list-funding-modals/asset-list-funding-modals.tsx b/ui/components/app/assets/asset-list/asset-list-funding-modals/asset-list-funding-modals.tsx new file mode 100644 index 000000000000..a92c225113e4 --- /dev/null +++ b/ui/components/app/assets/asset-list/asset-list-funding-modals/asset-list-funding-modals.tsx @@ -0,0 +1,74 @@ +import React, { useState } from 'react'; +import { ReceiveModal } from '../../../../multichain'; +import { FundingMethodModal } from '../../../../multichain/funding-method-modal/funding-method-modal'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { useSelector } from 'react-redux'; +import { getSelectedAccount } from '../../../../../selectors'; +///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) +import { + getMultichainIsBitcoin, + getMultichainSelectedAccountCachedBalanceIsZero, +} from '../../../../../selectors/multichain'; +import { getIsNativeTokenBuyable } from '../../../../../ducks/ramps'; +///: END:ONLY_INCLUDE_IF +import { RampsCard } from '../../../../multichain/ramps-card'; +import { RAMPS_CARD_VARIANT_TYPES } from '../../../../multichain/ramps-card/ramps-card'; + +const AssetListFundingModals = () => { + const t = useI18nContext(); + const selectedAccount = useSelector(getSelectedAccount); + + const [showFundingMethodModal, setShowFundingMethodModal] = useState(false); + const [showReceiveModal, setShowReceiveModal] = useState(false); + + const onClickReceive = () => { + setShowFundingMethodModal(false); + setShowReceiveModal(true); + }; + + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + const balanceIsZero = useSelector( + getMultichainSelectedAccountCachedBalanceIsZero, + ); + const isBuyableChain = useSelector(getIsNativeTokenBuyable); + const shouldShowBuy = isBuyableChain && balanceIsZero; + const isBtc = useSelector(getMultichainIsBitcoin); + ///: END:ONLY_INCLUDE_IF + + return ( + <> + { + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + shouldShowBuy ? ( + setShowFundingMethodModal(true) + } + /> + ) : null + ///: END:ONLY_INCLUDE_IF + } + {showReceiveModal && selectedAccount?.address && ( + setShowReceiveModal(false)} + /> + )} + {showFundingMethodModal && ( + setShowFundingMethodModal(false)} + title={t('fundingMethod')} + onClickReceive={onClickReceive} + /> + )} + + ); +}; + +export default AssetListFundingModals; diff --git a/ui/components/app/assets/asset-list/asset-list-funding-modals/index.ts b/ui/components/app/assets/asset-list/asset-list-funding-modals/index.ts new file mode 100644 index 000000000000..4ddad869fde1 --- /dev/null +++ b/ui/components/app/assets/asset-list/asset-list-funding-modals/index.ts @@ -0,0 +1 @@ +export { default } from './asset-list-funding-modals'; diff --git a/ui/components/app/assets/asset-list/asset-list.tsx b/ui/components/app/assets/asset-list/asset-list.tsx index 3fd5bbbd4369..18d42334ab48 100644 --- a/ui/components/app/assets/asset-list/asset-list.tsx +++ b/ui/components/app/assets/asset-list/asset-list.tsx @@ -1,53 +1,18 @@ -import React, { useContext, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; - -import { NetworkConfiguration } from '@metamask/network-controller'; +import React, { useContext } from 'react'; +import { useSelector } from 'react-redux'; import TokenList from '../token-list'; -import { PRIMARY } from '../../../../helpers/constants/common'; -import { useUserPreferencedCurrency } from '../../../../hooks/useUserPreferencedCurrency'; -import { - getAllDetectedTokensForSelectedAddress, - getDetectedTokensInCurrentNetwork, - getIsTokenNetworkFilterEqualCurrentNetwork, - getSelectedAccount, - getSelectedAddress, - getUseTokenDetection, -} from '../../../../selectors'; -import { - getMultichainIsEvm, - getMultichainSelectedAccountCachedBalance, - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - getMultichainIsBitcoin, - getMultichainSelectedAccountCachedBalanceIsZero, - ///: END:ONLY_INCLUDE_IF -} from '../../../../selectors/multichain'; -import { useCurrencyDisplay } from '../../../../hooks/useCurrencyDisplay'; +import { getMultichainIsEvm } from '../../../../selectors/multichain'; import { MetaMetricsContext } from '../../../../contexts/metametrics'; import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../../shared/constants/metametrics'; import DetectedToken from '../../detected-token/detected-token'; -import { ReceiveModal } from '../../../multichain'; -import { useI18nContext } from '../../../../hooks/useI18nContext'; -import { FundingMethodModal } from '../../../multichain/funding-method-modal/funding-method-modal'; -///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) -import { - RAMPS_CARD_VARIANT_TYPES, - RampsCard, -} from '../../../multichain/ramps-card/ramps-card'; -import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; -///: END:ONLY_INCLUDE_IF -import { - getCurrentChainId, - getNetworkConfigurationsByChainId, - getSelectedNetworkClientId, -} from '../../../../../shared/modules/selectors/networks'; -import { addImportedTokens } from '../../../../store/actions'; -import { Token } from '../token-list/token-list'; -import { importAllDetectedTokens } from '../util/importAllDetectedTokens'; +import useAssetListTokenDetection from '../hooks/useAssetListTokenDetection'; +import usePrimaryCurrencyProperties from '../hooks/usePrimaryCurrencyProperties'; import AssetListControlBar from './asset-list-control-bar'; import NativeToken from './native-token'; +import AssetListFundingModals from './asset-list-funding-modals'; export type TokenWithBalance = { address: string; @@ -65,91 +30,17 @@ export type AssetListProps = { }; const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { - const dispatch = useDispatch(); - const [showDetectedTokens, setShowDetectedTokens] = useState(false); - const selectedAccount = useSelector(getSelectedAccount); - const t = useI18nContext(); + const { showDetectedTokens, setShowDetectedTokens } = + useAssetListTokenDetection(); const trackEvent = useContext(MetaMetricsContext); - const balance = useSelector(getMultichainSelectedAccountCachedBalance); - const { - currency: primaryCurrency, - numberOfDecimals: primaryNumberOfDecimals, - } = useUserPreferencedCurrency(PRIMARY, { - ethNumberOfDecimals: 4, - shouldCheckShowNativeToken: true, - }); - - const [, primaryCurrencyProperties] = useCurrencyDisplay(balance, { - numberOfDecimals: primaryNumberOfDecimals, - currency: primaryCurrency, - }); - - const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork) || []; - - const allNetworks: Record<`0x${string}`, NetworkConfiguration> = useSelector( - getNetworkConfigurationsByChainId, - ); - const networkClientId = useSelector(getSelectedNetworkClientId); - const selectedAddress = useSelector(getSelectedAddress); - const useTokenDetection = useSelector(getUseTokenDetection); - const currentChainId = useSelector(getCurrentChainId); - const isOnCurrentNetwork = useSelector( - getIsTokenNetworkFilterEqualCurrentNetwork, - ); - - const [showFundingMethodModal, setShowFundingMethodModal] = useState(false); - const [showReceiveModal, setShowReceiveModal] = useState(false); - - const onClickReceive = () => { - setShowFundingMethodModal(false); - setShowReceiveModal(true); - }; - - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - const balanceIsZero = useSelector( - getMultichainSelectedAccountCachedBalanceIsZero, - ); - const isBuyableChain = useSelector(getIsNativeTokenBuyable); - const shouldShowBuy = isBuyableChain && balanceIsZero; - const isBtc = useSelector(getMultichainIsBitcoin); - ///: END:ONLY_INCLUDE_IF + const { primaryCurrencyProperties } = usePrimaryCurrencyProperties(); const isEvm = useSelector(getMultichainIsEvm); // NOTE: Since we can parametrize it now, we keep the original behavior // for EVM assets const shouldShowTokensLinks = showTokensLinks ?? isEvm; - const detectedTokensMultichain: { - [key: string]: Token[]; - } = useSelector(getAllDetectedTokensForSelectedAddress); - - const multichainDetectedTokensLength = Object.values( - detectedTokensMultichain || {}, - ).reduce((acc, tokens) => acc + tokens.length, 0); - - // Add detected tokens to sate - useEffect(() => { - importAllDetectedTokens( - useTokenDetection, - isOnCurrentNetwork, - detectedTokensMultichain, - allNetworks, - networkClientId, - addImportedTokens, - currentChainId, - trackEvent, - detectedTokens, - dispatch, - ); - }, [ - isOnCurrentNetwork, - selectedAddress, - networkClientId, - detectedTokens.length, - multichainDetectedTokensLength, - ]); - return ( <> @@ -169,39 +60,10 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { }); }} /> - { - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - shouldShowBuy ? ( - setShowFundingMethodModal(true) - } - /> - ) : null - ///: END:ONLY_INCLUDE_IF - } {showDetectedTokens && ( )} - {showReceiveModal && selectedAccount?.address && ( - setShowReceiveModal(false)} - /> - )} - {showFundingMethodModal && ( - setShowFundingMethodModal(false)} - title={t('fundingMethod')} - onClickReceive={onClickReceive} - /> - )} + ); }; diff --git a/ui/components/app/assets/hooks/useAssetListTokenDetection.tsx b/ui/components/app/assets/hooks/useAssetListTokenDetection.tsx new file mode 100644 index 000000000000..aa205aa22f19 --- /dev/null +++ b/ui/components/app/assets/hooks/useAssetListTokenDetection.tsx @@ -0,0 +1,70 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { Token } from '../token-list/token-list'; +import { + getAllDetectedTokensForSelectedAddress, + getDetectedTokensInCurrentNetwork, + getIsTokenNetworkFilterEqualCurrentNetwork, + getSelectedAddress, + getUseTokenDetection, +} from '../../../../selectors'; +import { useContext, useEffect, useState } from 'react'; +import { importAllDetectedTokens } from '../util/importAllDetectedTokens'; +import { + getCurrentChainId, + getNetworkConfigurationsByChainId, + getSelectedNetworkClientId, +} from '../../../../../shared/modules/selectors/networks'; +import { NetworkConfiguration } from '@metamask/network-controller'; +import { addImportedTokens } from '../../../../store/actions'; +import { MetaMetricsContext } from '../../../../contexts/metametrics'; + +const useAssetListTokenDetection = () => { + const trackEvent = useContext(MetaMetricsContext); + const dispatch = useDispatch(); + const detectedTokensMultichain: { + [key: string]: Token[]; + } = useSelector(getAllDetectedTokensForSelectedAddress); + const networkClientId = useSelector(getSelectedNetworkClientId); + const selectedAddress = useSelector(getSelectedAddress); + const useTokenDetection = useSelector(getUseTokenDetection); + const currentChainId = useSelector(getCurrentChainId); + const isOnCurrentNetwork = useSelector( + getIsTokenNetworkFilterEqualCurrentNetwork, + ); + const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork) || []; + const allNetworks: Record<`0x${string}`, NetworkConfiguration> = useSelector( + getNetworkConfigurationsByChainId, + ); + + const multichainDetectedTokensLength = Object.values( + detectedTokensMultichain || {}, + ).reduce((acc, tokens) => acc + tokens.length, 0); + + const [showDetectedTokens, setShowDetectedTokens] = useState(false); + + // Add detected tokens to sate + useEffect(() => { + importAllDetectedTokens( + useTokenDetection, + isOnCurrentNetwork, + detectedTokensMultichain, + allNetworks, + networkClientId, + addImportedTokens, + currentChainId, + trackEvent, + detectedTokens, + dispatch, + ); + }, [ + isOnCurrentNetwork, + selectedAddress, + networkClientId, + detectedTokens.length, + multichainDetectedTokensLength, + ]); + + return { showDetectedTokens, setShowDetectedTokens }; +}; + +export default useAssetListTokenDetection; diff --git a/ui/components/app/assets/hooks/useFundingModals.tsx b/ui/components/app/assets/hooks/useFundingModals.tsx new file mode 100644 index 000000000000..8f29993afdfa --- /dev/null +++ b/ui/components/app/assets/hooks/useFundingModals.tsx @@ -0,0 +1,13 @@ +import { useState } from 'react'; + +const useFundingModals = () => { + const [showFundingMethodModal, setShowFundingMethodModal] = useState(false); + const [showReceiveModal, setShowReceiveModal] = useState(false); + + const onClickReceive = () => { + setShowFundingMethodModal(false); + setShowReceiveModal(true); + }; +}; + +export default useFundingModals; diff --git a/ui/components/app/assets/hooks/usePrimaryCurrencyProperties.tsx b/ui/components/app/assets/hooks/usePrimaryCurrencyProperties.tsx new file mode 100644 index 000000000000..a3d1bf0e6936 --- /dev/null +++ b/ui/components/app/assets/hooks/usePrimaryCurrencyProperties.tsx @@ -0,0 +1,27 @@ +import { useUserPreferencedCurrency } from '../../../../hooks/useUserPreferencedCurrency'; +import { useCurrencyDisplay } from '../../../../hooks/useCurrencyDisplay'; +import { useSelector } from 'react-redux'; +import { getMultichainSelectedAccountCachedBalance } from '../../../../selectors/multichain'; + +const usePrimaryCurrencyProperties = () => { + const balance = useSelector(getMultichainSelectedAccountCachedBalance); + + const { + currency: primaryCurrency, + numberOfDecimals: primaryNumberOfDecimals, + } = useUserPreferencedCurrency('PRIMARY', { + ethNumberOfDecimals: 4, + shouldCheckShowNativeToken: true, + }); + + const [, primaryCurrencyProperties] = useCurrencyDisplay(balance, { + numberOfDecimals: primaryNumberOfDecimals, + currency: primaryCurrency, + }); + + return { + primaryCurrencyProperties, + }; +}; + +export default usePrimaryCurrencyProperties; From c70d298e4642ffeea7bb528fc2094b473af648c0 Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Thu, 6 Feb 2025 10:14:07 -0800 Subject: [PATCH 09/18] Lint and cleanup importAllDetectedTokens method --- .../hooks/useAssetListTokenDetection.tsx | 44 +++++++++++-- .../app/assets/token-list/token-list.tsx | 18 +++--- .../assets/util/importAllDetectedTokens.ts | 63 +++--------------- ui/pages/asset/util.ts | 64 ------------------- ui/selectors/selectors.js | 1 - 5 files changed, 58 insertions(+), 132 deletions(-) diff --git a/ui/components/app/assets/hooks/useAssetListTokenDetection.tsx b/ui/components/app/assets/hooks/useAssetListTokenDetection.tsx index aa205aa22f19..a53e3c05a514 100644 --- a/ui/components/app/assets/hooks/useAssetListTokenDetection.tsx +++ b/ui/components/app/assets/hooks/useAssetListTokenDetection.tsx @@ -15,8 +15,16 @@ import { getSelectedNetworkClientId, } from '../../../../../shared/modules/selectors/networks'; import { NetworkConfiguration } from '@metamask/network-controller'; -import { addImportedTokens } from '../../../../store/actions'; import { MetaMetricsContext } from '../../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, + MetaMetricsTokenEventSource, +} from '../../../../../shared/constants/metametrics'; +import { + AssetType, + TokenStandard, +} from '../../../../../shared/constants/transaction'; const useAssetListTokenDetection = () => { const trackEvent = useContext(MetaMetricsContext); @@ -42,19 +50,45 @@ const useAssetListTokenDetection = () => { const [showDetectedTokens, setShowDetectedTokens] = useState(false); + const addImportedTokens = async ( + tokens: Token[], + networkClientId: string, + ) => { + await dispatch(addImportedTokens(tokens as Token[], networkClientId)); + }; + + const trackTokenAddedEvent = (importedToken: Token, chainId: string) => { + trackEvent({ + event: MetaMetricsEventName.TokenAdded, + category: MetaMetricsEventCategory.Wallet, + sensitiveProperties: { + token_symbol: importedToken.symbol, + token_contract_address: importedToken.address, + token_decimal_precision: importedToken.decimals, + source: MetaMetricsTokenEventSource.Detected, + token_standard: TokenStandard.ERC20, + asset_type: AssetType.token, + token_added_type: 'detected', + chain_id: chainId, + }, + }); + }; + // Add detected tokens to sate useEffect(() => { + if (!useTokenDetection) { + return; + } + importAllDetectedTokens( - useTokenDetection, isOnCurrentNetwork, detectedTokensMultichain, allNetworks, networkClientId, - addImportedTokens, currentChainId, - trackEvent, detectedTokens, - dispatch, + addImportedTokens, + trackTokenAddedEvent, ); }, [ isOnCurrentNetwork, diff --git a/ui/components/app/assets/token-list/token-list.tsx b/ui/components/app/assets/token-list/token-list.tsx index 429dd48326e4..a0f13fd28b20 100644 --- a/ui/components/app/assets/token-list/token-list.tsx +++ b/ui/components/app/assets/token-list/token-list.tsx @@ -63,16 +63,16 @@ export default function TokenList({ return (
- {sortedFilteredTokens.map((tokenData) => ( + {sortedFilteredTokens.map((token) => ( diff --git a/ui/components/app/assets/util/importAllDetectedTokens.ts b/ui/components/app/assets/util/importAllDetectedTokens.ts index 2ee69587b34c..d80bfd6756e2 100644 --- a/ui/components/app/assets/util/importAllDetectedTokens.ts +++ b/ui/components/app/assets/util/importAllDetectedTokens.ts @@ -1,40 +1,21 @@ -import { Hex } from '@metamask/utils'; -import { - MetaMetricsEventCategory, - MetaMetricsEventName, - MetaMetricsTokenEventSource, -} from '../../../../../shared/constants/metametrics'; -import { - AssetType, - TokenStandard, -} from '../../../../../shared/constants/transaction'; -import { Token } from '../token-list/token-list'; import { NetworkClientId, NetworkConfiguration, } from '@metamask/network-controller'; +import { Token } from '../token-list/token-list'; export const importAllDetectedTokens = async ( - useTokenDetection: boolean, isOnCurrentNetwork: boolean, detectedTokensMultichain: { [key: string]: Token[]; }, allNetworks: Record, networkClientId: NetworkClientId, - addImportedTokens: ( - tokensToImport: Token[], - networkClientId?: NetworkClientId, - ) => void, currentChainId: string, - trackEvent: any, - detectedTokens: any, - dispatch: any, + detectedTokens: Token[], + addImportedTokens: (tokens: Token[], networkClientId: string) => void, + trackTokenAddedEvent: (importedToken: Token, chainId: string) => void, ) => { - // If autodetect tokens toggle is OFF, return - if (!useTokenDetection) { - return; - } // TODO add event for MetaMetricsEventName.TokenAdded if (process.env.PORTFOLIO_VIEW && !isOnCurrentNetwork) { @@ -45,46 +26,22 @@ export const importAllDetectedTokens = async ( const { networkClientId: networkInstanceId } = chainConfig.rpcEndpoints[defaultRpcEndpointIndex]; - await dispatch(addImportedTokens(tokens as Token[], networkInstanceId)); + await addImportedTokens(tokens as Token[], networkInstanceId); + tokens.forEach((importedToken) => { // when multichain is fully integrated, we should change these event signatures for analytics - trackEvent({ - event: MetaMetricsEventName.TokenAdded, - category: MetaMetricsEventCategory.Wallet, - sensitiveProperties: { - token_symbol: importedToken.symbol, - token_contract_address: importedToken.address, - token_decimal_precision: importedToken.decimals, - source: MetaMetricsTokenEventSource.Detected, - token_standard: TokenStandard.ERC20, - asset_type: AssetType.token, - token_added_type: 'detected', - chain_id: chainConfig.chainId, - }, - }); + trackTokenAddedEvent(importedToken, chainConfig.chainId); }); }, ); await Promise.all(importPromises); } else if (detectedTokens.length > 0) { - await dispatch(addImportedTokens(detectedTokens, networkClientId)); + await addImportedTokens(detectedTokens, networkClientId); + detectedTokens.forEach((importedToken: Token) => { // when multichain is fully integrated, we should change these event signatures for analytics - trackEvent({ - event: MetaMetricsEventName.TokenAdded, - category: MetaMetricsEventCategory.Wallet, - sensitiveProperties: { - token_symbol: importedToken.symbol, - token_contract_address: importedToken.address, - token_decimal_precision: importedToken.decimals, - source: MetaMetricsTokenEventSource.Detected, - token_standard: TokenStandard.ERC20, - asset_type: AssetType.token, - token_added_type: 'detected', - chain_id: currentChainId, - }, - }); + trackTokenAddedEvent(importedToken, currentChainId); }); } }; diff --git a/ui/pages/asset/util.ts b/ui/pages/asset/util.ts index 0d413fc6be8d..2f4a41df6cc9 100644 --- a/ui/pages/asset/util.ts +++ b/ui/pages/asset/util.ts @@ -92,67 +92,3 @@ export const findAssetByAddress = ( token.address && token.address.toLowerCase() === address.toLowerCase(), ); }; - -type ParsedAssetId = { - namespace: string; // Namespace (e.g., eip155, solana, bip122) - chainId: string; // Full chain ID (namespace + blockchain ID) - assetNamespace: string; // Asset namespace (e.g., slip44, erc20, token, ordinal) - assetReference: string; // Asset reference (on-chain address, token identifier, etc.) -}; - -const parseAssetId = (assetId: string): ParsedAssetId => { - // Split the assetId into chain_id and asset details - const [chainId, assetDetails] = assetId.split('/'); - - if (!chainId || !assetDetails) { - throw new Error( - 'Invalid assetId format. Must include both chainId and asset details.', - ); - } - - // Split asset details into namespace and reference - const [assetNamespace, assetReference] = assetDetails.split(':'); - - if (!assetNamespace || !assetReference) { - throw new Error( - 'Invalid asset details format. Must include both assetNamespace and assetReference.', - ); - } - - // Validate the chainId format (namespace:blockchainId) - const [namespace, blockchainId] = chainId.split(':'); - if (!namespace || !blockchainId) { - throw new Error( - 'Invalid chainId format. Must include both namespace and blockchain ID.', - ); - } - - // Validate assetNamespace (must match [-a-z0-9]{3,8}) - // https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md#syntax - const assetNamespaceRegex = /^[-a-z0-9]{3,8}$/u; - if (!assetNamespaceRegex.test(assetNamespace)) { - throw new Error( - `Invalid assetNamespace format: "${assetNamespace}". Must be 3-8 characters, containing only lowercase letters, numbers, or dashes.`, - ); - } - - // Validate assetReference (must match [-.%a-zA-Z0-9]{1,128}) - // https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md#syntax - const assetReferenceRegex = /^[-.%a-zA-Z0-9]{1,128}$/u; - if (!assetReferenceRegex.test(assetReference)) { - throw new Error( - `Invalid assetReference format: "${assetReference}". Must be 1-128 characters, containing only alphanumerics, dashes, dots, or percent signs.`, - ); - } - - // Ensure assetReference is URL-decoded if necessary - // https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md#syntax - const decodedAssetReference = decodeURIComponent(assetReference); - - return { - namespace, - chainId, - assetNamespace, - assetReference: decodedAssetReference, - }; -}; diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 1a5014b1c184..30725e645aab 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -568,7 +568,6 @@ export function getCrossChainMetaMaskCachedBalances(state) { }, {}); } -export function getMultichainTokenBalances(state) {} /** * Based on the current account address, return the balance for the native token of all chain networks on that account * From 65eb51732158f10d1359fa0c641b4fa538e72b38 Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Thu, 6 Feb 2025 10:42:08 -0800 Subject: [PATCH 10/18] fix: Lint file imports --- .../asset-list-funding-modals.tsx | 2 +- .../app/assets/hooks/useAssetListTokenDetection.tsx | 8 ++++---- ui/components/app/assets/hooks/useFundingModals.tsx | 13 ------------- .../assets/hooks/usePrimaryCurrencyProperties.tsx | 2 +- .../app/assets/hooks/useSortedFilteredTokens.tsx | 4 ++-- 5 files changed, 8 insertions(+), 21 deletions(-) delete mode 100644 ui/components/app/assets/hooks/useFundingModals.tsx diff --git a/ui/components/app/assets/asset-list/asset-list-funding-modals/asset-list-funding-modals.tsx b/ui/components/app/assets/asset-list/asset-list-funding-modals/asset-list-funding-modals.tsx index a92c225113e4..317949d38db8 100644 --- a/ui/components/app/assets/asset-list/asset-list-funding-modals/asset-list-funding-modals.tsx +++ b/ui/components/app/assets/asset-list/asset-list-funding-modals/asset-list-funding-modals.tsx @@ -1,8 +1,8 @@ import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; import { ReceiveModal } from '../../../../multichain'; import { FundingMethodModal } from '../../../../multichain/funding-method-modal/funding-method-modal'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; -import { useSelector } from 'react-redux'; import { getSelectedAccount } from '../../../../../selectors'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { diff --git a/ui/components/app/assets/hooks/useAssetListTokenDetection.tsx b/ui/components/app/assets/hooks/useAssetListTokenDetection.tsx index a53e3c05a514..a9104589443f 100644 --- a/ui/components/app/assets/hooks/useAssetListTokenDetection.tsx +++ b/ui/components/app/assets/hooks/useAssetListTokenDetection.tsx @@ -1,4 +1,6 @@ +import { useContext, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { NetworkConfiguration } from '@metamask/network-controller'; import { Token } from '../token-list/token-list'; import { getAllDetectedTokensForSelectedAddress, @@ -7,14 +9,12 @@ import { getSelectedAddress, getUseTokenDetection, } from '../../../../selectors'; -import { useContext, useEffect, useState } from 'react'; import { importAllDetectedTokens } from '../util/importAllDetectedTokens'; import { getCurrentChainId, getNetworkConfigurationsByChainId, getSelectedNetworkClientId, } from '../../../../../shared/modules/selectors/networks'; -import { NetworkConfiguration } from '@metamask/network-controller'; import { MetaMetricsContext } from '../../../../contexts/metametrics'; import { MetaMetricsEventCategory, @@ -52,9 +52,9 @@ const useAssetListTokenDetection = () => { const addImportedTokens = async ( tokens: Token[], - networkClientId: string, + networkClientIdProp: string, ) => { - await dispatch(addImportedTokens(tokens as Token[], networkClientId)); + await dispatch(addImportedTokens(tokens as Token[], networkClientIdProp)); }; const trackTokenAddedEvent = (importedToken: Token, chainId: string) => { diff --git a/ui/components/app/assets/hooks/useFundingModals.tsx b/ui/components/app/assets/hooks/useFundingModals.tsx deleted file mode 100644 index 8f29993afdfa..000000000000 --- a/ui/components/app/assets/hooks/useFundingModals.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { useState } from 'react'; - -const useFundingModals = () => { - const [showFundingMethodModal, setShowFundingMethodModal] = useState(false); - const [showReceiveModal, setShowReceiveModal] = useState(false); - - const onClickReceive = () => { - setShowFundingMethodModal(false); - setShowReceiveModal(true); - }; -}; - -export default useFundingModals; diff --git a/ui/components/app/assets/hooks/usePrimaryCurrencyProperties.tsx b/ui/components/app/assets/hooks/usePrimaryCurrencyProperties.tsx index a3d1bf0e6936..7f81c7568fe0 100644 --- a/ui/components/app/assets/hooks/usePrimaryCurrencyProperties.tsx +++ b/ui/components/app/assets/hooks/usePrimaryCurrencyProperties.tsx @@ -1,6 +1,6 @@ +import { useSelector } from 'react-redux'; import { useUserPreferencedCurrency } from '../../../../hooks/useUserPreferencedCurrency'; import { useCurrencyDisplay } from '../../../../hooks/useCurrencyDisplay'; -import { useSelector } from 'react-redux'; import { getMultichainSelectedAccountCachedBalance } from '../../../../selectors/multichain'; const usePrimaryCurrencyProperties = () => { diff --git a/ui/components/app/assets/hooks/useSortedFilteredTokens.tsx b/ui/components/app/assets/hooks/useSortedFilteredTokens.tsx index 42e66addac94..f8c57a263e57 100644 --- a/ui/components/app/assets/hooks/useSortedFilteredTokens.tsx +++ b/ui/components/app/assets/hooks/useSortedFilteredTokens.tsx @@ -1,9 +1,8 @@ import { useMemo } from 'react'; +import { shallowEqual, useSelector } from 'react-redux'; import { sortAssets } from '../util/sort'; import { filterAssets } from '../util/filter'; import { TokenWithFiatAmount } from '../token-list/token-list'; -import useNetworkFilter from './useNetworkFilter'; -import { shallowEqual, useSelector } from 'react-redux'; import { getCurrentNetwork, getNewTokensImported, @@ -13,6 +12,7 @@ import { getTokenExchangeRates, } from '../../../../selectors'; import { getConversionRate } from '../../../../ducks/metamask/metamask'; +import useNetworkFilter from './useNetworkFilter'; const useSortedFilteredTokens = () => { const currentNetwork = useSelector(getCurrentNetwork); From fb12981d7e1ae5a7de0a0298d7eab98656bf8276 Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Thu, 6 Feb 2025 11:00:18 -0800 Subject: [PATCH 11/18] fix: unit test --- ui/selectors/selectors.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 30725e645aab..2b80c9639f0b 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -90,7 +90,6 @@ import { isAddressLedger, getIsUnlocked, getCompletedOnboarding, - getTokenBalances, } from '../ducks/metamask/metamask'; import { getLedgerWebHidConnectedStatus, @@ -2986,7 +2985,7 @@ export function getKeyringSnapAccounts(state) { export const getTokenBalancesEvm = createDeepEqualSelector( getSelectedAccountTokensAcrossChains, // TODO: useFilteredAccountTokens, we need to filter Testnets getSelectedAccountNativeTokenCachedBalanceByChainId, - getTokenBalances, + (state) => state.metamask.tokenBalances, getMarketData, getCurrencyRates, getPreferences, From b2981100a68ef46095e27aef9be083a0a827b8ce Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Thu, 6 Feb 2025 11:22:50 -0800 Subject: [PATCH 12/18] fix: Update circular deps --- development/circular-deps.jsonc | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/development/circular-deps.jsonc b/development/circular-deps.jsonc index 7ab42272d313..a4e209cae2a3 100644 --- a/development/circular-deps.jsonc +++ b/development/circular-deps.jsonc @@ -57,12 +57,8 @@ "ui/components/app/assets/asset-list/native-token/use-native-token-balance.ts" ], [ - "ui/components/app/assets/token-list/token-list.tsx", - "ui/components/app/assets/util/calculateTokenBalance.ts" - ], - [ - "ui/components/app/assets/token-list/token-list.tsx", - "ui/components/app/assets/util/calculateTokenFiatAmount.ts" + "ui/components/app/assets/hooks/useSortedFilteredTokens.tsx", + "ui/components/app/assets/token-list/token-list.tsx" ], [ "ui/components/app/name/name-details/name-details.tsx", From 2c19b46029969f8cdd3d92c52edc357a0a334652 Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Thu, 6 Feb 2025 12:33:27 -0800 Subject: [PATCH 13/18] chore: Reorganize types --- .../import-control/import-control.tsx | 1 - .../hooks/useAssetListTokenDetection.tsx | 2 +- .../assets/hooks/useSortedFilteredTokens.tsx | 2 +- .../app/assets/token-list/token-list.tsx | 22 ------------------ ui/components/app/assets/types.ts | 23 +++++++++++++++++++ .../app/assets/util/calculateTokenBalance.ts | 2 +- .../assets/util/calculateTokenFiatAmount.ts | 2 +- .../assets/util/importAllDetectedTokens.ts | 2 +- .../asset-picker-modal/types.ts | 2 +- ui/hooks/bridge/useTokensWithFiltering.ts | 2 +- ui/hooks/useMultichainBalances.ts | 5 +--- ui/pages/asset/asset.tsx | 2 +- 12 files changed, 32 insertions(+), 35 deletions(-) create mode 100644 ui/components/app/assets/types.ts diff --git a/ui/components/app/assets/asset-list/import-control/import-control.tsx b/ui/components/app/assets/asset-list/import-control/import-control.tsx index 6ac4a6b1fce1..1b17bb6414c5 100644 --- a/ui/components/app/assets/asset-list/import-control/import-control.tsx +++ b/ui/components/app/assets/asset-list/import-control/import-control.tsx @@ -9,7 +9,6 @@ import { BackgroundColor, TextColor, } from '../../../../../helpers/constants/design-system'; - import { getMultichainIsEvm } from '../../../../../selectors/multichain'; type AssetListControlBarProps = { diff --git a/ui/components/app/assets/hooks/useAssetListTokenDetection.tsx b/ui/components/app/assets/hooks/useAssetListTokenDetection.tsx index a9104589443f..7d860e88df1f 100644 --- a/ui/components/app/assets/hooks/useAssetListTokenDetection.tsx +++ b/ui/components/app/assets/hooks/useAssetListTokenDetection.tsx @@ -1,7 +1,7 @@ import { useContext, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { NetworkConfiguration } from '@metamask/network-controller'; -import { Token } from '../token-list/token-list'; +import { Token } from '../types'; import { getAllDetectedTokensForSelectedAddress, getDetectedTokensInCurrentNetwork, diff --git a/ui/components/app/assets/hooks/useSortedFilteredTokens.tsx b/ui/components/app/assets/hooks/useSortedFilteredTokens.tsx index f8c57a263e57..ee7854674ed6 100644 --- a/ui/components/app/assets/hooks/useSortedFilteredTokens.tsx +++ b/ui/components/app/assets/hooks/useSortedFilteredTokens.tsx @@ -2,7 +2,6 @@ import { useMemo } from 'react'; import { shallowEqual, useSelector } from 'react-redux'; import { sortAssets } from '../util/sort'; import { filterAssets } from '../util/filter'; -import { TokenWithFiatAmount } from '../token-list/token-list'; import { getCurrentNetwork, getNewTokensImported, @@ -13,6 +12,7 @@ import { } from '../../../../selectors'; import { getConversionRate } from '../../../../ducks/metamask/metamask'; import useNetworkFilter from './useNetworkFilter'; +import { TokenWithFiatAmount } from '../types'; const useSortedFilteredTokens = () => { const currentNetwork = useSelector(getCurrentNetwork); diff --git a/ui/components/app/assets/token-list/token-list.tsx b/ui/components/app/assets/token-list/token-list.tsx index a0f13fd28b20..d84444f38d33 100644 --- a/ui/components/app/assets/token-list/token-list.tsx +++ b/ui/components/app/assets/token-list/token-list.tsx @@ -13,28 +13,6 @@ type TokenListProps = { nativeToken?: ReactNode; }; -export type Token = { - address: Hex; - aggregators: string[]; - chainId: Hex; - decimals: number; - isNative: boolean; - symbol: string; - image: string; -}; - -export type TokenWithFiatAmount = Token & { - tokenFiatAmount: number | null; - balance?: string; - string: string; // needed for backwards compatability TODO: fix this -}; - -export type AddressBalanceMapping = Record>; -export type ChainAddressMarketData = Record< - Hex, - Record> ->; - export default function TokenList({ onTokenClick, nativeToken, diff --git a/ui/components/app/assets/types.ts b/ui/components/app/assets/types.ts new file mode 100644 index 000000000000..827c1caaed84 --- /dev/null +++ b/ui/components/app/assets/types.ts @@ -0,0 +1,23 @@ +import { Hex } from '@metamask/utils'; + +export type Token = { + address: Hex; + aggregators: string[]; + chainId: Hex; + decimals: number; + isNative: boolean; + symbol: string; + image: string; +}; + +export type TokenWithFiatAmount = Token & { + tokenFiatAmount: number | null; + balance?: string; + string: string; // needed for backwards compatability TODO: fix this +}; + +export type AddressBalanceMapping = Record>; +export type ChainAddressMarketData = Record< + Hex, + Record> +>; diff --git a/ui/components/app/assets/util/calculateTokenBalance.ts b/ui/components/app/assets/util/calculateTokenBalance.ts index 3eb1fc7373e7..dd19feaf5f6d 100644 --- a/ui/components/app/assets/util/calculateTokenBalance.ts +++ b/ui/components/app/assets/util/calculateTokenBalance.ts @@ -2,7 +2,7 @@ import BN from 'bn.js'; import { Hex } from '@metamask/utils'; import { stringifyBalance } from '../../../../hooks/useTokenBalances'; import { hexToDecimal } from '../../../../../shared/modules/conversion.utils'; -import { AddressBalanceMapping } from '../token-list/token-list'; +import { AddressBalanceMapping } from '../types'; type CalculateTokenBalanceParams = { isNative: boolean; diff --git a/ui/components/app/assets/util/calculateTokenFiatAmount.ts b/ui/components/app/assets/util/calculateTokenFiatAmount.ts index 5155c7b1ca94..cc087fdd8a76 100644 --- a/ui/components/app/assets/util/calculateTokenFiatAmount.ts +++ b/ui/components/app/assets/util/calculateTokenFiatAmount.ts @@ -1,5 +1,5 @@ import { Hex } from '@metamask/utils'; -import { ChainAddressMarketData, Token } from '../token-list/token-list'; +import { ChainAddressMarketData, Token } from '../types'; export type SymbolCurrencyRateMapping = Record>; diff --git a/ui/components/app/assets/util/importAllDetectedTokens.ts b/ui/components/app/assets/util/importAllDetectedTokens.ts index d80bfd6756e2..0e47b856a9ce 100644 --- a/ui/components/app/assets/util/importAllDetectedTokens.ts +++ b/ui/components/app/assets/util/importAllDetectedTokens.ts @@ -2,7 +2,7 @@ import { NetworkClientId, NetworkConfiguration, } from '@metamask/network-controller'; -import { Token } from '../token-list/token-list'; +import { Token } from '../types'; export const importAllDetectedTokens = async ( isOnCurrentNetwork: boolean, diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/types.ts b/ui/components/multichain/asset-picker-amount/asset-picker-modal/types.ts index 00a16b72e1b8..6fb9f3f7bc39 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/types.ts +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/types.ts @@ -8,7 +8,7 @@ import { CHAIN_ID_TO_CURRENCY_SYMBOL_MAP, CHAIN_ID_TOKEN_IMAGE_MAP, } from '../../../../../shared/constants/network'; -import { TokenWithFiatAmount } from '../../../app/assets/token-list/token-list'; +import { TokenWithFiatAmount } from '../../../app/assets/types'; export type NFT = { address: string; diff --git a/ui/hooks/bridge/useTokensWithFiltering.ts b/ui/hooks/bridge/useTokensWithFiltering.ts index 118ab13555ad..eaa07ded3674 100644 --- a/ui/hooks/bridge/useTokensWithFiltering.ts +++ b/ui/hooks/bridge/useTokensWithFiltering.ts @@ -19,7 +19,7 @@ import { import { AssetType } from '../../../shared/constants/transaction'; import { isNativeAddress } from '../../pages/bridge/utils/quote'; import { CHAIN_ID_TOKEN_IMAGE_MAP } from '../../../shared/constants/network'; -import { Token } from '../../components/app/assets/token-list/token-list'; +import { Token } from '../../components/app/assets/types'; import { useMultichainBalances } from '../useMultichainBalances'; import { useAsyncResult } from '../useAsyncResult'; import { fetchTopAssetsList } from '../../pages/swaps/swaps.util'; diff --git a/ui/hooks/useMultichainBalances.ts b/ui/hooks/useMultichainBalances.ts index b953043f7851..dbb0af52775a 100644 --- a/ui/hooks/useMultichainBalances.ts +++ b/ui/hooks/useMultichainBalances.ts @@ -9,10 +9,7 @@ import { getSelectedAccountTokensAcrossChains, selectERC20TokensByChain, } from '../selectors'; -import { - ChainAddressMarketData, - Token, -} from '../components/app/assets/token-list/token-list'; +import { ChainAddressMarketData, Token } from '../components/app/assets/types'; import { calculateTokenFiatAmount } from '../components/app/assets/util/calculateTokenFiatAmount'; import { calculateTokenBalance } from '../components/app/assets/util/calculateTokenBalance'; import { diff --git a/ui/pages/asset/asset.tsx b/ui/pages/asset/asset.tsx index 19f487987110..f8efd94c8bf2 100644 --- a/ui/pages/asset/asset.tsx +++ b/ui/pages/asset/asset.tsx @@ -8,7 +8,7 @@ import NftDetails from '../../components/app/assets/nfts/nft-details/nft-details import { getSelectedAccountTokensAcrossChains } from '../../selectors'; import { getNFTsByChainId } from '../../ducks/metamask/metamask'; import { DEFAULT_ROUTE } from '../../helpers/constants/routes'; -import { Token } from '../../components/app/assets/token-list/token-list'; +import { Token } from '../../components/app/assets/types'; import TokenAsset from './components/token-asset'; import { findAssetByAddress } from './util'; import NativeAsset from './components/native-asset'; From b3755122d1452ee78d8e7c0a17316930d16e542b Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Thu, 6 Feb 2025 12:34:33 -0800 Subject: [PATCH 14/18] fix: Update circular deps --- development/circular-deps.jsonc | 4 ---- 1 file changed, 4 deletions(-) diff --git a/development/circular-deps.jsonc b/development/circular-deps.jsonc index 3a626a84520d..ac89a9532415 100644 --- a/development/circular-deps.jsonc +++ b/development/circular-deps.jsonc @@ -43,10 +43,6 @@ "ui/components/app/assets/asset-list/native-token/native-token.tsx", "ui/components/app/assets/asset-list/native-token/use-native-token-balance.ts" ], - [ - "ui/components/app/assets/hooks/useSortedFilteredTokens.tsx", - "ui/components/app/assets/token-list/token-list.tsx" - ], [ "ui/components/app/name/name-details/name-details.tsx", "ui/components/app/name/name.tsx" From fa06c7948a5ea9fcd2d6ddea75a1d8b336fe797b Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Thu, 6 Feb 2025 12:55:43 -0800 Subject: [PATCH 15/18] Lint --- ui/components/app/assets/hooks/useSortedFilteredTokens.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/components/app/assets/hooks/useSortedFilteredTokens.tsx b/ui/components/app/assets/hooks/useSortedFilteredTokens.tsx index ee7854674ed6..a6e42739be82 100644 --- a/ui/components/app/assets/hooks/useSortedFilteredTokens.tsx +++ b/ui/components/app/assets/hooks/useSortedFilteredTokens.tsx @@ -11,8 +11,8 @@ import { getTokenExchangeRates, } from '../../../../selectors'; import { getConversionRate } from '../../../../ducks/metamask/metamask'; -import useNetworkFilter from './useNetworkFilter'; import { TokenWithFiatAmount } from '../types'; +import useNetworkFilter from './useNetworkFilter'; const useSortedFilteredTokens = () => { const currentNetwork = useSelector(getCurrentNetwork); From 8dd574d664193e5a6b2311a2d739e74503219d07 Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Thu, 6 Feb 2025 13:01:02 -0800 Subject: [PATCH 16/18] fix: Update flask code fencing --- .../asset-list-funding-modals/asset-list-funding-modals.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/components/app/assets/asset-list/asset-list-funding-modals/asset-list-funding-modals.tsx b/ui/components/app/assets/asset-list/asset-list-funding-modals/asset-list-funding-modals.tsx index 317949d38db8..7041b3534443 100644 --- a/ui/components/app/assets/asset-list/asset-list-funding-modals/asset-list-funding-modals.tsx +++ b/ui/components/app/assets/asset-list/asset-list-funding-modals/asset-list-funding-modals.tsx @@ -10,9 +10,9 @@ import { getMultichainSelectedAccountCachedBalanceIsZero, } from '../../../../../selectors/multichain'; import { getIsNativeTokenBuyable } from '../../../../../ducks/ramps'; -///: END:ONLY_INCLUDE_IF import { RampsCard } from '../../../../multichain/ramps-card'; import { RAMPS_CARD_VARIANT_TYPES } from '../../../../multichain/ramps-card/ramps-card'; +///: END:ONLY_INCLUDE_IF const AssetListFundingModals = () => { const t = useI18nContext(); From 17a718aa38d84bf1a6a10c91e509a1ba8b13cf80 Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Thu, 6 Feb 2025 13:51:18 -0800 Subject: [PATCH 17/18] fix: Rename action dispatcher handler --- .../app/assets/hooks/useAssetListTokenDetection.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/components/app/assets/hooks/useAssetListTokenDetection.tsx b/ui/components/app/assets/hooks/useAssetListTokenDetection.tsx index 7d860e88df1f..6a12def1bd71 100644 --- a/ui/components/app/assets/hooks/useAssetListTokenDetection.tsx +++ b/ui/components/app/assets/hooks/useAssetListTokenDetection.tsx @@ -25,6 +25,7 @@ import { AssetType, TokenStandard, } from '../../../../../shared/constants/transaction'; +import { addImportedTokens } from '../../../../store/actions'; const useAssetListTokenDetection = () => { const trackEvent = useContext(MetaMetricsContext); @@ -50,7 +51,7 @@ const useAssetListTokenDetection = () => { const [showDetectedTokens, setShowDetectedTokens] = useState(false); - const addImportedTokens = async ( + const handleAddImportedTokens = async ( tokens: Token[], networkClientIdProp: string, ) => { @@ -87,7 +88,7 @@ const useAssetListTokenDetection = () => { networkClientId, currentChainId, detectedTokens, - addImportedTokens, + handleAddImportedTokens, trackTokenAddedEvent, ); }, [ From 1885f1e23f79dc7ae766ea0ce7dea89fd93947dc Mon Sep 17 00:00:00 2001 From: Nicholas Gambino Date: Thu, 6 Feb 2025 14:48:04 -0800 Subject: [PATCH 18/18] fix: Cleanup --- ui/components/app/assets/types.ts | 2 ++ ui/components/app/assets/util/calculateTokenFiatAmount.ts | 8 +++++--- ui/selectors/selectors.js | 7 ++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/ui/components/app/assets/types.ts b/ui/components/app/assets/types.ts index 827c1caaed84..a9bf642b3900 100644 --- a/ui/components/app/assets/types.ts +++ b/ui/components/app/assets/types.ts @@ -21,3 +21,5 @@ export type ChainAddressMarketData = Record< Hex, Record> >; + +export type SymbolCurrencyRateMapping = Record>; diff --git a/ui/components/app/assets/util/calculateTokenFiatAmount.ts b/ui/components/app/assets/util/calculateTokenFiatAmount.ts index cc087fdd8a76..5917665e82cd 100644 --- a/ui/components/app/assets/util/calculateTokenFiatAmount.ts +++ b/ui/components/app/assets/util/calculateTokenFiatAmount.ts @@ -1,7 +1,9 @@ import { Hex } from '@metamask/utils'; -import { ChainAddressMarketData, Token } from '../types'; - -export type SymbolCurrencyRateMapping = Record>; +import { + ChainAddressMarketData, + SymbolCurrencyRateMapping, + Token, +} from '../types'; type CalculateTokenFiatAmountParams = { token: Token; diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 2b80c9639f0b..fda9e5bf7aec 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -2981,9 +2981,8 @@ export function getKeyringSnapAccounts(state) { } ///: END:ONLY_INCLUDE_IF -// consolidateTokenBalances export const getTokenBalancesEvm = createDeepEqualSelector( - getSelectedAccountTokensAcrossChains, // TODO: useFilteredAccountTokens, we need to filter Testnets + getSelectedAccountTokensAcrossChains, getSelectedAccountNativeTokenCachedBalanceByChainId, (state) => state.metamask.tokenBalances, getMarketData, @@ -3007,6 +3006,7 @@ export const getTokenBalancesEvm = createDeepEqualSelector( const selectedAccountTokenBalancesAcrossChains = tokenBalances[selectedAccount.address]; + // we need to filter Testnets const isTestNetwork = TEST_CHAINS.includes(currentNetwork.chainId); const filteredAccountTokensChains = Object.fromEntries( Object.entries(selectedAccountTokensChains).filter(([chainId]) => @@ -3019,7 +3019,6 @@ export const getTokenBalancesEvm = createDeepEqualSelector( Object.entries(filteredAccountTokensChains).forEach( ([stringChainKey, tokens]) => { const chainId = stringChainKey; - // @ts-ignore tokens.forEach((token) => { const { isNative, address, decimals } = token; const balance = @@ -3028,9 +3027,7 @@ export const getTokenBalancesEvm = createDeepEqualSelector( chainId, address, decimals, - // @ts-ignore nativeBalances, - // @ts-ignore selectedAccountTokenBalancesAcrossChains, }) || '0';