diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index ab3b05a5585..5393baacbda 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Hide native tokens on Tempo networks (testnet and mainnet) in `getAssets` method ([#7882](https://github.com/MetaMask/core/pull/7882)) - Bump `@metamask/assets-controllers` from `^100.0.1` to `^100.0.2` ([#8004](https://github.com/MetaMask/core/pull/8004)) ## [2.0.2] diff --git a/packages/assets-controller/src/AssetsController.test.ts b/packages/assets-controller/src/AssetsController.test.ts index b0ffe5a3fd6..2f2e114ab10 100644 --- a/packages/assets-controller/src/AssetsController.test.ts +++ b/packages/assets-controller/src/AssetsController.test.ts @@ -569,6 +569,181 @@ describe('AssetsController', () => { expect(assets).toBeDefined(); }); }); + + it('hides native tokens on Tempo testnet (eip155:42431)', async () => { + await withController( + { + state: { + assetsInfo: { + 'eip155:42431/slip44:60': { + type: 'native', + symbol: 'ETH', + name: 'Ethereum', + decimals: 18, + }, + 'eip155:42431/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': { + type: 'erc20', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + }, + }, + assetsBalance: { + [MOCK_ACCOUNT_ID]: { + 'eip155:42431/slip44:60': { + amount: '1', + unit: 'ETH', + }, + 'eip155:42431/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': + { + amount: '100', + unit: 'USDC', + }, + }, + }, + assetsPrice: {}, + customAssets: {}, + assetPreferences: {}, + }, + }, + async ({ controller }) => { + const accounts = [createMockInternalAccount()]; + const assets = await controller.getAssets(accounts, { + chainIds: ['eip155:42431'], + }); + + // Native token should be hidden + expect( + assets[MOCK_ACCOUNT_ID]['eip155:42431/slip44:60'], + ).toBeUndefined(); + + // ERC20 token should still be visible + expect( + assets[MOCK_ACCOUNT_ID][ + 'eip155:42431/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' + ], + ).toBeDefined(); + }, + ); + }); + + it('hides native tokens on Tempo mainnet (eip155:4217)', async () => { + await withController( + { + state: { + assetsInfo: { + 'eip155:4217/slip44:60': { + type: 'native', + symbol: 'ETH', + name: 'Ethereum', + decimals: 18, + }, + }, + assetsBalance: { + [MOCK_ACCOUNT_ID]: { + 'eip155:4217/slip44:60': { + amount: '1', + unit: 'ETH', + }, + }, + }, + assetsPrice: {}, + customAssets: {}, + assetPreferences: {}, + }, + }, + async ({ controller }) => { + const accounts = [createMockInternalAccount()]; + const assets = await controller.getAssets(accounts, { + chainIds: ['eip155:4217'], + }); + + // Native token should be hidden + expect( + assets[MOCK_ACCOUNT_ID]['eip155:4217/slip44:60'], + ).toBeUndefined(); + }, + ); + }); + + it('does not hide native tokens on non-Tempo networks', async () => { + await withController( + { + state: { + assetsInfo: { + 'eip155:1/slip44:60': { + type: 'native', + symbol: 'ETH', + name: 'Ethereum', + decimals: 18, + }, + }, + assetsBalance: { + [MOCK_ACCOUNT_ID]: { + 'eip155:1/slip44:60': { + amount: '1', + unit: 'ETH', + }, + }, + }, + assetsPrice: {}, + customAssets: {}, + assetPreferences: {}, + }, + }, + async ({ controller }) => { + const accounts = [createMockInternalAccount()]; + const assets = await controller.getAssets(accounts, { + chainIds: ['eip155:1'], + }); + + // Native token should still be visible on Ethereum + expect(assets[MOCK_ACCOUNT_ID]['eip155:1/slip44:60']).toBeDefined(); + expect( + assets[MOCK_ACCOUNT_ID]['eip155:1/slip44:60'].metadata.symbol, + ).toBe('ETH'); + }, + ); + }); + + it('hides native tokens identified by metadata type', async () => { + await withController( + { + state: { + assetsInfo: { + 'eip155:42431/some:other': { + type: 'native', + symbol: 'ETH', + name: 'Ethereum', + decimals: 18, + }, + }, + assetsBalance: { + [MOCK_ACCOUNT_ID]: { + 'eip155:42431/some:other': { + amount: '1', + unit: 'ETH', + }, + }, + }, + assetsPrice: {}, + customAssets: {}, + assetPreferences: {}, + }, + }, + async ({ controller }) => { + const accounts = [createMockInternalAccount()]; + const assets = await controller.getAssets(accounts, { + chainIds: ['eip155:42431'], + }); + + // Native token should be hidden even if assetId doesn't have slip44 + expect( + assets[MOCK_ACCOUNT_ID]['eip155:42431/some:other'], + ).toBeUndefined(); + }, + ); + }); }); describe('getAssetsBalance', () => { diff --git a/packages/assets-controller/src/AssetsController.ts b/packages/assets-controller/src/AssetsController.ts index ba421627e37..403185187bc 100644 --- a/packages/assets-controller/src/AssetsController.ts +++ b/packages/assets-controller/src/AssetsController.ts @@ -1397,6 +1397,11 @@ export class AssetsController extends BaseController< const assetChainId = extractChainId(typedAssetId); + // Skip native tokens on Tempo networks + if (this.#shouldHideNativeToken(assetChainId, typedAssetId, metadata)) { + continue; + } + if (!chainIdSet.has(assetChainId)) { continue; } @@ -1438,6 +1443,41 @@ export class AssetsController extends BaseController< return result; } + /** + * Determines if a native token should be hidden on specific networks. + * + * @param chainId - The CAIP-2 chain ID (e.g., "eip155:42431"). + * @param assetId - The CAIP-19 asset ID (e.g., "eip155:42431/slip44:60"). + * @param metadata - The asset metadata. + * @returns True if the token should be hidden, false otherwise. + */ + #shouldHideNativeToken( + chainId: ChainId, + assetId: Caip19AssetId, + metadata: AssetMetadata, + ): boolean { + // Chain IDs where native tokens should be skipped (Tempo networks) + // These networks return arbitrary large numbers for native token balances via eth_getBalance + const CHAIN_IDS_TO_SKIP_NATIVE_TOKEN = [ + 'eip155:42431', // Tempo Testnet + 'eip155:4217', // Tempo Mainnet + ] as const; + + // Check if it's a chain that should skip native tokens + if ( + !CHAIN_IDS_TO_SKIP_NATIVE_TOKEN.includes( + chainId as (typeof CHAIN_IDS_TO_SKIP_NATIVE_TOKEN)[number], + ) + ) { + return false; + } + + // Check if it's a native token (either by metadata type or assetId format) + const isNative = metadata.type === 'native' || assetId.includes('/slip44:'); + + return isNative; + } + /** * Maps a token standard to its corresponding asset type. * diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 2cfcf1d4ebb..362f60d0231 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Hide native tokens on Tempo networks (testnet and mainnet) in asset selectors ([#7882](https://github.com/MetaMask/core/pull/7882)) - Blockaid token filtering in `MultichainAssetsController` now only removes tokens flagged as `Malicious` ([#8003](https://github.com/MetaMask/core/pull/8003)) - `Spam`, `Warning`, and `Benign` tokens are no longer filtered out diff --git a/packages/assets-controllers/src/AccountTrackerController.ts b/packages/assets-controllers/src/AccountTrackerController.ts index f9c6f0e7780..1da2b0866b3 100644 --- a/packages/assets-controllers/src/AccountTrackerController.ts +++ b/packages/assets-controllers/src/AccountTrackerController.ts @@ -45,6 +45,7 @@ import type { AssetsContractController, StakedBalance, } from './AssetsContractController'; +import { shouldIncludeNativeToken } from './constants'; import { AccountsApiBalanceFetcher } from './multi-chain-accounts-service/api-balance-fetcher'; import type { BalanceFetcher, @@ -855,7 +856,19 @@ export class AccountTrackerController extends StaticIntervalPollingController + >((acc, address) => { + acc[address] = { balance: '0x0' }; + return acc; + }, {}); + } // TODO: This should use multicall when enabled by the user. return await Promise.all( diff --git a/packages/assets-controllers/src/constants.ts b/packages/assets-controllers/src/constants.ts index 96340d3aac1..6cc8e94efd5 100644 --- a/packages/assets-controllers/src/constants.ts +++ b/packages/assets-controllers/src/constants.ts @@ -18,3 +18,52 @@ export const SUPPORTED_NETWORKS_ACCOUNTS_API_V4 = [ '0x8f', // 143 '0x3e7', // 999 HyperEVM ]; + +/** + * Chain IDs where native tokens should be skipped. + * These networks return arbitrary large numbers for native token balances via eth_getBalance. + * Currently includes: Tempo Testnet (eip155:42431) and Tempo Mainnet (eip155:4217). + */ +const CHAIN_IDS_TO_SKIP_NATIVE_TOKEN = [ + 'eip155:42431', // Tempo Testnet + 'eip155:4217', // Tempo Mainnet +] as const; + +/** + * Determines if native token fetching should be included for the given chain. + * Returns false for chains that return arbitrary large numbers (e.g., Tempo networks). + * + * @param chainId - Chain ID in hex format (e.g., "0xa5bf") or CAIP-2 format (e.g., "eip155:42431"). + * @returns True if native token should be included, false if it should be skipped. + */ +export function shouldIncludeNativeToken(chainId: string): boolean { + // Convert hex format to CAIP-2 for comparison + if (chainId.startsWith('0x')) { + try { + const decimal = parseInt(chainId, 16); + const caipChainId = `eip155:${decimal}`; + if ( + CHAIN_IDS_TO_SKIP_NATIVE_TOKEN.includes( + caipChainId as (typeof CHAIN_IDS_TO_SKIP_NATIVE_TOKEN)[number], + ) + ) { + return false; + } + } catch { + // If conversion fails, assume it should be included + return true; + } + return true; + } + + // Check CAIP-2 format directly + if ( + CHAIN_IDS_TO_SKIP_NATIVE_TOKEN.includes( + chainId as (typeof CHAIN_IDS_TO_SKIP_NATIVE_TOKEN)[number], + ) + ) { + return false; + } + + return true; +} diff --git a/packages/assets-controllers/src/rpc-service/rpc-balance-fetcher.ts b/packages/assets-controllers/src/rpc-service/rpc-balance-fetcher.ts index 8cfa0936a83..a46d626f760 100644 --- a/packages/assets-controllers/src/rpc-service/rpc-balance-fetcher.ts +++ b/packages/assets-controllers/src/rpc-service/rpc-balance-fetcher.ts @@ -9,6 +9,7 @@ import type { Hex } from '@metamask/utils'; import BN from 'bn.js'; import { STAKING_CONTRACT_ADDRESS_BY_CHAINID } from '../AssetsContractController'; +import { shouldIncludeNativeToken } from '../constants'; import { getTokenBalancesForMultipleAddresses } from '../multicall'; import type { TokensControllerState } from '../TokensController'; @@ -101,13 +102,16 @@ export class RpcBalanceFetcher implements BalanceFetcher { const provider = this.#getProvider(chainId); await this.#ensureFreshBlockData(chainId); + // Skip native token fetching for chains that return arbitrary large numbers + const includeNative = shouldIncludeNativeToken(chainId); + const balanceResult = await safelyExecuteWithTimeout( async () => { return await getTokenBalancesForMultipleAddresses( accountTokenGroups, chainId, provider, - true, // include native + includeNative, // Skip native for Tempo chains true, // include staked ); }, diff --git a/packages/assets-controllers/src/selectors/token-selectors.test.ts b/packages/assets-controllers/src/selectors/token-selectors.test.ts index 6b3126d8c08..ed9eb8c8153 100644 --- a/packages/assets-controllers/src/selectors/token-selectors.test.ts +++ b/packages/assets-controllers/src/selectors/token-selectors.test.ts @@ -11,7 +11,10 @@ import type { Hex } from '@metamask/utils'; import { cloneDeep } from 'lodash'; import { MOCK_TRON_TOKENS } from './__fixtures__/arrange-tron-state'; -import { selectAssetsBySelectedAccountGroup } from './token-selectors'; +import { + AssetListState, + selectAssetsBySelectedAccountGroup, +} from './token-selectors'; import type { AccountGroupMultichainAccountObject } from '../../../account-tree-controller/src/group'; import type { CurrencyRateState } from '../CurrencyRateController'; import type { MultichainAssetsControllerState } from '../MultichainAssetsController'; @@ -1111,5 +1114,131 @@ describe('token-selectors', () => { // Should have undefined fiat since there's no currency rate for 'INK' expect(inkNativeToken?.fiat).toBeUndefined(); }); + + it('hides native tokens on Tempo testnet (0xa5bf)', () => { + const tempoTestnetChainId = '0xa5bf' as Hex; // 42431 in decimal + const stateWithTempoTestnet = { + ...mockedMergedState, + networkConfigurationsByChainId: { + ...mockNetworkControllerState.networkConfigurationsByChainId, + [tempoTestnetChainId]: { + nativeCurrency: 'ETH', + }, + }, + accountsByChainId: { + ...mockAccountsTrackerControllerState.accountsByChainId, + [tempoTestnetChainId]: { + '0x2bd63233fe369b0f13eaf25292af5a9b63d2b7ab': { + balance: '0xDE0B6B3A7640000', // 1 ETH + }, + }, + }, + }; + + const result = selectAssetsBySelectedAccountGroup(stateWithTempoTestnet); + + // Native token should be hidden on Tempo testnet + const nativeToken = result[tempoTestnetChainId]?.find( + (asset) => asset.isNative, + ); + expect(nativeToken).toBeUndefined(); + }); + + it('hides native tokens on Tempo mainnet (0x1079)', () => { + const tempoMainnetChainId = '0x1079' as Hex; // 4217 in decimal + const stateWithTempoMainnet = { + ...mockedMergedState, + networkConfigurationsByChainId: { + ...mockNetworkControllerState.networkConfigurationsByChainId, + [tempoMainnetChainId]: { + nativeCurrency: 'ETH', + }, + }, + accountsByChainId: { + ...mockAccountsTrackerControllerState.accountsByChainId, + [tempoMainnetChainId]: { + '0x2bd63233fe369b0f13eaf25292af5a9b63d2b7ab': { + balance: '0xDE0B6B3A7640000', // 1 ETH + }, + }, + }, + }; + + const result = selectAssetsBySelectedAccountGroup(stateWithTempoMainnet); + + // Native token should be hidden on Tempo mainnet + const nativeToken = result[tempoMainnetChainId]?.find( + (asset) => asset.isNative, + ); + expect(nativeToken).toBeUndefined(); + }); + + it('does not hide native tokens on non-Tempo networks', () => { + const ethereumChainId = '0x1' as Hex; + const result = selectAssetsBySelectedAccountGroup(mockedMergedState); + + // Native token should still be visible on Ethereum + const nativeToken = result[ethereumChainId]?.find( + (asset) => asset.isNative, + ); + expect(nativeToken).toBeDefined(); + expect(nativeToken?.symbol).toBe('ETH'); + }); + + it('hides native multichain tokens on Tempo networks', () => { + const tempoCaipChainId = 'eip155:42431'; + const tempoNativeAssetId = `${tempoCaipChainId}/slip44:60` as const; + + const stateWithTempoMultichain: AssetListState = { + ...mockedMergedState, + accountsAssets: { + ...mockMultichainAssetsControllerState.accountsAssets, + '2d89e6a0-b4e6-45a8-a707-f10cef143b42': [ + ...mockMultichainAssetsControllerState.accountsAssets[ + '2d89e6a0-b4e6-45a8-a707-f10cef143b42' + ], + tempoNativeAssetId, + ], + }, + assetsMetadata: { + ...mockMultichainAssetsControllerState.assetsMetadata, + [tempoNativeAssetId]: { + fungible: true, + iconUrl: '', + name: 'Ethereum', + symbol: 'ETH', + units: [ + { + decimals: 18, + name: 'Ethereum', + symbol: 'ETH', + }, + ], + }, + }, + balances: { + ...mockMultichainBalancesControllerState.balances, + '2d89e6a0-b4e6-45a8-a707-f10cef143b42': { + ...mockMultichainBalancesControllerState.balances[ + '2d89e6a0-b4e6-45a8-a707-f10cef143b42' + ], + [tempoNativeAssetId]: { + amount: '1', + unit: 'ETH', + }, + }, + }, + }; + + const result = selectAssetsBySelectedAccountGroup( + stateWithTempoMultichain, + ); + + // Native token should be hidden on Tempo testnet + const nativeToken = result[tempoCaipChainId]?.find( + (asset) => asset.isNative, + ); + expect(nativeToken).toBeUndefined(); + }); }); }); diff --git a/packages/assets-controllers/src/selectors/token-selectors.ts b/packages/assets-controllers/src/selectors/token-selectors.ts index 61716fc7fdb..3835e6250f4 100644 --- a/packages/assets-controllers/src/selectors/token-selectors.ts +++ b/packages/assets-controllers/src/selectors/token-selectors.ts @@ -14,6 +14,7 @@ import { parseBalanceWithDecimals, stringifyBalanceWithDecimals, } from './stringify-balance'; +import { shouldIncludeNativeToken } from '../constants'; import type { CurrencyRateState } from '../CurrencyRateController'; import type { MultichainAssetsControllerState } from '../MultichainAssetsController'; import type { MultichainAssetsRatesControllerState } from '../MultichainAssetsRatesController'; @@ -43,6 +44,22 @@ export const TRON_RESOURCE_SYMBOLS = Object.values( export const TRON_RESOURCE_SYMBOLS_SET: ReadonlySet = new Set(TRON_RESOURCE_SYMBOLS); +/** + * Determines if a native token should be hidden. + * Returns true if the token is native and should be excluded for the given chain. + * + * @param chainId - The chain ID in hex format (e.g., "0xa5bf") or CAIP-2 format (e.g., "eip155:42431"). + * @param isNative - Whether the token is a native token. + * @returns True if the token should be hidden, false otherwise. + */ +function shouldHideNativeToken( + chainId: Hex | string, + isNative: boolean, +): boolean { + // Only hide native tokens on chains that should skip native token fetching + return isNative && !shouldIncludeNativeToken(chainId); +} + export type AssetsByAccountGroup = { [accountGroupId: AccountGroupId]: AccountGroupAssets; }; @@ -182,6 +199,11 @@ const selectAllEvmAccountNativeBalances = createAssetListSelector( const { accountGroupId, type, accountId } = account; + // Skip native tokens on Tempo networks + if (shouldHideNativeToken(chainId, true)) { + continue; + } + groupAssets[accountGroupId] ??= {}; groupAssets[accountGroupId][chainId] ??= []; const groupChainAssets = groupAssets[accountGroupId][chainId]; @@ -384,6 +406,12 @@ const selectAllMultichainAssets = createAssetListSelector( continue; } + // Skip native tokens on Tempo networks + const isNative = caipAsset.assetNamespace === 'slip44'; + if (shouldHideNativeToken(chainId, isNative)) { + continue; + } + groupAssets[accountGroupId] ??= {}; groupAssets[accountGroupId][chainId] ??= []; const groupChainAssets = groupAssets[accountGroupId][chainId];