From 82fb2bce1443825d24e69b233c109bd971026ba1 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Wed, 22 Oct 2025 09:46:51 +0100 Subject: [PATCH 01/12] chore: fetch price api supported chainIds --- .../src/TokenRatesController.test.ts | 7 +-- .../src/TokenRatesController.ts | 46 +++++++++++++++++++ ...TokenSearchDiscoveryDataController.test.ts | 4 +- .../assets-controllers/src/assetsUtil.test.ts | 3 ++ .../abstract-token-prices-service.ts | 9 +++- .../token-prices-service/codefi-v2.test.ts | 20 +++++++- .../src/token-prices-service/codefi-v2.ts | 25 +++++++++- 7 files changed, 102 insertions(+), 12 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index 5db4713b5fd..82ee940b479 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -2352,9 +2352,6 @@ describe('TokenRatesController', () => { value: 0.002, }, }), - validateChainIdSupported(_chainId: unknown): _chainId is Hex { - return false; - }, }); await withController( { @@ -2973,8 +2970,8 @@ function buildMockTokenPricesService( async fetchTokenPrices() { return {}; }, - validateChainIdSupported(_chainId: unknown): _chainId is Hex { - return true; + async fetchSupportedChainIds() { + return []; }, validateCurrencySupported(_currency: unknown): _currency is string { return true; diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 37ae246919f..832a614401f 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -58,6 +58,8 @@ export type Token = { const DEFAULT_INTERVAL = 180000; +const DEFAULT_CACHE_REFRESH_THRESHOLD = 1000 * 60 * 60 * 24; // 24 hours + export type ContractExchangeRates = { [address: string]: number | undefined; }; @@ -123,9 +125,14 @@ export const controllerName = 'TokenRatesController'; * * Token rates controller state * @property marketData - Market data for tokens, keyed by chain ID and then token contract address. + * @property supportedChainIds - Supported chain ids. */ export type TokenRatesControllerState = { marketData: Record>; + supportedChainIds: { + timestamp: number; + data: Hex[]; + }; }; /** @@ -209,6 +216,12 @@ const tokenRatesControllerMetadata = { anonymous: false, usedInUi: true, }, + supportedChainIds: { + includeInStateLogs: false, + persist: true, + anonymous: false, + usedInUi: true, + }, }; /** @@ -220,6 +233,10 @@ export const getDefaultTokenRatesControllerState = (): TokenRatesControllerState => { return { marketData: {}, + supportedChainIds: { + timestamp: 0, + data: [], + }, }; }; @@ -488,6 +505,32 @@ export class TokenRatesController extends StaticIntervalPollingController { + const supportedChainIds = + await this.#tokenPricesService.fetchSupportedChainIds(); + this.update((state) => { + state.supportedChainIds = { + timestamp: Date.now(), + data: supportedChainIds, + }; + }); + } + /** * Updates exchange rates for all tokens. * @@ -499,6 +542,9 @@ export class TokenRatesController extends StaticIntervalPollingController>>; + /** + * Fetches the supported chain ids from the price api. + * + * @returns The supported chain ids in hexadecimal format. + */ + fetchSupportedChainIds(): Promise; + /** * Type guard for whether the API can return token prices for the given chain * ID. @@ -82,7 +89,7 @@ export type AbstractTokenPricesService< * @param chainId - The chain ID to check. * @returns True if the API supports the chain ID, false otherwise. */ - validateChainIdSupported(chainId: unknown): chainId is ChainId; + // validateChainIdSupported(chainId: unknown): chainId is ChainId; /** * Type guard for whether the API can return token prices in the given diff --git a/packages/assets-controllers/src/token-prices-service/codefi-v2.test.ts b/packages/assets-controllers/src/token-prices-service/codefi-v2.test.ts index 8f644e7e02e..32f59ef1da2 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.test.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.test.ts @@ -1271,7 +1271,23 @@ describe('CodefiTokenPricesServiceV2', () => { }); }); - describe('validateChainIdSupported', () => { + describe('fetchSupportedChainIds', () => { + it('should return the supported chain ids in hexadecimal format', async () => { + nock('https://price.api.cx.metamask.io') + .get('/v1/supportedNetworks') + .reply(200, { + fullSupport: [1], + partialSupport: { spotPricesV2: [2] }, + }); + + const supportedChainIds = + await new CodefiTokenPricesServiceV2().fetchSupportedChainIds(); + + expect(supportedChainIds).toEqual(['0x1', '0x2']); + }); + }); + + /* describe('validateChainIdSupported', () => { it.each(SUPPORTED_CHAIN_IDS)( 'returns true if the given chain ID is %s', (chainId) => { @@ -1288,7 +1304,7 @@ describe('CodefiTokenPricesServiceV2', () => { ), ).toBe(false); }); - }); + }); */ describe('validateCurrencySupported', () => { it.each(SUPPORTED_CURRENCIES)( 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 cf93d67401f..1715db863b1 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -5,6 +5,7 @@ import { DEFAULT_MAX_CONSECUTIVE_FAILURES, DEFAULT_MAX_RETRIES, handleFetch, + toHex, } from '@metamask/controller-utils'; import type { ServicePolicy } from '@metamask/controller-utils'; import type { Hex } from '@metamask/utils'; @@ -275,6 +276,11 @@ type SupportedChainId = (typeof SUPPORTED_CHAIN_IDS)[number]; */ const BASE_URL = 'https://price.api.cx.metamask.io/v2'; +/** + * All requests to V1 of the Price API start with this. + */ +const BASE_URL_V1 = 'https://price.api.cx.metamask.io/v1'; + /** * The shape of the data that the /spot-prices endpoint returns. */ @@ -529,6 +535,21 @@ export class CodefiTokenPricesServiceV2 ) as Partial>; } + /** + * Fetches the supported chain ids from the price api. + * + * @returns The supported chain ids in hexadecimal format. + */ + async fetchSupportedChainIds(): Promise { + const url = new URL(`${BASE_URL_V1}/supportedNetworks`); + const response = await handleFetch(url); + + const supportedChainIds = response.fullSupport.concat( + response.partialSupport.spotPricesV2, + ); + return supportedChainIds.map((chainId: number) => toHex(chainId)); + } + /** * Type guard for whether the API can return token prices for the given chain * ID. @@ -536,10 +557,10 @@ export class CodefiTokenPricesServiceV2 * @param chainId - The chain ID to check. * @returns True if the API supports the chain ID, false otherwise. */ - validateChainIdSupported(chainId: unknown): chainId is SupportedChainId { + /* validateChainIdSupported(chainId: unknown): chainId is SupportedChainId { const supportedChainIds: readonly string[] = SUPPORTED_CHAIN_IDS; return typeof chainId === 'string' && supportedChainIds.includes(chainId); - } + } */ /** * Type guard for whether the API can return token prices in the given From 68c478e01d2bf5be53685796ed2f834e1e2dd8fb Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Wed, 29 Oct 2025 18:54:16 +0100 Subject: [PATCH 02/12] fix: fix unit test wip --- .../src/TokenRatesController.test.ts | 82 +++++++++++-- .../src/TokenRatesController.ts | 33 +++++- packages/assets-controllers/src/assetsUtil.ts | 6 +- packages/assets-controllers/src/index.ts | 1 - .../token-prices-service/codefi-v2.test.ts | 20 ---- .../src/token-prices-service/codefi-v2.ts | 111 +----------------- .../src/token-prices-service/index.test.ts | 1 - .../src/token-prices-service/index.ts | 6 +- 8 files changed, 117 insertions(+), 143 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index 82ee940b479..275dd937f1a 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -34,7 +34,11 @@ import type { TokenPrice, TokenPricesByTokenAddress, } from './token-prices-service/abstract-token-prices-service'; -import { controllerName, TokenRatesController } from './TokenRatesController'; +import { + controllerName, + DEFAULT_CACHE_REFRESH_THRESHOLD, + TokenRatesController, +} from './TokenRatesController'; import type { AllowedActions, AllowedEvents, @@ -98,6 +102,10 @@ describe('TokenRatesController', () => { await withController(async ({ controller }) => { expect(controller.state).toStrictEqual({ marketData: {}, + supportedChainIds: { + timestamp: 0, + data: [], + }, }); }); }); @@ -113,6 +121,10 @@ describe('TokenRatesController', () => { async ({ controller }) => { expect(controller.state).toStrictEqual({ marketData: {}, + supportedChainIds: { + timestamp: 0, + data: [], + }, }); }, ); @@ -729,6 +741,10 @@ describe('TokenRatesController', () => { options: { interval: 100, state: { + supportedChainIds: { + timestamp: Date.now() + DEFAULT_CACHE_REFRESH_THRESHOLD + 1, + data: [ChainId.mainnet], + }, marketData: { [ChainId.mainnet]: { '0x02': { @@ -789,6 +805,10 @@ describe('TokenRatesController', () => { options: { interval: 100, state: { + supportedChainIds: { + timestamp: Date.now() + DEFAULT_CACHE_REFRESH_THRESHOLD + 1, + data: [ChainId.mainnet], + }, marketData: { [ChainId.mainnet]: { '0x02': { @@ -844,10 +864,16 @@ describe('TokenRatesController', () => { }); it('should update exchange rates when network state changes without adding a new network', async () => { + const tokenPricesService = buildMockTokenPricesService({ + fetchSupportedChainIds: jest + .fn() + .mockResolvedValue([ChainId.mainnet]), + }); await withController( { options: { interval: 100, + tokenPricesService, }, mockNetworkClientConfigurationsByNetworkClientId: { 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ @@ -1297,7 +1323,11 @@ describe('TokenRatesController', () => { describe('start', () => { it('should poll and update rate in the right interval', async () => { const interval = 100; - const tokenPricesService = buildMockTokenPricesService(); + const tokenPricesService = buildMockTokenPricesService({ + fetchSupportedChainIds: jest + .fn() + .mockResolvedValue([ChainId.mainnet]), + }); jest.spyOn(tokenPricesService, 'fetchTokenPrices'); await withController( { @@ -1344,7 +1374,11 @@ describe('TokenRatesController', () => { describe('stop', () => { it('should stop polling', async () => { const interval = 100; - const tokenPricesService = buildMockTokenPricesService(); + const tokenPricesService = buildMockTokenPricesService({ + fetchSupportedChainIds: jest + .fn() + .mockResolvedValue([ChainId.mainnet]), + }); jest.spyOn(tokenPricesService, 'fetchTokenPrices'); await withController( { @@ -1399,7 +1433,9 @@ describe('TokenRatesController', () => { it('should poll on the right interval', async () => { const interval = 100; - const tokenPricesService = buildMockTokenPricesService(); + const tokenPricesService = buildMockTokenPricesService({ + fetchSupportedChainIds: jest.fn().mockResolvedValue([ChainId.mainnet]), + }); jest.spyOn(tokenPricesService, 'fetchTokenPrices'); await withController( { @@ -1444,6 +1480,9 @@ describe('TokenRatesController', () => { it('returns the exchange rates directly', async () => { const tokenPricesService = buildMockTokenPricesService({ fetchTokenPrices: fetchTokenPricesWithIncreasingPriceForEachToken, + fetchSupportedChainIds: jest + .fn() + .mockResolvedValue([ChainId.mainnet]), validateCurrencySupported(currency: unknown): currency is string { return currency === 'ETH'; }, @@ -1531,6 +1570,10 @@ describe('TokenRatesController', () => { }, }, }, + supportedChainIds: { + timestamp: Date.now(), + data: [ChainId.mainnet], + }, }); }, ); @@ -1544,6 +1587,9 @@ describe('TokenRatesController', () => { .reply(200, { LOL: fallbackRate }); const tokenPricesService = buildMockTokenPricesService({ fetchTokenPrices: fetchTokenPricesWithIncreasingPriceForEachToken, + fetchSupportedChainIds: jest + .fn() + .mockResolvedValue([ChainId.mainnet]), validateCurrencySupported(currency: unknown): currency is string { return currency !== 'LOL'; }, @@ -1650,7 +1696,11 @@ describe('TokenRatesController', () => { new Error('market does not exist for this coin pair'), ); - const tokenPricesService = buildMockTokenPricesService(); + const tokenPricesService = buildMockTokenPricesService({ + fetchSupportedChainIds: jest + .fn() + .mockResolvedValue([ChainId.mainnet]), + }); await withController( { options: { @@ -1704,7 +1754,11 @@ describe('TokenRatesController', () => { it('should stop polling', async () => { const interval = 100; - const tokenPricesService = buildMockTokenPricesService(); + const tokenPricesService = buildMockTokenPricesService({ + fetchSupportedChainIds: jest + .fn() + .mockResolvedValue([ChainId.mainnet]), + }); jest.spyOn(tokenPricesService, 'fetchTokenPrices'); await withController( { @@ -1796,8 +1850,14 @@ describe('TokenRatesController', () => { ); }); - it('does not update state if there are no tokens for the given chain', async () => { + it.only('does not update state if there are no tokens for the given chain', async () => { + const tokenPricesService = buildMockTokenPricesService({ + fetchSupportedChainIds: jest + .fn() + .mockResolvedValue([toHex(2), ChainId.mainnet]), + }); await withController( + { options: { tokenPricesService } }, async ({ controller, triggerTokensStateChange, @@ -1829,6 +1889,10 @@ describe('TokenRatesController', () => { }); expect(controller.state).toStrictEqual({ + supportedChainIds: { + timestamp: Date.now(), + data: [toHex(2), ChainId.mainnet], + }, marketData: { [ChainId.mainnet]: { '0x0000000000000000000000000000000000000000': { @@ -2658,6 +2722,10 @@ describe('TokenRatesController', () => { }, }, }, + supportedChainIds: { + timestamp: 0, + data: [], + }, }; await withController( diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 832a614401f..3577ba9729a 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -58,7 +58,7 @@ export type Token = { const DEFAULT_INTERVAL = 180000; -const DEFAULT_CACHE_REFRESH_THRESHOLD = 1000 * 60 * 60 * 24; // 24 hours +export const DEFAULT_CACHE_REFRESH_THRESHOLD = 1000 * 60 * 60 * 24; // 24 hours export type ContractExchangeRates = { [address: string]: number | undefined; @@ -523,6 +523,10 @@ export class TokenRatesController extends StaticIntervalPollingController { const supportedChainIds = await this.#tokenPricesService.fetchSupportedChainIds(); + console.log( + '🚀 ~ TokenRatesController ~ supportedChainIds::::::::::: from service', + supportedChainIds, + ); this.update((state) => { state.supportedChainIds = { timestamp: Date.now(), @@ -542,9 +546,12 @@ export class TokenRatesController extends StaticIntervalPollingController { + console.log( + '🚀 ~ TokenRatesController ~ updateExchangeRatesByChainId ~ chainId:===============', + chainId, + ); const tokenAddresses = this.#getTokenAddresses(chainId); + console.log( + '🚀 ~ TokenRatesController ~ updateExchangeRatesByChainId ~ tokenAddresses:==================', + tokenAddresses, + ); // Build a unique key based on chainId, nativeCurrency, and the number of token addresses. const updateKey: `${Hex}:${string}` = `${chainId}:${nativeCurrency}:${tokenAddresses.length}`; @@ -667,7 +682,18 @@ export class TokenRatesController extends StaticIntervalPollingController { - if (!this.#tokenPricesService.validateChainIdSupported(chainId)) { + console.log('🚀 ~ TokenRatesController ~ chainId:', chainId); + const supportedChainIds = this.state.supportedChainIds.data; + console.log( + '🚀 ~ TokenRatesController ~ supportedChainIds:', + supportedChainIds, + ); + if (!supportedChainIds.includes(chainId)) { + console.log( + 'early return because chainId is not supported', + chainId, + supportedChainIds, + ); return tokenAddresses.reduce((obj, tokenAddress) => { obj = { ...obj, @@ -721,7 +747,8 @@ export class TokenRatesController extends StaticIntervalPollingController({ * @param args.nativeCurrency - The native currency to request price in. * @param args.tokenAddresses - The list of contract addresses. * @param args.chainId - The chainId of the tokens. + * @param args.tokenRatesState - The state of the token rates controller. * @returns The prices for the requested tokens. */ export async function fetchTokenContractExchangeRates({ @@ -398,14 +400,16 @@ export async function fetchTokenContractExchangeRates({ nativeCurrency, tokenAddresses, chainId, + tokenRatesState, }: { tokenPricesService: AbstractTokenPricesService; nativeCurrency: string; tokenAddresses: Hex[]; chainId: Hex; + tokenRatesState: TokenRatesControllerState; }): Promise { const isChainIdSupported = - tokenPricesService.validateChainIdSupported(chainId); + tokenRatesState.supportedChainIds.data.includes(chainId); const isCurrencySupported = tokenPricesService.validateCurrencySupported(nativeCurrency); diff --git a/packages/assets-controllers/src/index.ts b/packages/assets-controllers/src/index.ts index c7f64f8b2bd..76612d7c93a 100644 --- a/packages/assets-controllers/src/index.ts +++ b/packages/assets-controllers/src/index.ts @@ -139,7 +139,6 @@ export { } from './assetsUtil'; export { CodefiTokenPricesServiceV2, - SUPPORTED_CHAIN_IDS, getNativeTokenAddress, } from './token-prices-service'; export { RatesController, Cryptocurrency } from './RatesController'; diff --git a/packages/assets-controllers/src/token-prices-service/codefi-v2.test.ts b/packages/assets-controllers/src/token-prices-service/codefi-v2.test.ts index 32f59ef1da2..7fded04a965 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.test.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.test.ts @@ -3,7 +3,6 @@ import { useFakeTimers } from 'sinon'; import { CodefiTokenPricesServiceV2, - SUPPORTED_CHAIN_IDS, SUPPORTED_CURRENCIES, ZERO_ADDRESS, getNativeTokenAddress, @@ -1287,25 +1286,6 @@ describe('CodefiTokenPricesServiceV2', () => { }); }); - /* describe('validateChainIdSupported', () => { - it.each(SUPPORTED_CHAIN_IDS)( - 'returns true if the given chain ID is %s', - (chainId) => { - expect( - new CodefiTokenPricesServiceV2().validateChainIdSupported(chainId), - ).toBe(true); - }, - ); - - it('returns false if the given chain ID is not one of the supported chain IDs', () => { - expect( - new CodefiTokenPricesServiceV2().validateChainIdSupported( - '0x999999999999999', - ), - ).toBe(false); - }); - }); */ - describe('validateCurrencySupported', () => { it.each(SUPPORTED_CURRENCIES)( 'returns true if the given currency is %s', 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 1715db863b1..d918ba6e866 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -181,96 +181,6 @@ type SupportedCurrency = | (typeof SUPPORTED_CURRENCIES)[number] | Uppercase<(typeof SUPPORTED_CURRENCIES)[number]>; -/** - * The list of chain IDs that can be supplied in the URL for the `/spot-prices` - * endpoint, but in hexadecimal form (for consistency with how we represent - * chain IDs in other places). - * @see Used by {@link CodefiTokenPricesServiceV2} to validate that a given chain ID is supported by V2 of the Codefi Price API. - */ -export const SUPPORTED_CHAIN_IDS = [ - // Ethereum Mainnet - '0x1', - // OP Mainnet - '0xa', - // Cronos Mainnet - '0x19', - // BNB Smart Chain Mainnet - '0x38', - // Syscoin Mainnet - '0x39', - // OKXChain Mainnet - '0x42', - // Hoo Smart Chain - '0x46', - // Meter Mainnet - '0x52', - // TomoChain - '0x58', - // Gnosis - '0x64', - // Velas EVM Mainnet - '0x6a', - // Fuse Mainnet - '0x7a', - // Huobi ECO Chain Mainnet - '0x80', - // Polygon Mainnet - '0x89', - // Fantom Opera - '0xfa', - // Boba Network - '0x120', - // KCC Mainnet - '0x141', - // zkSync Era Mainnet - '0x144', - // Theta Mainnet - '0x169', - // Metis Andromeda Mainnet - '0x440', - // Moonbeam - '0x504', - // Moonriver - '0x505', - // Mantle - '0x1388', - // Base - '0x2105', - // Shiden - '0x150', - // Smart Bitcoin Cash - '0x2710', - // Arbitrum One - '0xa4b1', - // Celo Mainnet - '0xa4ec', - // Oasis Emerald - '0xa516', - // Avalanche C-Chain - '0xa86a', - // Polis Mainnet - '0x518af', - // Aurora Mainnet - '0x4e454152', - // Harmony Mainnet Shard 0 - '0x63564c40', - // Linea Mainnet - '0xe708', - // Sei Mainnet - '0x531', - // Sonic Mainnet - '0x92', - // Monad Mainnet - '0x8f', -] as const; - -/** - * A chain ID that can be supplied in the URL for the `/spot-prices` endpoint, - * but in hexadecimal form (for consistency with how we represent chain IDs in - * other places). - */ -type SupportedChainId = (typeof SUPPORTED_CHAIN_IDS)[number]; - /** * All requests to V2 of the Price API start with this. */ @@ -365,8 +275,7 @@ type MarketDataByTokenAddress = { [address: Hex]: MarketData }; * fetch token prices. */ export class CodefiTokenPricesServiceV2 - implements - AbstractTokenPricesService + implements AbstractTokenPricesService { readonly #policy: ServicePolicy; @@ -485,7 +394,7 @@ export class CodefiTokenPricesServiceV2 tokenAddresses, currency, }: { - chainId: SupportedChainId; + chainId: Hex; tokenAddresses: Hex[]; currency: SupportedCurrency; }): Promise>> { @@ -543,6 +452,10 @@ export class CodefiTokenPricesServiceV2 async fetchSupportedChainIds(): Promise { const url = new URL(`${BASE_URL_V1}/supportedNetworks`); const response = await handleFetch(url); + console.log( + '🚀 ~ CodefiTokenPricesServiceV2 ~ fetchSupportedChainIds ~ response:!!!!!!!!!!!', + response, + ); const supportedChainIds = response.fullSupport.concat( response.partialSupport.spotPricesV2, @@ -550,18 +463,6 @@ export class CodefiTokenPricesServiceV2 return supportedChainIds.map((chainId: number) => toHex(chainId)); } - /** - * Type guard for whether the API can return token prices for the given chain - * ID. - * - * @param chainId - The chain ID to check. - * @returns True if the API supports the chain ID, false otherwise. - */ - /* validateChainIdSupported(chainId: unknown): chainId is SupportedChainId { - const supportedChainIds: readonly string[] = SUPPORTED_CHAIN_IDS; - return typeof chainId === 'string' && supportedChainIds.includes(chainId); - } */ - /** * Type guard for whether the API can return token prices in the given * currency. diff --git a/packages/assets-controllers/src/token-prices-service/index.test.ts b/packages/assets-controllers/src/token-prices-service/index.test.ts index a59be2ba4de..0f7cb7b2c51 100644 --- a/packages/assets-controllers/src/token-prices-service/index.test.ts +++ b/packages/assets-controllers/src/token-prices-service/index.test.ts @@ -5,7 +5,6 @@ describe('token-prices-service', () => { expect(Object.keys(allExports)).toMatchInlineSnapshot(` Array [ "CodefiTokenPricesServiceV2", - "SUPPORTED_CHAIN_IDS", "getNativeTokenAddress", ] `); diff --git a/packages/assets-controllers/src/token-prices-service/index.ts b/packages/assets-controllers/src/token-prices-service/index.ts index 509fc680055..5f466192fed 100644 --- a/packages/assets-controllers/src/token-prices-service/index.ts +++ b/packages/assets-controllers/src/token-prices-service/index.ts @@ -1,6 +1,2 @@ export type { AbstractTokenPricesService } from './abstract-token-prices-service'; -export { - CodefiTokenPricesServiceV2, - SUPPORTED_CHAIN_IDS, - getNativeTokenAddress, -} from './codefi-v2'; +export { CodefiTokenPricesServiceV2, getNativeTokenAddress } from './codefi-v2'; From 7f2ba5f75690188d5c4c65a6a62a7e1bba2f78f3 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Thu, 30 Oct 2025 10:49:10 +0100 Subject: [PATCH 03/12] fix: test --- .../src/TokenRatesController.test.ts | 316 ++++++++++++------ .../src/TokenRatesController.ts | 43 +-- 2 files changed, 225 insertions(+), 134 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index 275dd937f1a..cad77d343fc 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -1850,7 +1850,7 @@ describe('TokenRatesController', () => { ); }); - it.only('does not update state if there are no tokens for the given chain', async () => { + it('does not update state if there are no tokens for the given chain', async () => { const tokenPricesService = buildMockTokenPricesService({ fetchSupportedChainIds: jest .fn() @@ -1907,6 +1907,9 @@ describe('TokenRatesController', () => { it('does not update state if the price update fails', async () => { const tokenPricesService = buildMockTokenPricesService({ + fetchSupportedChainIds: jest + .fn() + .mockResolvedValue([ChainId.mainnet]), fetchTokenPrices: jest .fn() .mockRejectedValue(new Error('Failed to fetch')), @@ -1956,6 +1959,9 @@ describe('TokenRatesController', () => { .sort(); const tokenPricesService = buildMockTokenPricesService({ fetchTokenPrices: fetchTokenPricesWithIncreasingPriceForEachToken, + fetchSupportedChainIds: jest + .fn() + .mockResolvedValue([ChainId.mainnet, 0x3e7]), }); const fetchTokenPricesSpy = jest.spyOn( tokenPricesService, @@ -2019,6 +2025,9 @@ describe('TokenRatesController', () => { '0x0000000000000000000000000000000000000003', ]; const tokenPricesService = buildMockTokenPricesService({ + fetchSupportedChainIds: jest + .fn() + .mockResolvedValue([ChainId.mainnet]), fetchTokenPrices: jest.fn().mockResolvedValue({ [tokenAddresses[0]]: { currency: 'ETH', @@ -2081,29 +2090,31 @@ describe('TokenRatesController', () => { selectedNetworkClientId: InfuraNetworkType.mainnet, }); - expect(controller.state).toMatchInlineSnapshot(` - Object { - "marketData": Object { - "0x1": Object { - "0x0000000000000000000000000000000000000001": Object { - "currency": "ETH", - "tokenAddress": "0x0000000000000000000000000000000000000001", - "value": 0.001, - }, - "0x0000000000000000000000000000000000000002": Object { - "currency": "ETH", - "tokenAddress": "0x0000000000000000000000000000000000000002", - "value": 0.002, - }, - "0x0000000000000000000000000000000000000003": Object { - "currency": "ETH", - "tokenAddress": "0x0000000000000000000000000000000000000003", - "value": 0.003, - }, - }, - }, - } - `); + expect(controller.state).toStrictEqual({ + supportedChainIds: { + timestamp: Date.now(), + data: ['0x1'], + }, + marketData: { + '0x1': { + '0x0000000000000000000000000000000000000001': { + currency: 'ETH', + tokenAddress: '0x0000000000000000000000000000000000000001', + value: 0.001, + }, + '0x0000000000000000000000000000000000000002': { + currency: 'ETH', + tokenAddress: '0x0000000000000000000000000000000000000002', + value: 0.002, + }, + '0x0000000000000000000000000000000000000003': { + currency: 'ETH', + tokenAddress: '0x0000000000000000000000000000000000000003', + value: 0.003, + }, + }, + }, + }); }, ); }); @@ -2115,6 +2126,7 @@ describe('TokenRatesController', () => { '0x0000000000000000000000000000000000000002', ]; const tokenPricesService = buildMockTokenPricesService({ + fetchSupportedChainIds: jest.fn().mockResolvedValue([toHex(2)]), fetchTokenPrices: jest.fn().mockResolvedValue({ [tokenAddresses[0]]: { currency: 'ETH', @@ -2163,24 +2175,28 @@ describe('TokenRatesController', () => { setChainAsCurrent: false, }); - expect(controller.state).toMatchInlineSnapshot(` - Object { - "marketData": Object { - "0x2": Object { - "0x0000000000000000000000000000000000000001": Object { - "currency": "ETH", - "tokenAddress": "0x0000000000000000000000000000000000000001", - "value": 0.001, - }, - "0x0000000000000000000000000000000000000002": Object { - "currency": "ETH", - "tokenAddress": "0x0000000000000000000000000000000000000002", - "value": 0.002, - }, - }, - }, - } - `); + expect(controller.state).toStrictEqual({ + supportedChainIds: { + timestamp: Date.now(), + data: ['0x2'], + }, + marketData: { + '0x2': { + '0x0000000000000000000000000000000000000001': { + currency: 'ETH', + tokenAddress: + '0x0000000000000000000000000000000000000001', + value: 0.001, + }, + '0x0000000000000000000000000000000000000002': { + currency: 'ETH', + tokenAddress: + '0x0000000000000000000000000000000000000002', + value: 0.002, + }, + }, + }, + }); }, ); }); @@ -2198,6 +2214,7 @@ describe('TokenRatesController', () => { '0x0000000000000000000000000000000000000002', ]; const tokenPricesService = buildMockTokenPricesService({ + fetchSupportedChainIds: jest.fn().mockResolvedValue([toHex(137)]), fetchTokenPrices: jest.fn().mockResolvedValue({ [tokenAddresses[0]]: { currency: 'ETH', @@ -2263,38 +2280,40 @@ describe('TokenRatesController', () => { }); // token value in terms of matic should be (token value in eth) * (eth value in matic) - expect(controller.state).toMatchInlineSnapshot(` - Object { - "marketData": Object { - "0x89": Object { - "0x0000000000000000000000000000000000000001": Object { - "allTimeHigh": undefined, - "allTimeLow": undefined, - "currency": "UNSUPPORTED", - "dilutedMarketCap": undefined, - "high1d": undefined, - "low1d": undefined, - "marketCap": undefined, - "price": 0.0005, - "tokenAddress": "0x0000000000000000000000000000000000000001", - "totalVolume": undefined, - }, - "0x0000000000000000000000000000000000000002": Object { - "allTimeHigh": undefined, - "allTimeLow": undefined, - "currency": "UNSUPPORTED", - "dilutedMarketCap": undefined, - "high1d": undefined, - "low1d": undefined, - "marketCap": undefined, - "price": 0.001, - "tokenAddress": "0x0000000000000000000000000000000000000002", - "totalVolume": undefined, - }, - }, - }, - } - `); + expect(controller.state).toStrictEqual({ + supportedChainIds: { + timestamp: Date.now(), + data: ['0x89'], + }, + marketData: { + '0x89': { + '0x0000000000000000000000000000000000000001': { + allTimeHigh: undefined, + allTimeLow: undefined, + currency: 'UNSUPPORTED', + dilutedMarketCap: undefined, + high1d: undefined, + low1d: undefined, + marketCap: undefined, + price: 0.0005, + tokenAddress: '0x0000000000000000000000000000000000000001', + totalVolume: undefined, + }, + '0x0000000000000000000000000000000000000002': { + allTimeHigh: undefined, + allTimeLow: undefined, + currency: 'UNSUPPORTED', + dilutedMarketCap: undefined, + high1d: undefined, + low1d: undefined, + marketCap: undefined, + price: 0.001, + tokenAddress: '0x0000000000000000000000000000000000000002', + totalVolume: undefined, + }, + }, + }, + }); }, ); }); @@ -2310,6 +2329,7 @@ describe('TokenRatesController', () => { .map(buildAddress) .sort(); const tokenPricesService = buildMockTokenPricesService({ + fetchSupportedChainIds: jest.fn().mockResolvedValue([toHex(999)]), fetchTokenPrices: fetchTokenPricesWithIncreasingPriceForEachToken, validateCurrencySupported: ( currency: unknown, @@ -2404,6 +2424,9 @@ describe('TokenRatesController', () => { '0x0000000000000000000000000000000000000002', ]; const tokenPricesService = buildMockTokenPricesService({ + fetchSupportedChainIds: jest + .fn() + .mockResolvedValue([ChainId.mainnet]), fetchTokenPrices: jest.fn().mockResolvedValue({ [tokenAddresses[0]]: { currency: 'ETH', @@ -2457,22 +2480,25 @@ describe('TokenRatesController', () => { selectedNetworkClientId, }); - expect(controller.state).toMatchInlineSnapshot(` - Object { - "marketData": Object { - "0x3e7": Object { - "0x0000000000000000000000000000000000000001": undefined, - "0x0000000000000000000000000000000000000002": undefined, - }, - }, - } - `); + expect(controller.state).toStrictEqual({ + supportedChainIds: { + timestamp: Date.now(), + data: ['0x1'], + }, + marketData: { + '0x3e7': { + '0x0000000000000000000000000000000000000001': undefined, + '0x0000000000000000000000000000000000000002': undefined, + }, + }, + }); }, ); }); it('correctly calls the Price API with unqiue native token addresses (e.g. MATIC)', async () => { const tokenPricesService = buildMockTokenPricesService({ + fetchSupportedChainIds: jest.fn().mockResolvedValue(['0x89']), fetchTokenPrices: jest.fn().mockResolvedValue({ '0x0000000000000000000000000000000000001010': { currency: 'MATIC', @@ -2539,6 +2565,7 @@ describe('TokenRatesController', () => { }); const tokenPricesService = buildMockTokenPricesService({ fetchTokenPrices: fetchTokenPricesMock, + fetchSupportedChainIds: jest.fn().mockResolvedValue([toHex(1)]), }); await withController( { options: { tokenPricesService } }, @@ -2580,24 +2607,26 @@ describe('TokenRatesController', () => { expect(fetchTokenPricesMock).toHaveBeenCalledTimes(1); - expect(controller.state).toMatchInlineSnapshot(` - Object { - "marketData": Object { - "0x1": Object { - "0x0000000000000000000000000000000000000001": Object { - "currency": "ETH", - "tokenAddress": "0x0000000000000000000000000000000000000001", - "value": 0.001, - }, - "0x0000000000000000000000000000000000000002": Object { - "currency": "ETH", - "tokenAddress": "0x0000000000000000000000000000000000000002", - "value": 0.002, - }, - }, - }, - } - `); + expect(controller.state).toStrictEqual({ + supportedChainIds: { + timestamp: Date.now(), + data: ['0x1'], + }, + marketData: { + '0x1': { + '0x0000000000000000000000000000000000000001': { + currency: 'ETH', + tokenAddress: '0x0000000000000000000000000000000000000001', + value: 0.001, + }, + '0x0000000000000000000000000000000000000002': { + currency: 'ETH', + tokenAddress: '0x0000000000000000000000000000000000000002', + value: 0.002, + }, + }, + }, + }); }, ); }); @@ -2623,7 +2652,17 @@ describe('TokenRatesController', () => { fetchTokenPrices: fetchTokenPricesMock, }); await withController( - { options: { tokenPricesService } }, + { + options: { + tokenPricesService, + state: { + supportedChainIds: { + timestamp: Date.now() + DEFAULT_CACHE_REFRESH_THRESHOLD, + data: ['0x1'], + }, + }, + }, + }, async ({ controller, triggerTokensStateChange, @@ -2690,6 +2729,65 @@ describe('TokenRatesController', () => { }, ); }); + + it('will not call fetchSupportedChainIds if the cache is valid', async () => { + const tokenAddresses = [ + '0x0000000000000000000000000000000000000001', + '0x0000000000000000000000000000000000000002', + ]; + const fetchTokenPricesMock = jest.fn().mockResolvedValue({ + [tokenAddresses[0]]: { + currency: 'ETH', + tokenAddress: tokenAddresses[0], + value: 0.001, + }, + [tokenAddresses[1]]: { + currency: 'ETH', + tokenAddress: tokenAddresses[1], + value: 0.002, + }, + }); + const fetchSupportedChainIdsMock = jest.fn().mockResolvedValue(['0x1']); + const tokenPricesService = buildMockTokenPricesService({ + fetchTokenPrices: fetchTokenPricesMock, + fetchSupportedChainIds: fetchSupportedChainIdsMock, + }); + await withController( + { + options: { + tokenPricesService, + }, + mockTokensControllerState: { + allTokens: { + '0x1': { + [defaultSelectedAddress]: [ + { + address: mockTokenAddress, + decimals: 0, + symbol: '', + aggregators: [], + }, + ], + }, + }, + }, + }, + async ({ controller }) => { + // First call should call fetchSupportedChainIds + await controller.updateExchangeRates([ + { chainId: ChainId.mainnet, nativeCurrency: 'ETH' }, + ]); + + // Second call should not call fetchSupportedChainIds + await controller.updateExchangeRates([ + { chainId: ChainId.mainnet, nativeCurrency: 'ETH' }, + ]); + + expect(fetchSupportedChainIdsMock).toHaveBeenCalledTimes(1); + expect(fetchTokenPricesMock).toHaveBeenCalledTimes(2); + }, + ); + }); }); }); @@ -2741,6 +2839,10 @@ describe('TokenRatesController', () => { expect(controller.state).toStrictEqual({ marketData: {}, + supportedChainIds: { + timestamp: 0, + data: [], + }, }); }, ); @@ -2783,6 +2885,10 @@ describe('TokenRatesController', () => { ).toMatchInlineSnapshot(` Object { "marketData": Object {}, + "supportedChainIds": Object { + "data": Array [], + "timestamp": 0, + }, } `); }); @@ -2799,6 +2905,10 @@ describe('TokenRatesController', () => { ).toMatchInlineSnapshot(` Object { "marketData": Object {}, + "supportedChainIds": Object { + "data": Array [], + "timestamp": 0, + }, } `); }); diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 3577ba9729a..4375d5ca7f5 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -364,6 +364,7 @@ export class TokenRatesController extends StaticIntervalPollingController { @@ -523,10 +524,7 @@ export class TokenRatesController extends StaticIntervalPollingController { const supportedChainIds = await this.#tokenPricesService.fetchSupportedChainIds(); - console.log( - '🚀 ~ TokenRatesController ~ supportedChainIds::::::::::: from service', - supportedChainIds, - ); + this.update((state) => { state.supportedChainIds = { timestamp: Date.now(), @@ -546,12 +544,6 @@ export class TokenRatesController extends StaticIntervalPollingController { - console.log( - '🚀 ~ TokenRatesController ~ updateExchangeRatesByChainId ~ chainId:===============', - chainId, - ); const tokenAddresses = this.#getTokenAddresses(chainId); - console.log( - '🚀 ~ TokenRatesController ~ updateExchangeRatesByChainId ~ tokenAddresses:==================', - tokenAddresses, - ); + // Build a unique key based on chainId, nativeCurrency, and the number of token addresses. const updateKey: `${Hex}:${string}` = `${chainId}:${nativeCurrency}:${tokenAddresses.length}`; @@ -682,18 +672,9 @@ export class TokenRatesController extends StaticIntervalPollingController { - console.log('🚀 ~ TokenRatesController ~ chainId:', chainId); const supportedChainIds = this.state.supportedChainIds.data; - console.log( - '🚀 ~ TokenRatesController ~ supportedChainIds:', - supportedChainIds, - ); + if (!supportedChainIds.includes(chainId)) { - console.log( - 'early return because chainId is not supported', - chainId, - supportedChainIds, - ); return tokenAddresses.reduce((obj, tokenAddress) => { obj = { ...obj, @@ -746,9 +727,8 @@ export class TokenRatesController extends StaticIntervalPollingController { let contractNativeInformations; + console.log('fetchAndMapExchangeRatesForSupportedNativeCurrency: starting'); const tokenPricesByTokenAddress = await reduceInBatchesSerially< Hex, Awaited> From 229d0034c6e984163828ecec22aff11b4d1d6816 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Thu, 30 Oct 2025 11:34:53 +0100 Subject: [PATCH 04/12] fix: fix test --- .../src/TokenRatesController.ts | 2 +- .../assets-controllers/src/assetsUtil.test.ts | 45 +++++++++++++++++-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 3b396c8edfb..e089549ccba 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -218,7 +218,7 @@ const tokenRatesControllerMetadata: StateMetadata = { supportedChainIds: { includeInStateLogs: false, persist: true, - anonymous: false, + includeInDebugSnapshot: false, usedInUi: true, }, }; diff --git a/packages/assets-controllers/src/assetsUtil.test.ts b/packages/assets-controllers/src/assetsUtil.test.ts index fbb16a49820..39e0533498a 100644 --- a/packages/assets-controllers/src/assetsUtil.test.ts +++ b/packages/assets-controllers/src/assetsUtil.test.ts @@ -11,6 +11,7 @@ import * as assetsUtil from './assetsUtil'; import { TOKEN_PRICES_BATCH_SIZE } from './assetsUtil'; import type { Nft, NftMetadata } from './NftController'; import type { AbstractTokenPricesService } from './token-prices-service'; +import type { TokenRatesControllerState } from './TokenRatesController'; const DEFAULT_IPFS_URL_FORMAT = 'ipfs://'; const ALTERNATIVE_IPFS_URL_FORMAT = 'ipfs://ipfs/'; @@ -586,16 +587,20 @@ describe('assetsUtil', () => { it('should return empty object when chainId not supported', async () => { const testTokenAddress = '0x7BEF710a5759d197EC0Bf621c3Df802C2D60D848'; const mockPriceService = createMockPriceService(); - - jest - .spyOn(mockPriceService, 'validateChainIdSupported') - .mockReturnValue(false); + const testTokenRatesState: TokenRatesControllerState = { + marketData: {}, + supportedChainIds: { + timestamp: Date.now(), + data: ['0x1'], + }, + }; const result = await assetsUtil.fetchTokenContractExchangeRates({ tokenPricesService: mockPriceService, nativeCurrency: 'ETH', tokenAddresses: [testTokenAddress], chainId: '0x0', + tokenRatesState: testTokenRatesState, }); expect(result).toStrictEqual({}); @@ -607,12 +612,20 @@ describe('assetsUtil', () => { jest .spyOn(mockPriceService, 'validateCurrencySupported') .mockReturnValue(false); + const testTokenRatesState: TokenRatesControllerState = { + marketData: {}, + supportedChainIds: { + timestamp: Date.now(), + data: ['0x1'], + }, + }; const result = await assetsUtil.fetchTokenContractExchangeRates({ tokenPricesService: mockPriceService, nativeCurrency: 'X', tokenAddresses: [testTokenAddress], chainId: '0x1', + tokenRatesState: testTokenRatesState, }); expect(result).toStrictEqual({}); @@ -648,12 +661,20 @@ describe('assetsUtil', () => { pricePercentChange1d: 100, }, }); + const testTokenRatesState: TokenRatesControllerState = { + marketData: {}, + supportedChainIds: { + timestamp: Date.now(), + data: [testChainId], + }, + }; const result = await assetsUtil.fetchTokenContractExchangeRates({ tokenPricesService: mockPriceService, nativeCurrency: testNativeCurrency, tokenAddresses: [testTokenAddress], chainId: testChainId, + tokenRatesState: testTokenRatesState, }); expect(result).toMatchObject({ @@ -674,12 +695,20 @@ describe('assetsUtil', () => { mockPriceService, 'fetchTokenPrices', ); + const testTokenRatesState: TokenRatesControllerState = { + marketData: {}, + supportedChainIds: { + timestamp: Date.now(), + data: [testChainId], + }, + }; await assetsUtil.fetchTokenContractExchangeRates({ tokenPricesService: mockPriceService, nativeCurrency: testNativeCurrency, tokenAddresses: tokenAddresses as Hex[], chainId: testChainId, + tokenRatesState: testTokenRatesState, }); const numBatches = Math.ceil( @@ -716,11 +745,19 @@ describe('assetsUtil', () => { 'fetchTokenPrices', ); + const testTokenRatesState: TokenRatesControllerState = { + marketData: {}, + supportedChainIds: { + timestamp: Date.now(), + data: [testChainId], + }, + }; await assetsUtil.fetchTokenContractExchangeRates({ tokenPricesService: mockPriceService, nativeCurrency: testNativeCurrency, tokenAddresses: tokenAddresses as Hex[], chainId: testChainId, + tokenRatesState: testTokenRatesState, }); // Expect batches in ascending order From c52a24e30c5a6e19ff8b9a9667e625a22a6445ee Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Thu, 30 Oct 2025 12:16:54 +0100 Subject: [PATCH 05/12] fix: lint --- eslint-warning-thresholds.json | 4 ++-- packages/assets-controllers/src/TokenRatesController.ts | 6 +++--- .../src/token-prices-service/codefi-v2.test.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index 9d4a3f71419..383fdaf5874 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -57,7 +57,7 @@ }, "packages/assets-controllers/src/TokenRatesController.ts": { "@typescript-eslint/prefer-readonly": 1, - "jsdoc/check-tag-names": 11 + "jsdoc/check-tag-names": 9 }, "packages/assets-controllers/src/TokensController.test.ts": { "import-x/namespace": 1, @@ -80,7 +80,7 @@ "@typescript-eslint/prefer-promise-reject-errors": 2 }, "packages/assets-controllers/src/token-prices-service/codefi-v2.ts": { - "jsdoc/tag-lines": 2 + "jsdoc/tag-lines": 1 }, "packages/base-controller/src/BaseController.test.ts": { "import-x/namespace": 13 diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index e089549ccba..a323fed62c4 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -122,11 +122,11 @@ export type AllowedEvents = export const controllerName = 'TokenRatesController'; /** - * @type TokenRatesState + * TokenRatesState * * Token rates controller state - * @property marketData - Market data for tokens, keyed by chain ID and then token contract address. - * @property supportedChainIds - Supported chain ids. + * marketData - Market data for tokens, keyed by chain ID and then token contract address. + * supportedChainIds - Supported chain ids. */ export type TokenRatesControllerState = { marketData: Record>; diff --git a/packages/assets-controllers/src/token-prices-service/codefi-v2.test.ts b/packages/assets-controllers/src/token-prices-service/codefi-v2.test.ts index b6024b19dea..e570aba786a 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.test.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.test.ts @@ -1282,7 +1282,7 @@ describe('CodefiTokenPricesServiceV2', () => { const supportedChainIds = await new CodefiTokenPricesServiceV2().fetchSupportedChainIds(); - expect(supportedChainIds).toEqual(['0x1', '0x2']); + expect(supportedChainIds).toStrictEqual(['0x1', '0x2']); }); }); From c9326addc8a2c6fcc01567f88f4bdb32d473e005 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Thu, 30 Oct 2025 12:29:37 +0100 Subject: [PATCH 06/12] fix: changelog --- packages/assets-controllers/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 29464344c0c..409020390b5 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **BREAKING:** Removed exported `SUPPORTED_CHAIN_IDS` from `codefi-v2.ts` ([#7005](https://github.com/MetaMask/core/pull/7005)) +- Removed `validateChainIdSupported` from `codefi-v2.ts` ([#7005](https://github.com/MetaMask/core/pull/7005)) +- Added `fetchSupportedChainIds` in `codefi-v2.ts` ([#7005](https://github.com/MetaMask/core/pull/7005)) + ## [85.0.0] ### Added From 81db2b255d26d55323c944d10f259f9153c3011c Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Thu, 30 Oct 2025 13:44:22 +0100 Subject: [PATCH 07/12] fix: test --- .../transaction-pay-controller/src/utils/token.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/transaction-pay-controller/src/utils/token.test.ts b/packages/transaction-pay-controller/src/utils/token.test.ts index 3e95bfe857f..2c4187e992d 100644 --- a/packages/transaction-pay-controller/src/utils/token.test.ts +++ b/packages/transaction-pay-controller/src/utils/token.test.ts @@ -295,6 +295,10 @@ describe('Token Utils', () => { marketData: { [CHAIN_ID_MOCK]: {}, }, + supportedChainIds: { + timestamp: 0, + data: [], + }, }); const result = getTokenFiatRate( @@ -354,6 +358,10 @@ describe('Token Utils', () => { getTokenRatesControllerStateMock.mockReturnValue({ marketData: {}, + supportedChainIds: { + timestamp: 0, + data: [], + }, }); const result = getTokenFiatRate( From 21b4e2460e75ff4e456a061e22aa5356a914e9a2 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Thu, 30 Oct 2025 13:48:05 +0100 Subject: [PATCH 08/12] fix: test --- .../src/CurrencyRateController.test.ts | 4 ++-- .../abstract-token-prices-service.ts | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/assets-controllers/src/CurrencyRateController.test.ts b/packages/assets-controllers/src/CurrencyRateController.test.ts index ddec0853ae6..9a2b5d27c76 100644 --- a/packages/assets-controllers/src/CurrencyRateController.test.ts +++ b/packages/assets-controllers/src/CurrencyRateController.test.ts @@ -49,8 +49,8 @@ function buildMockTokenPricesService( async fetchExchangeRates() { return {}; }, - validateChainIdSupported(_chainId: unknown): _chainId is Hex { - return true; + async fetchSupportedChainIds() { + return []; }, validateCurrencySupported(_currency: unknown): _currency is string { return true; diff --git a/packages/assets-controllers/src/token-prices-service/abstract-token-prices-service.ts b/packages/assets-controllers/src/token-prices-service/abstract-token-prices-service.ts index 050c6041f07..db655f95056 100644 --- a/packages/assets-controllers/src/token-prices-service/abstract-token-prices-service.ts +++ b/packages/assets-controllers/src/token-prices-service/abstract-token-prices-service.ts @@ -119,15 +119,6 @@ export type AbstractTokenPricesService< cryptocurrencies: string[]; }): Promise>; - /** - * Type guard for whether the API can return token prices for the given chain - * ID. - * - * @param chainId - The chain ID to check. - * @returns True if the API supports the chain ID, false otherwise. - */ - // validateChainIdSupported(chainId: unknown): chainId is ChainId; - /** * Type guard for whether the API can return token prices in the given * currency. From 1ae9d4fd0f7234c7c460ce4d86bb4aa40e05e712 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Thu, 30 Oct 2025 13:51:32 +0100 Subject: [PATCH 09/12] fix: lint --- packages/assets-controllers/src/CurrencyRateController.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/assets-controllers/src/CurrencyRateController.test.ts b/packages/assets-controllers/src/CurrencyRateController.test.ts index 9a2b5d27c76..27c259f1adc 100644 --- a/packages/assets-controllers/src/CurrencyRateController.test.ts +++ b/packages/assets-controllers/src/CurrencyRateController.test.ts @@ -11,7 +11,6 @@ import { type MessengerEvents, type MockAnyNamespace, } from '@metamask/messenger'; -import type { Hex } from '@metamask/utils'; import nock from 'nock'; import { useFakeTimers } from 'sinon'; From cd13b660fa214e09bc69fd23f1e521353080dca3 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Fri, 31 Oct 2025 10:05:53 +0100 Subject: [PATCH 10/12] fix: update fetchTokenContractExchangeRates --- packages/assets-controllers/CHANGELOG.md | 1 + .../assets-controllers/src/assetsUtil.test.ts | 45 +++---------------- packages/assets-controllers/src/assetsUtil.ts | 10 ++--- 3 files changed, 10 insertions(+), 46 deletions(-) diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 409020390b5..0f1397f774b 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **BREAKING:** Added constructor argument `tokenPricesService` in `currencyRateController` ([#6863](https://github.com/MetaMask/core/pull/6863)) +- **BREAKING:** Added supportedChainIds argument in `fetchTokenContractExchangeRates` util function ([#6863](https://github.com/MetaMask/core/pull/6863)) - Added `fetchExchangeRates` function to fetch exchange rates from price-api ([#6863](https://github.com/MetaMask/core/pull/6863)) - Added `ignoreAssets` to allow ignoring assets for non-EVM chains ([#6981](https://github.com/MetaMask/core/pull/6981)) diff --git a/packages/assets-controllers/src/assetsUtil.test.ts b/packages/assets-controllers/src/assetsUtil.test.ts index 39e0533498a..693755ce7df 100644 --- a/packages/assets-controllers/src/assetsUtil.test.ts +++ b/packages/assets-controllers/src/assetsUtil.test.ts @@ -587,20 +587,13 @@ describe('assetsUtil', () => { it('should return empty object when chainId not supported', async () => { const testTokenAddress = '0x7BEF710a5759d197EC0Bf621c3Df802C2D60D848'; const mockPriceService = createMockPriceService(); - const testTokenRatesState: TokenRatesControllerState = { - marketData: {}, - supportedChainIds: { - timestamp: Date.now(), - data: ['0x1'], - }, - }; const result = await assetsUtil.fetchTokenContractExchangeRates({ tokenPricesService: mockPriceService, nativeCurrency: 'ETH', tokenAddresses: [testTokenAddress], chainId: '0x0', - tokenRatesState: testTokenRatesState, + supportedChainIds: ['0x1'], }); expect(result).toStrictEqual({}); @@ -612,20 +605,13 @@ describe('assetsUtil', () => { jest .spyOn(mockPriceService, 'validateCurrencySupported') .mockReturnValue(false); - const testTokenRatesState: TokenRatesControllerState = { - marketData: {}, - supportedChainIds: { - timestamp: Date.now(), - data: ['0x1'], - }, - }; const result = await assetsUtil.fetchTokenContractExchangeRates({ tokenPricesService: mockPriceService, nativeCurrency: 'X', tokenAddresses: [testTokenAddress], chainId: '0x1', - tokenRatesState: testTokenRatesState, + supportedChainIds: ['0x1'], }); expect(result).toStrictEqual({}); @@ -661,20 +647,13 @@ describe('assetsUtil', () => { pricePercentChange1d: 100, }, }); - const testTokenRatesState: TokenRatesControllerState = { - marketData: {}, - supportedChainIds: { - timestamp: Date.now(), - data: [testChainId], - }, - }; const result = await assetsUtil.fetchTokenContractExchangeRates({ tokenPricesService: mockPriceService, nativeCurrency: testNativeCurrency, tokenAddresses: [testTokenAddress], chainId: testChainId, - tokenRatesState: testTokenRatesState, + supportedChainIds: [testChainId], }); expect(result).toMatchObject({ @@ -695,20 +674,13 @@ describe('assetsUtil', () => { mockPriceService, 'fetchTokenPrices', ); - const testTokenRatesState: TokenRatesControllerState = { - marketData: {}, - supportedChainIds: { - timestamp: Date.now(), - data: [testChainId], - }, - }; await assetsUtil.fetchTokenContractExchangeRates({ tokenPricesService: mockPriceService, nativeCurrency: testNativeCurrency, tokenAddresses: tokenAddresses as Hex[], chainId: testChainId, - tokenRatesState: testTokenRatesState, + supportedChainIds: [testChainId], }); const numBatches = Math.ceil( @@ -745,19 +717,12 @@ describe('assetsUtil', () => { 'fetchTokenPrices', ); - const testTokenRatesState: TokenRatesControllerState = { - marketData: {}, - supportedChainIds: { - timestamp: Date.now(), - data: [testChainId], - }, - }; await assetsUtil.fetchTokenContractExchangeRates({ tokenPricesService: mockPriceService, nativeCurrency: testNativeCurrency, tokenAddresses: tokenAddresses as Hex[], chainId: testChainId, - tokenRatesState: testTokenRatesState, + supportedChainIds: [testChainId], }); // Expect batches in ascending order diff --git a/packages/assets-controllers/src/assetsUtil.ts b/packages/assets-controllers/src/assetsUtil.ts index 90f0b7b164b..9a35853f57b 100644 --- a/packages/assets-controllers/src/assetsUtil.ts +++ b/packages/assets-controllers/src/assetsUtil.ts @@ -15,7 +15,6 @@ import { CID } from 'multiformats/cid'; import type { Nft, NftMetadata } from './NftController'; import type { AbstractTokenPricesService } from './token-prices-service'; -import type { TokenRatesControllerState } from './TokenRatesController'; import { type ContractExchangeRates } from './TokenRatesController'; /** @@ -392,7 +391,7 @@ export async function reduceInBatchesSerially({ * @param args.nativeCurrency - The native currency to request price in. * @param args.tokenAddresses - The list of contract addresses. * @param args.chainId - The chainId of the tokens. - * @param args.tokenRatesState - The state of the token rates controller. + * @param args.supportedChainIds - The supported chain ids. * @returns The prices for the requested tokens. */ export async function fetchTokenContractExchangeRates({ @@ -400,16 +399,15 @@ export async function fetchTokenContractExchangeRates({ nativeCurrency, tokenAddresses, chainId, - tokenRatesState, + supportedChainIds, }: { tokenPricesService: AbstractTokenPricesService; nativeCurrency: string; tokenAddresses: Hex[]; chainId: Hex; - tokenRatesState: TokenRatesControllerState; + supportedChainIds: Hex[]; }): Promise { - const isChainIdSupported = - tokenRatesState.supportedChainIds.data.includes(chainId); + const isChainIdSupported = supportedChainIds.includes(chainId); const isCurrencySupported = tokenPricesService.validateCurrencySupported(nativeCurrency); From eee46d4b8f5bd3bda04bc7403e3a7e4f5636c7fc Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Fri, 31 Oct 2025 10:12:21 +0100 Subject: [PATCH 11/12] fix: lint --- packages/assets-controllers/src/assetsUtil.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/assets-controllers/src/assetsUtil.test.ts b/packages/assets-controllers/src/assetsUtil.test.ts index 693755ce7df..a928082e02f 100644 --- a/packages/assets-controllers/src/assetsUtil.test.ts +++ b/packages/assets-controllers/src/assetsUtil.test.ts @@ -11,7 +11,6 @@ import * as assetsUtil from './assetsUtil'; import { TOKEN_PRICES_BATCH_SIZE } from './assetsUtil'; import type { Nft, NftMetadata } from './NftController'; import type { AbstractTokenPricesService } from './token-prices-service'; -import type { TokenRatesControllerState } from './TokenRatesController'; const DEFAULT_IPFS_URL_FORMAT = 'ipfs://'; const ALTERNATIVE_IPFS_URL_FORMAT = 'ipfs://ipfs/'; From 66373b49910026675778e6227223710ede75436b Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Tue, 4 Nov 2025 14:23:32 +0100 Subject: [PATCH 12/12] fix: cleanup --- .../src/TokenRatesController.test.ts | 17 ++++++++--------- .../src/TokenRatesController.ts | 12 +++--------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index 81feb2e886a..a0b2530dd61 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -2794,18 +2794,17 @@ describe('TokenRatesController', () => { }, }, async ({ controller }) => { - // First call should call fetchSupportedChainIds - await controller.updateExchangeRates([ - { chainId: ChainId.mainnet, nativeCurrency: 'ETH' }, - ]); - - // Second call should not call fetchSupportedChainIds - await controller.updateExchangeRates([ - { chainId: ChainId.mainnet, nativeCurrency: 'ETH' }, + // use promise.all to call the updateExchangeRates method twice + await Promise.all([ + controller.updateExchangeRates([ + { chainId: ChainId.mainnet, nativeCurrency: 'ETH' }, + ]), + controller.updateExchangeRates([ + { chainId: ChainId.mainnet, nativeCurrency: 'ETH' }, + ]), ]); expect(fetchSupportedChainIdsMock).toHaveBeenCalledTimes(1); - expect(fetchTokenPricesMock).toHaveBeenCalledTimes(2); }, ); }); diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index a323fed62c4..75909be3a31 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -363,7 +363,6 @@ export class TokenRatesController extends StaticIntervalPollingController { @@ -566,12 +565,6 @@ export class TokenRatesController extends StaticIntervalPollingController { @@ -671,6 +664,9 @@ export class TokenRatesController extends StaticIntervalPollingController { + if (!this.#isPriceApiSupportedChainIdsCacheValid()) { + await this.#updateSupportedChainIdsCache(); + } const supportedChainIds = this.state.supportedChainIds.data; if (!supportedChainIds.includes(chainId)) { @@ -726,7 +722,6 @@ export class TokenRatesController extends StaticIntervalPollingController { let contractNativeInformations; - console.log('fetchAndMapExchangeRatesForSupportedNativeCurrency: starting'); const tokenPricesByTokenAddress = await reduceInBatchesSerially< Hex, Awaited>