Skip to content

Commit e27f069

Browse files
khanti42mikespositomaxime-oearafetbenmakhlouf
authored
fix: hide native tokens on Tempo networks (testnet and mainnet) (#7882)
## Explanation <!-- Thanks for your contribution! Take a moment to answer these questions so that reviewers have the information they need to properly understand your changes: * What is the current state of things and why does it need to change? * What is the solution your changes offer and how does it work? * Are there any changes whose purpose might not obvious to those unfamiliar with the domain? * If your primary goal was to update one package but you found you had to update another one along the way, why did you do so? * If you had to upgrade a dependency, why did you do so? --> **Context:** Tempo is a EVM-ish chain that doesn't have the concept of native token. This PR changes the behavior of asset-controller(s) so: - Hides native tokens on those chains end-to-end: in `AssetsController.getAssets`, in balance fetching (AccountTrackerController and RPC balance fetcher), and in asset selectors so the UI never shows Tempo native tokens. - The portfolio USD value doesn't take in account the native token on Tempo networks - Tempo testnet (`eip155:42431`) and Tempo mainnet (`eip155:4217`) return arbitrary large numbers for native token balances via `eth_getBalance`. ## References <!-- Are there any issues that this pull request is tied to? Are there other links that reviewers should consult to understand these changes better? Are there client or consumer pull requests to adopt any breaking changes? For example: * Fixes #12345 * Related to #67890 --> ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches balance-fetching and asset-selection logic, which can impact portfolio totals and token visibility across networks; changes are gated to a specific chain allowlist and backed by targeted tests. > > **Overview** > Native token handling is changed to **exclude Tempo networks end-to-end**: `AssetsController.getAssets` now filters out native assets on chain IDs listed in `CHAIN_IDS_WITH_NO_NATIVE_TOKEN`, and balance fetching in `AccountTrackerController`/`rpc-balance-fetcher` now short-circuits native balance retrieval (returning `0x0` for those addresses) to avoid bogus `eth_getBalance` results. > > Selectors are updated to omit native assets for Tempo chains, and `token-service.ts` forces `occurrenceFloor=1` for Tempo Mainnet. New unit tests cover Tempo mainnet/testnet behavior and ensure non-Tempo networks are unaffected; changelogs are updated accordingly. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e35e3ee. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Michele Esposito <michele@esposito.codes> Co-authored-by: Maxime OUAIRY <maxime.ouairy-ext@consensys.net> Co-authored-by: Arafet (CN - Hong Kong) <52028926+arafetbenmakhlouf@users.noreply.github.com>
1 parent c1ace48 commit e27f069

11 files changed

Lines changed: 407 additions & 4 deletions

File tree

packages/assets-controller/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Changed
1111

1212
- Bump `@metamask/controller-utils` from `^11.19.0` to `^11.20.0` ([#8344](https://github.com/MetaMask/core/pull/8344))
13+
- Hide native tokens on Tempo networks (testnet and mainnet) in `getAssets` method ([#7882](https://github.com/MetaMask/core/pull/7882))
1314

1415
## [3.2.1]
1516

packages/assets-controller/src/AssetsController.test.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,181 @@ describe('AssetsController', () => {
612612
expect(assets).toBeDefined();
613613
});
614614
});
615+
616+
it('hides native tokens on Tempo testnet (eip155:42431)', async () => {
617+
await withController(
618+
{
619+
state: {
620+
assetsInfo: {
621+
'eip155:42431/slip44:60': {
622+
type: 'native',
623+
symbol: 'ETH',
624+
name: 'Ethereum',
625+
decimals: 18,
626+
},
627+
'eip155:42431/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': {
628+
type: 'erc20',
629+
symbol: 'USDC',
630+
name: 'USD Coin',
631+
decimals: 6,
632+
},
633+
},
634+
assetsBalance: {
635+
[MOCK_ACCOUNT_ID]: {
636+
'eip155:42431/slip44:60': {
637+
amount: '1',
638+
unit: 'ETH',
639+
},
640+
'eip155:42431/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48':
641+
{
642+
amount: '100',
643+
unit: 'USDC',
644+
},
645+
},
646+
},
647+
assetsPrice: {},
648+
customAssets: {},
649+
assetPreferences: {},
650+
},
651+
},
652+
async ({ controller }) => {
653+
const accounts = [createMockInternalAccount()];
654+
const assets = await controller.getAssets(accounts, {
655+
chainIds: ['eip155:42431'],
656+
});
657+
658+
// Native token should be hidden
659+
expect(
660+
assets[MOCK_ACCOUNT_ID]['eip155:42431/slip44:60'],
661+
).toBeUndefined();
662+
663+
// ERC20 token should still be visible
664+
expect(
665+
assets[MOCK_ACCOUNT_ID][
666+
'eip155:42431/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
667+
],
668+
).toBeDefined();
669+
},
670+
);
671+
});
672+
673+
it('hides native tokens on Tempo mainnet (eip155:4217)', async () => {
674+
await withController(
675+
{
676+
state: {
677+
assetsInfo: {
678+
'eip155:4217/slip44:60': {
679+
type: 'native',
680+
symbol: 'ETH',
681+
name: 'Ethereum',
682+
decimals: 18,
683+
},
684+
},
685+
assetsBalance: {
686+
[MOCK_ACCOUNT_ID]: {
687+
'eip155:4217/slip44:60': {
688+
amount: '1',
689+
unit: 'ETH',
690+
},
691+
},
692+
},
693+
assetsPrice: {},
694+
customAssets: {},
695+
assetPreferences: {},
696+
},
697+
},
698+
async ({ controller }) => {
699+
const accounts = [createMockInternalAccount()];
700+
const assets = await controller.getAssets(accounts, {
701+
chainIds: ['eip155:4217'],
702+
});
703+
704+
// Native token should be hidden
705+
expect(
706+
assets[MOCK_ACCOUNT_ID]['eip155:4217/slip44:60'],
707+
).toBeUndefined();
708+
},
709+
);
710+
});
711+
712+
it('does not hide native tokens on non-Tempo networks', async () => {
713+
await withController(
714+
{
715+
state: {
716+
assetsInfo: {
717+
'eip155:1/slip44:60': {
718+
type: 'native',
719+
symbol: 'ETH',
720+
name: 'Ethereum',
721+
decimals: 18,
722+
},
723+
},
724+
assetsBalance: {
725+
[MOCK_ACCOUNT_ID]: {
726+
'eip155:1/slip44:60': {
727+
amount: '1',
728+
unit: 'ETH',
729+
},
730+
},
731+
},
732+
assetsPrice: {},
733+
customAssets: {},
734+
assetPreferences: {},
735+
},
736+
},
737+
async ({ controller }) => {
738+
const accounts = [createMockInternalAccount()];
739+
const assets = await controller.getAssets(accounts, {
740+
chainIds: ['eip155:1'],
741+
});
742+
743+
// Native token should still be visible on Ethereum
744+
expect(assets[MOCK_ACCOUNT_ID]['eip155:1/slip44:60']).toBeDefined();
745+
expect(
746+
assets[MOCK_ACCOUNT_ID]['eip155:1/slip44:60'].metadata.symbol,
747+
).toBe('ETH');
748+
},
749+
);
750+
});
751+
752+
it('hides native tokens identified by metadata type', async () => {
753+
await withController(
754+
{
755+
state: {
756+
assetsInfo: {
757+
'eip155:42431/some:other': {
758+
type: 'native',
759+
symbol: 'ETH',
760+
name: 'Ethereum',
761+
decimals: 18,
762+
},
763+
},
764+
assetsBalance: {
765+
[MOCK_ACCOUNT_ID]: {
766+
'eip155:42431/some:other': {
767+
amount: '1',
768+
unit: 'ETH',
769+
},
770+
},
771+
},
772+
assetsPrice: {},
773+
customAssets: {},
774+
assetPreferences: {},
775+
},
776+
},
777+
async ({ controller }) => {
778+
const accounts = [createMockInternalAccount()];
779+
const assets = await controller.getAssets(accounts, {
780+
chainIds: ['eip155:42431'],
781+
});
782+
783+
// Native token should be hidden even if assetId doesn't have slip44
784+
expect(
785+
assets[MOCK_ACCOUNT_ID]['eip155:42431/some:other'],
786+
).toBeUndefined();
787+
},
788+
);
789+
});
615790
});
616791

617792
describe('handleAssetsUpdate', () => {

packages/assets-controller/src/AssetsController.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
} from '@metamask/base-controller';
1212
import type { ClientControllerStateChangeEvent } from '@metamask/client-controller';
1313
import { clientControllerSelectors } from '@metamask/client-controller';
14+
import { CHAIN_IDS_WITH_NO_NATIVE_TOKEN } from '@metamask/controller-utils';
1415
import type { TraceCallback } from '@metamask/controller-utils';
1516
import type {
1617
ApiPlatformClient,
@@ -1905,6 +1906,11 @@ export class AssetsController extends BaseController<
19051906

19061907
const assetChainId = extractChainId(typedAssetId);
19071908

1909+
// Skip native tokens on Tempo networks
1910+
if (this.#shouldHideNativeToken(assetChainId, typedAssetId, metadata)) {
1911+
continue;
1912+
}
1913+
19081914
if (!chainIdSet.has(assetChainId)) {
19091915
continue;
19101916
}
@@ -1946,6 +1952,34 @@ export class AssetsController extends BaseController<
19461952
return result;
19471953
}
19481954

1955+
/**
1956+
* Determines if a native token should be hidden on specific networks.
1957+
*
1958+
* @param chainId - The CAIP-2 chain ID (e.g., "eip155:42431").
1959+
* @param assetId - The CAIP-19 asset ID (e.g., "eip155:42431/slip44:60").
1960+
* @param metadata - The asset metadata.
1961+
* @returns True if the token should be hidden, false otherwise.
1962+
*/
1963+
#shouldHideNativeToken(
1964+
chainId: ChainId,
1965+
assetId: Caip19AssetId,
1966+
metadata: AssetMetadata,
1967+
): boolean {
1968+
// Check if it's a chain that should skip native tokens
1969+
if (
1970+
!CHAIN_IDS_WITH_NO_NATIVE_TOKEN.includes(
1971+
chainId as (typeof CHAIN_IDS_WITH_NO_NATIVE_TOKEN)[number],
1972+
)
1973+
) {
1974+
return false;
1975+
}
1976+
1977+
// Check if it's a native token (either by metadata type or assetId format)
1978+
const isNative = metadata.type === 'native' || assetId.includes('/slip44:');
1979+
1980+
return isNative;
1981+
}
1982+
19491983
/**
19501984
* Maps a token standard to its corresponding asset type.
19511985
*

packages/assets-controllers/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Changed
1111

1212
- Bump `@metamask/controller-utils` from `^11.19.0` to `^11.20.0` ([#8344](https://github.com/MetaMask/core/pull/8344))
13+
- Hide native tokens on Tempo networks (testnet and mainnet) in asset selectors ([#7882](https://github.com/MetaMask/core/pull/7882))
14+
- Force `occurrenceFloor` to `1` for Tempo Mainnet in `token-service.ts` ([#7882](https://github.com/MetaMask/core/pull/7882))
1315

1416
## [103.0.0]
1517

packages/assets-controllers/src/AccountTrackerController.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1646,6 +1646,62 @@ describe('AccountTrackerController', () => {
16461646
);
16471647
});
16481648

1649+
it('should return zero-balance entries if network is Tempo Mainnet', async () => {
1650+
await withController(
1651+
{
1652+
isMultiAccountBalancesEnabled: true,
1653+
selectedAccount: ACCOUNT_1,
1654+
listAccounts: [],
1655+
networkClientById: {
1656+
'tempo-mainnet-mock-client-id':
1657+
buildCustomNetworkClientConfiguration({
1658+
chainId: '0x1079',
1659+
ticker: 'USD',
1660+
}),
1661+
},
1662+
},
1663+
async ({ controller }) => {
1664+
mockedQuery
1665+
.mockReturnValueOnce(Promise.resolve('0x10'))
1666+
.mockReturnValueOnce(Promise.resolve('0x20'));
1667+
const result = await controller.syncBalanceWithAddresses(
1668+
[ADDRESS_1, ADDRESS_2],
1669+
'tempo-mainnet-mock-client-id',
1670+
);
1671+
expect(result[ADDRESS_1].balance).toBe('0x0');
1672+
expect(result[ADDRESS_2].balance).toBe('0x0');
1673+
},
1674+
);
1675+
});
1676+
1677+
it('should return zero-balance entries if network is Tempo Testnet', async () => {
1678+
await withController(
1679+
{
1680+
isMultiAccountBalancesEnabled: true,
1681+
selectedAccount: ACCOUNT_1,
1682+
listAccounts: [],
1683+
networkClientById: {
1684+
'tempo-testnet-mock-client-id':
1685+
buildCustomNetworkClientConfiguration({
1686+
chainId: '0xa5bf',
1687+
ticker: 'USD',
1688+
}),
1689+
},
1690+
},
1691+
async ({ controller }) => {
1692+
mockedQuery
1693+
.mockReturnValueOnce(Promise.resolve('0x10'))
1694+
.mockReturnValueOnce(Promise.resolve('0x20'));
1695+
const result = await controller.syncBalanceWithAddresses(
1696+
[ADDRESS_1, ADDRESS_2],
1697+
'tempo-testnet-mock-client-id',
1698+
);
1699+
expect(result[ADDRESS_1].balance).toBe('0x0');
1700+
expect(result[ADDRESS_2].balance).toBe('0x0');
1701+
},
1702+
);
1703+
});
1704+
16491705
it('should sync staked balance with addresses', async () => {
16501706
await withController(
16511707
{

packages/assets-controllers/src/AccountTrackerController.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import type {
5050
AssetsContractController,
5151
StakedBalance,
5252
} from './AssetsContractController';
53+
import { shouldIncludeNativeToken } from './constants';
5354
import { AccountsApiBalanceFetcher } from './multi-chain-accounts-service/api-balance-fetcher';
5455
import type {
5556
BalanceFetcher,
@@ -881,7 +882,19 @@ export class AccountTrackerController extends StaticIntervalPollingController<Ac
881882
return {};
882883
}
883884

884-
const { ethQuery } = this.#getCorrectNetworkClient(networkClientId);
885+
const { ethQuery, chainId } =
886+
this.#getCorrectNetworkClient(networkClientId);
887+
888+
// Skip native token fetching for chains that return arbitrary large numbers
889+
if (!shouldIncludeNativeToken(chainId)) {
890+
// Return empty balances for chains that skip native token fetching
891+
return addresses.reduce<
892+
Record<string, { balance: string; stakedBalance?: StakedBalance }>
893+
>((acc, address) => {
894+
acc[address] = { balance: '0x0' };
895+
return acc;
896+
}, {});
897+
}
885898

886899
// TODO: This should use multicall when enabled by the user.
887900
return await Promise.all(

packages/assets-controllers/src/constants.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { CHAIN_IDS_WITH_NO_NATIVE_TOKEN } from '@metamask/controller-utils';
2+
13
export enum Source {
24
Custom = 'custom',
35
Dapp = 'dapp',
@@ -18,3 +20,42 @@ export const SUPPORTED_NETWORKS_ACCOUNTS_API_V4 = [
1820
'0x8f', // 143
1921
'0x3e7', // 999 HyperEVM
2022
];
23+
24+
/**
25+
* Determines if native token fetching should be included for the given chain.
26+
* Returns false for chains that return arbitrary large numbers (e.g., Tempo networks).
27+
*
28+
* @param chainId - Chain ID in hex format (e.g., "0xa5bf") or CAIP-2 format (e.g., "eip155:42431").
29+
* @returns True if native token should be included, false if it should be skipped.
30+
*/
31+
export function shouldIncludeNativeToken(chainId: string): boolean {
32+
// Convert hex format to CAIP-2 for comparison
33+
if (chainId.startsWith('0x')) {
34+
try {
35+
const decimal = parseInt(chainId, 16);
36+
const caipChainId = `eip155:${decimal}`;
37+
if (
38+
CHAIN_IDS_WITH_NO_NATIVE_TOKEN.includes(
39+
caipChainId as (typeof CHAIN_IDS_WITH_NO_NATIVE_TOKEN)[number],
40+
)
41+
) {
42+
return false;
43+
}
44+
} catch {
45+
// If conversion fails, assume it should be included
46+
return true;
47+
}
48+
return true;
49+
}
50+
51+
// Check CAIP-2 format directly
52+
if (
53+
CHAIN_IDS_WITH_NO_NATIVE_TOKEN.includes(
54+
chainId as (typeof CHAIN_IDS_WITH_NO_NATIVE_TOKEN)[number],
55+
)
56+
) {
57+
return false;
58+
}
59+
60+
return true;
61+
}

0 commit comments

Comments
 (0)