From b01420f457ec9df596950f2ef0e0a7fb87cc2823 Mon Sep 17 00:00:00 2001 From: juanmigdr Date: Mon, 2 Feb 2026 16:22:31 +0100 Subject: [PATCH 1/5] fix: avoid making unecessary api calls --- .../token-prices-service/codefi-v2.test.ts | 33 +++++++++ .../src/token-prices-service/codefi-v2.ts | 70 +++++++++++++------ 2 files changed, 80 insertions(+), 23 deletions(-) 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 f00735fccd9..984f01d95e4 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 @@ -1987,6 +1987,39 @@ describe('CodefiTokenPricesServiceV2', () => { expect(result).toStrictEqual(mockResponse); }); + + it('deduplicates concurrent requests to the same endpoint', async () => { + const mockResponse = { + fullSupport: ['eip155:1'], + partialSupport: { + spotPricesV2: ['eip155:1', 'eip155:56'], + spotPricesV3: ['eip155:1', 'eip155:42161'], + }, + }; + // Only set up the mock to respond once + nock('https://price.api.cx.metamask.io') + .get('/v2/supportedNetworks') + .reply(200, mockResponse); + + // Make 5 concurrent calls + const promises = [ + fetchSupportedNetworks(), + fetchSupportedNetworks(), + fetchSupportedNetworks(), + fetchSupportedNetworks(), + fetchSupportedNetworks(), + ]; + + const results = await Promise.all(promises); + + // All promises should resolve to the same response + results.forEach((result) => { + expect(result).toStrictEqual(mockResponse); + }); + + // Verify no pending mocks (i.e., only one request was made) + expect(nock.isDone()).toBe(true); + }); }); describe('getSupportedNetworks', () => { 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 9496053cab2..43c87727935 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -367,6 +367,11 @@ type SupportedNetworksResponse = { */ let lastFetchedSupportedNetworks: SupportedNetworksResponse | null = null; +/** + * In-flight promise to prevent concurrent requests to the same endpoint. + */ +let inFlightFetchPromise: Promise | null = null; + /** * Converts a CAIP-2 chain ID (e.g., 'eip155:1') to a hex chain ID (e.g., '0x1'). * @@ -384,32 +389,46 @@ function caipChainIdToHex(caipChainId: string): Hex | null { /** * Fetches the list of supported networks from the API. * Falls back to the hardcoded list if the fetch fails. + * Deduplicates concurrent requests by returning the same promise if a fetch is already in progress. * * @returns The supported networks response. */ export async function fetchSupportedNetworks(): Promise { - try { - const url = `${BASE_URL_V2}/supportedNetworks`; - const response = await handleFetch(url, { - headers: { 'Cache-Control': 'no-cache' }, - }); + // If a fetch is already in progress, return the same promise + if (inFlightFetchPromise) { + return inFlightFetchPromise; + } + + // Start a new fetch and cache the promise + inFlightFetchPromise = (async (): Promise => { + try { + const url = `${BASE_URL_V2}/supportedNetworks`; + const response = await handleFetch(url, { + headers: { 'Cache-Control': 'no-cache' }, + }); + + if ( + response && + typeof response === 'object' && + 'fullSupport' in response && + 'partialSupport' in response + ) { + lastFetchedSupportedNetworks = response as SupportedNetworksResponse; + return lastFetchedSupportedNetworks; + } - if ( - response && - typeof response === 'object' && - 'fullSupport' in response && - 'partialSupport' in response - ) { - lastFetchedSupportedNetworks = response as SupportedNetworksResponse; - return lastFetchedSupportedNetworks; + // Invalid response format, fall back to hardcoded list + return getSupportedNetworksFallback(); + } catch { + // On any error, fall back to the hardcoded list + return getSupportedNetworksFallback(); + } finally { + // Clear the in-flight promise once the request completes + inFlightFetchPromise = null; } + })(); - // Invalid response format, fall back to hardcoded list - return getSupportedNetworksFallback(); - } catch { - // On any error, fall back to the hardcoded list - return getSupportedNetworksFallback(); - } + return inFlightFetchPromise; } /** @@ -453,6 +472,7 @@ function getSupportedNetworksFallback(): SupportedNetworksResponse { */ export function resetSupportedNetworksCache(): void { lastFetchedSupportedNetworks = null; + inFlightFetchPromise = null; } /** @@ -687,8 +707,10 @@ export class CodefiTokenPricesServiceV2 // Refresh supported networks in background (non-blocking) // This ensures the list stays fresh during normal polling // Note: fetchSupportedNetworks handles errors internally and always resolves - // eslint-disable-next-line @typescript-eslint/no-floating-promises - fetchSupportedNetworks(); + if (!lastFetchedSupportedNetworks) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + fetchSupportedNetworks(); + } // Get dynamically fetched supported chain IDs for V3 const supportedChainIdsV3 = getSupportedChainIdsV3AsHex(); @@ -791,8 +813,10 @@ export class CodefiTokenPricesServiceV2 // Refresh supported currencies in background (non-blocking) // This ensures the list stays fresh during normal polling // Note: fetchSupportedCurrencies handles errors internally and always resolves - // eslint-disable-next-line @typescript-eslint/no-floating-promises - fetchSupportedCurrencies(); + if (!lastFetchedCurrencies) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + fetchSupportedCurrencies(); + } const url = new URL(`${BASE_URL_V1}/exchange-rates`); url.searchParams.append('baseCurrency', baseCurrency); From 90c28a35eab551fa22ab1642499cc090c5f2b182 Mon Sep 17 00:00:00 2001 From: juanmigdr Date: Tue, 3 Feb 2026 10:22:18 +0100 Subject: [PATCH 2/5] chore: fixed linting --- .../src/token-prices-service/codefi-v2.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 984f01d95e4..18085643b66 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 @@ -1,6 +1,6 @@ import { KnownCaipNamespace } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; -import nock from 'nock'; +import nock, { isDone } from 'nock'; import { useFakeTimers } from 'sinon'; import { @@ -2018,7 +2018,7 @@ describe('CodefiTokenPricesServiceV2', () => { }); // Verify no pending mocks (i.e., only one request was made) - expect(nock.isDone()).toBe(true); + expect(isDone()).toBe(true); }); }); From 0f5edbe3f07b77c98ff0c2399d5d052be8da1b6f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Feb 2026 09:25:43 +0000 Subject: [PATCH 3/5] docs: update assets-controllers changelog for API call optimization Co-authored-by: juan.gutierrez --- packages/assets-controllers/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 046e34cdf4f..49f67739c64 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Optimize Price API performance by deduplicating concurrent API calls ([#7811](https://github.com/MetaMask/core/pull/7811)) + - Add in-flight promise caching for `fetchSupportedNetworks()` to prevent duplicate concurrent requests + - Update `fetchTokenPrices()` and `fetchExchangeRates()` to only refresh supported networks/currencies when no cached value exists + - Add `resetSupportedNetworksCache()` method to clear cached promises when needed + ## [99.2.0] ### Added From 2924ddf337cc9a8ee36669094c88192b4d4b4302 Mon Sep 17 00:00:00 2001 From: juanmigdr Date: Tue, 3 Feb 2026 10:45:18 +0100 Subject: [PATCH 4/5] chore: minor rename --- .../src/token-prices-service/codefi-v2.ts | 64 ++++++++++--------- 1 file changed, 33 insertions(+), 31 deletions(-) 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 43c87727935..8b6854ea412 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -368,9 +368,10 @@ type SupportedNetworksResponse = { let lastFetchedSupportedNetworks: SupportedNetworksResponse | null = null; /** - * In-flight promise to prevent concurrent requests to the same endpoint. + * In-flight promise to prevent concurrent requests to the supported networks endpoint. */ -let inFlightFetchPromise: Promise | null = null; +let runningSupportedNetworksRequest: Promise | null = + null; /** * Converts a CAIP-2 chain ID (e.g., 'eip155:1') to a hex chain ID (e.g., '0x1'). @@ -395,40 +396,41 @@ function caipChainIdToHex(caipChainId: string): Hex | null { */ export async function fetchSupportedNetworks(): Promise { // If a fetch is already in progress, return the same promise - if (inFlightFetchPromise) { - return inFlightFetchPromise; + if (runningSupportedNetworksRequest) { + return runningSupportedNetworksRequest; } // Start a new fetch and cache the promise - inFlightFetchPromise = (async (): Promise => { - try { - const url = `${BASE_URL_V2}/supportedNetworks`; - const response = await handleFetch(url, { - headers: { 'Cache-Control': 'no-cache' }, - }); + runningSupportedNetworksRequest = + (async (): Promise => { + try { + const url = `${BASE_URL_V2}/supportedNetworks`; + const response = await handleFetch(url, { + headers: { 'Cache-Control': 'no-cache' }, + }); + + if ( + response && + typeof response === 'object' && + 'fullSupport' in response && + 'partialSupport' in response + ) { + lastFetchedSupportedNetworks = response as SupportedNetworksResponse; + return lastFetchedSupportedNetworks; + } - if ( - response && - typeof response === 'object' && - 'fullSupport' in response && - 'partialSupport' in response - ) { - lastFetchedSupportedNetworks = response as SupportedNetworksResponse; - return lastFetchedSupportedNetworks; + // Invalid response format, fall back to hardcoded list + return getSupportedNetworksFallback(); + } catch { + // On any error, fall back to the hardcoded list + return getSupportedNetworksFallback(); + } finally { + // Clear the in-flight promise once the request completes + runningSupportedNetworksRequest = null; } + })(); - // Invalid response format, fall back to hardcoded list - return getSupportedNetworksFallback(); - } catch { - // On any error, fall back to the hardcoded list - return getSupportedNetworksFallback(); - } finally { - // Clear the in-flight promise once the request completes - inFlightFetchPromise = null; - } - })(); - - return inFlightFetchPromise; + return runningSupportedNetworksRequest; } /** @@ -472,7 +474,7 @@ function getSupportedNetworksFallback(): SupportedNetworksResponse { */ export function resetSupportedNetworksCache(): void { lastFetchedSupportedNetworks = null; - inFlightFetchPromise = null; + runningSupportedNetworksRequest = null; } /** From 21e43225d6f54dbfb27d8d632201188838ed9795 Mon Sep 17 00:00:00 2001 From: juanmigdr Date: Tue, 3 Feb 2026 10:47:34 +0100 Subject: [PATCH 5/5] chore: minor --- packages/assets-controllers/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 49f67739c64..0a56dc8b57a 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -12,7 +12,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Optimize Price API performance by deduplicating concurrent API calls ([#7811](https://github.com/MetaMask/core/pull/7811)) - Add in-flight promise caching for `fetchSupportedNetworks()` to prevent duplicate concurrent requests - Update `fetchTokenPrices()` and `fetchExchangeRates()` to only refresh supported networks/currencies when no cached value exists - - Add `resetSupportedNetworksCache()` method to clear cached promises when needed ## [99.2.0]