Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/assets-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ 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

## [99.2.0]

### Added
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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', () => {
Expand Down
74 changes: 50 additions & 24 deletions packages/assets-controllers/src/token-prices-service/codefi-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SupportedNetworksResponse> | null =
null;

/**
* Converts a CAIP-2 chain ID (e.g., 'eip155:1') to a hex chain ID (e.g., '0x1').
*
Expand All @@ -384,32 +390,47 @@ 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<SupportedNetworksResponse> {
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 (runningSupportedNetworksRequest) {
return runningSupportedNetworksRequest;
}

if (
response &&
typeof response === 'object' &&
'fullSupport' in response &&
'partialSupport' in response
) {
lastFetchedSupportedNetworks = response as SupportedNetworksResponse;
return lastFetchedSupportedNetworks;
}
// Start a new fetch and cache the promise
runningSupportedNetworksRequest =
(async (): Promise<SupportedNetworksResponse> => {
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;
}

// Invalid response format, fall back to hardcoded list
return getSupportedNetworksFallback();
} catch {
// On any error, fall back to the hardcoded list
return getSupportedNetworksFallback();
}
// 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;
}
})();

return runningSupportedNetworksRequest;
}

/**
Expand Down Expand Up @@ -453,6 +474,7 @@ function getSupportedNetworksFallback(): SupportedNetworksResponse {
*/
export function resetSupportedNetworksCache(): void {
lastFetchedSupportedNetworks = null;
runningSupportedNetworksRequest = null;
}

/**
Expand Down Expand Up @@ -687,8 +709,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();
Expand Down Expand Up @@ -791,8 +815,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);
Expand Down
Loading