diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 92fee49f56e..67c308c101f 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/account-tree-controller` from `^4.0.0` to `^4.1.0` ([#7869](https://github.com/MetaMask/core/pull/7869)) - Bump `@metamask/multichain-account-service` from `^5.1.0` to `^6.0.0` ([#7869](https://github.com/MetaMask/core/pull/7869)) +- 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 ## [99.3.1] 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..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 { @@ -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(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..ea7d4f4272c 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,12 @@ type SupportedNetworksResponse = { */ let lastFetchedSupportedNetworks: SupportedNetworksResponse | null = null; +/** + * In-flight promise to prevent concurrent requests to the supported networks endpoint. + */ +let runningSupportedNetworksRequest: Promise | null = + null; + /** * Converts a CAIP-2 chain ID (e.g., 'eip155:1') to a hex chain ID (e.g., '0x1'). * @@ -382,12 +388,13 @@ 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. + * Executes the actual fetch to the supported networks endpoint. + * Handles errors internally by falling back to the hardcoded list, + * and clears the in-flight promise when done. * * @returns The supported networks response. */ -export async function fetchSupportedNetworks(): Promise { +async function executeSupportedNetworksFetch(): Promise { try { const url = `${BASE_URL_V2}/supportedNetworks`; const response = await handleFetch(url, { @@ -409,7 +416,29 @@ export async function fetchSupportedNetworks(): Promise { + // If a fetch is already in progress, return the same promise + if (runningSupportedNetworksRequest) { + return runningSupportedNetworksRequest; } + + // Start a new fetch and cache the promise + runningSupportedNetworksRequest = executeSupportedNetworksFetch(); + + return runningSupportedNetworksRequest; } /** @@ -453,6 +482,7 @@ function getSupportedNetworksFallback(): SupportedNetworksResponse { */ export function resetSupportedNetworksCache(): void { lastFetchedSupportedNetworks = null; + runningSupportedNetworksRequest = null; } /** @@ -687,8 +717,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 +823,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);