From 4e0ae1bc9f3d4d3d5f2d43e4d5f9c9d437e8e646 Mon Sep 17 00:00:00 2001 From: Maxime OUAIRY Date: Wed, 29 Apr 2026 17:22:40 +0200 Subject: [PATCH] feat: asset-controller tempo adjustments before migration --- packages/assets-controller/CHANGELOG.md | 1 + .../assets-controller/src/AssetsController.ts | 8 +-- .../src/data-sources/RpcDataSource.test.ts | 60 +++++++++++++++++++ .../src/data-sources/RpcDataSource.ts | 38 +++++++----- .../evm-rpc-services/utils/assets.ts | 10 ++++ packages/assets-controllers/CHANGELOG.md | 1 + .../src/token-prices-service/codefi-v2.ts | 6 +- 7 files changed, 98 insertions(+), 26 deletions(-) create mode 100644 packages/assets-controller/src/data-sources/evm-rpc-services/utils/assets.ts diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index eae60506f8..ec7fb51fa7 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/keyring-controller` from `^25.3.0` to `^25.4.0` ([#8665](https://github.com/MetaMask/core/pull/8665)) - Bump `@metamask/accounts-controller` from `^37.2.0` to `^38.0.0` ([#8665](https://github.com/MetaMask/core/pull/8665)) - Bump `@metamask/account-tree-controller` from `^7.1.0` to `^7.2.0` ([#8665](https://github.com/MetaMask/core/pull/8665)) +- Update `RpcDataSource` to prevent native `getEthBalance` fetching for Tempo chains ([#8638](https://github.com/MetaMask/core/pull/8638)) ## [6.3.0] diff --git a/packages/assets-controller/src/AssetsController.ts b/packages/assets-controller/src/AssetsController.ts index f18fe2d56e..aaa255f7f4 100644 --- a/packages/assets-controller/src/AssetsController.ts +++ b/packages/assets-controller/src/AssetsController.ts @@ -12,7 +12,6 @@ import type { } from '@metamask/base-controller'; import type { ClientControllerStateChangeEvent } from '@metamask/client-controller'; import { clientControllerSelectors } from '@metamask/client-controller'; -import { CHAIN_IDS_WITH_NO_NATIVE_TOKEN } from '@metamask/controller-utils'; import type { TraceCallback } from '@metamask/controller-utils'; import type { ApiPlatformClient, @@ -73,6 +72,7 @@ import type { import type { AccountsApiDataSourceConfig } from './data-sources/AccountsApiDataSource'; import { AccountsApiDataSource } from './data-sources/AccountsApiDataSource'; import { BackendWebsocketDataSource } from './data-sources/BackendWebsocketDataSource'; +import { shouldSkipNativeForCaipChainId } from './data-sources/evm-rpc-services/utils/assets'; import type { PriceDataSourceConfig } from './data-sources/PriceDataSource'; import { PriceDataSource } from './data-sources/PriceDataSource'; import type { RpcDataSourceConfig } from './data-sources/RpcDataSource'; @@ -2377,11 +2377,7 @@ export class AssetsController extends BaseController< */ #shouldHideNativeToken(chainId: ChainId, metadata: AssetMetadata): boolean { // Check if it's a chain that should skip native tokens - if ( - !CHAIN_IDS_WITH_NO_NATIVE_TOKEN.includes( - chainId as (typeof CHAIN_IDS_WITH_NO_NATIVE_TOKEN)[number], - ) - ) { + if (!shouldSkipNativeForCaipChainId(chainId)) { return false; } diff --git a/packages/assets-controller/src/data-sources/RpcDataSource.test.ts b/packages/assets-controller/src/data-sources/RpcDataSource.test.ts index edd4032cc9..6686e096df 100644 --- a/packages/assets-controller/src/data-sources/RpcDataSource.test.ts +++ b/packages/assets-controller/src/data-sources/RpcDataSource.test.ts @@ -19,6 +19,7 @@ import type { BalanceFetchResult, TokenDetectionResult, } from './evm-rpc-services'; +import { shouldSkipNativeForCaipChainId } from './evm-rpc-services/utils/assets'; import type { RpcDataSourceOptions } from './RpcDataSource'; import { RpcDataSource, @@ -247,6 +248,10 @@ jest.mock('@ethersproject/providers', () => ({ })), })); +jest.mock('./evm-rpc-services/utils/assets', () => ({ + shouldSkipNativeForCaipChainId: jest.fn().mockReturnValue(false), +})); + describe('caipChainIdToHex', () => { it('returns hex unchanged when given hex string', () => { expect(caipChainIdToHex('0x1')).toBe('0x1'); @@ -469,6 +474,18 @@ describe('RpcDataSource', () => { }); }); + it('fetches balances for accounts except native for native skip chain', async () => { + jest.mocked(shouldSkipNativeForCaipChainId).mockReturnValue(true); + await withController(async ({ controller }) => { + const response = await controller.fetch(createDataRequest()); + expect(response).toBeDefined(); + expect(response.assetsBalance).toBeDefined(); + expect(response.assetsBalance?.[MOCK_ACCOUNT_ID]).not.toHaveProperty( + 'eip155:1/slip44:60', + ); + }); + }); + it('converts fetched balances to human-readable and merges metadata', async () => { const nativeAssetId = 'eip155:1/slip44:60' as Caip19AssetId; await withController(async ({ controller }) => { @@ -506,6 +523,30 @@ describe('RpcDataSource', () => { }); }); + it('skips native asset call even in the getBalance fallback when Multicall aggregate3 fails', async () => { + const { Web3Provider } = jest.requireMock('@ethersproject/providers'); + jest.mocked(shouldSkipNativeForCaipChainId).mockReturnValue(true); + + const mockCall = jest + .fn() + .mockRejectedValueOnce(new Error('aggregate3 unavailable')) + .mockResolvedValue('0x0'); + const mockGetBalance = jest + .fn() + .mockResolvedValue({ toString: () => '1000000000000000000' }); + (Web3Provider as jest.Mock).mockImplementationOnce(() => ({ + call: mockCall, + getBalance: mockGetBalance, + })); + + await withController(async ({ controller }) => { + const response = await controller.fetch(createDataRequest()); + expect(response.assetsBalance).toBeDefined(); + expect(response.assetsBalance?.[MOCK_ACCOUNT_ID]).toStrictEqual({}); + expect(mockGetBalance).not.toHaveBeenCalled(); + }); + }); + it('uses getBalance when Multicall aggregate3 fails (#getMulticallProvider getBalance)', async () => { const { Web3Provider } = jest.requireMock('@ethersproject/providers'); const mockCall = jest @@ -576,6 +617,25 @@ describe('RpcDataSource', () => { }); }); + it('initializes assetsBalance[accountId] with no native in catch when first fetch for account throws on native skip chain', async () => { + await withController(async ({ controller }) => { + jest + .spyOn(BalanceFetcher.prototype, 'fetchBalancesForAssets') + .mockRejectedValue(new Error('RPC unavailable')); + // Indicates that we want to skip native for that chain + jest.mocked(shouldSkipNativeForCaipChainId).mockReturnValue(true); + const request = createDataRequest(); + const response = await controller.fetch(request); + expect(response.errors).toBeDefined(); + expect(response.errors?.[MOCK_CHAIN_ID_CAIP]).toBe('RPC fetch failed'); + expect(response.assetsBalance).toBeDefined(); + expect(response.assetsBalance?.[MOCK_ACCOUNT_ID]).toBeDefined(); + expect(response.assetsBalance?.[MOCK_ACCOUNT_ID]).not.toHaveProperty( + 'eip155:1/slip44:60', + ); + }); + }); + it('returns undefined from #getProvider when network client has no provider', async () => { const networkState = createMockNetworkState(NetworkStatus.Available); (networkState.networkConfigurationsByChainId as Record)[ diff --git a/packages/assets-controller/src/data-sources/RpcDataSource.ts b/packages/assets-controller/src/data-sources/RpcDataSource.ts index 941e89e78d..92e73bc88e 100644 --- a/packages/assets-controller/src/data-sources/RpcDataSource.ts +++ b/packages/assets-controller/src/data-sources/RpcDataSource.ts @@ -60,6 +60,7 @@ import type { BalanceFetchResult, TokenDetectionResult, } from './evm-rpc-services/types'; +import { shouldSkipNativeForCaipChainId } from './evm-rpc-services/utils/assets'; const CONTROLLER_NAME = 'RpcDataSource'; const DEFAULT_BALANCE_INTERVAL = 30_000; // 30 seconds @@ -919,12 +920,14 @@ export class RpcDataSource extends AbstractDataSource< for (const chainId of chainsForAccount) { const hexChainId = caipChainIdToHex(chainId); - - // Build a single AssetFetchEntry[] for native + custom ERC-20s const nativeAssetId = this.#getNativeAssetForChain(chainId); - const assetsToFetch: AssetFetchEntry[] = [ - { assetId: nativeAssetId, address: ZERO_ADDRESS }, - ]; + + const shouldSkipNative = shouldSkipNativeForCaipChainId(chainId); + const assetsToFetch: AssetFetchEntry[] = []; + if (!shouldSkipNative) { + // Build a single AssetFetchEntry[] for native + custom ERC-20s + assetsToFetch.push({ assetId: nativeAssetId, address: ZERO_ADDRESS }); + } if (request.customAssets) { const existingMetadata = this.#getExistingAssetsMetadata(); @@ -1019,17 +1022,20 @@ export class RpcDataSource extends AbstractDataSource< if (!assetsBalance[accountId]) { assetsBalance[accountId] = {}; } - assetsBalance[accountId][nativeAssetId] = { amount: '0' }; - - // Even on error, include native token metadata - const chainStatus = this.#chainStatuses[chainId]; - if (chainStatus) { - assetsInfo[nativeAssetId] = { - type: 'native', - symbol: chainStatus.nativeCurrency, - name: chainStatus.nativeCurrency, - decimals: 18, - }; + + if (!shouldSkipNative) { + assetsBalance[accountId][nativeAssetId] = { amount: '0' }; + + // Even on error, include native token metadata + const chainStatus = this.#chainStatuses[chainId]; + if (chainStatus) { + assetsInfo[nativeAssetId] = { + type: 'native', + symbol: chainStatus.nativeCurrency, + name: chainStatus.nativeCurrency, + decimals: 18, + }; + } } if (!failedChains.includes(chainId)) { diff --git a/packages/assets-controller/src/data-sources/evm-rpc-services/utils/assets.ts b/packages/assets-controller/src/data-sources/evm-rpc-services/utils/assets.ts new file mode 100644 index 0000000000..62a820ad4c --- /dev/null +++ b/packages/assets-controller/src/data-sources/evm-rpc-services/utils/assets.ts @@ -0,0 +1,10 @@ +import { CHAIN_IDS_WITH_NO_NATIVE_TOKEN } from '@metamask/controller-utils'; +import { CaipChainId } from '@metamask/utils'; + +export function shouldSkipNativeForCaipChainId( + caipChainId: CaipChainId, +): boolean { + return (CHAIN_IDS_WITH_NO_NATIVE_TOKEN as readonly string[]).includes( + caipChainId, + ); +} diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 7f7ab40455..96e0133e9c 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/keyring-controller` from `^25.3.0` to `^25.4.0` ([#8665](https://github.com/MetaMask/core/pull/8665)) - Bump `@metamask/accounts-controller` from `^37.2.0` to `^38.0.0` ([#8665](https://github.com/MetaMask/core/pull/8665)) - Bump `@metamask/account-tree-controller` from `^7.1.0` to `^7.2.0` ([#8665](https://github.com/MetaMask/core/pull/8665)) +- Modify `SPOT_PRICES_SUPPORT_INFO` entries for Tempo chains (`0x1079` and `0xa5bf`) ([#8638](https://github.com/MetaMask/core/pull/8638)) ## [105.1.0] diff --git a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts index f72bee5495..9804580bfb 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -233,8 +233,6 @@ const chainIdToNativeTokenAddress: Record = { '0x64': '0xe91d153e0b41518a2ce8dd3d7944fa863463a97d', // Gnosis '0x1e': '0x542fda317318ebf1d3deaf76e0b632741a7e677d', // Rootstock Mainnet - Native symbol: RBTC '0x3dc': '0x779ded0c9e1022225f8e0630b35a9b54be713736', // Stable - Native symbol: USDT0 - '0x1079': '0x20c0000000000000000000000000000000000000', // Tempo Mainnet - Pseudo-Native symbol: pathUSD - '0xa5bf': '0x20c0000000000000000000000000000000000000', // Tempo Moderato Testnet - Pseudo-Native symbol: pathUSD }; /** @@ -289,7 +287,7 @@ export const SPOT_PRICES_SUPPORT_INFO = { '0x74c': 'eip155:1868/erc20:0x0000000000000000000000000000000000000000', // Soneium - Native symbol: ETH '0xa729': 'eip155:42793/erc20:0x0000000000000000000000000000000000000000', // Etherlink - Native symbol: XTZ (Tezos L2) '0xab5': 'eip155:2741/erc20:0x0000000000000000000000000000000000000000', // Abstract - Native symbol: ETH - '0x1079': 'eip155:4217/erc20:0x20c0000000000000000000000000000000000000', // Tempo Mainnet - Pseudo-Native symbol: pathUSD + '0x1079': 'eip155:4217/slip44:60', // Tempo Mainnet - No native asset '0x10e6': 'eip155:4326/erc20:0x0000000000000000000000000000000000000000', // MegaETH Mainnet - Native symbol: ETH '0x1388': 'eip155:5000/erc20:0xdeaddeaddeaddeaddeaddeaddeaddeaddead0000', // Mantle - Native symbol: MNT '0x2105': 'eip155:8453/slip44:60', // Base - Native symbol: ETH @@ -302,7 +300,7 @@ export const SPOT_PRICES_SUPPORT_INFO = { '0xa516': 'eip155:42262/slip44:474', // Oasis Emerald - Native symbol: ROSE '0xa867': 'eip155:43111/erc20:0x0000000000000000000000000000000000000000', // Hemi - Native symbol: ETH '0xa86a': 'eip155:43114/slip44:9005', // Avalanche C-Chain - Native symbol: AVAX - '0xa5bf': 'eip155:42431/erc20:0x20c0000000000000000000000000000000000000', // Tempo Testnet Moderato - Pseudo-Native symbol: pathUSD + '0xa5bf': 'eip155:42431/slip44:60', // Tempo Testnet Moderato - No native asset '0xe708': 'eip155:59144/slip44:60', // Linea Mainnet - Native symbol: ETH '0xed88': 'eip155:60808/erc20:0x0000000000000000000000000000000000000000', // BOB - Native symbol: ETH '0x138de': 'eip155:80094/erc20:0x0000000000000000000000000000000000000000', // Berachain - Native symbol: Bera',