From d070106d336f5994b44ecfac67fe28fa75353750 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 11 Nov 2025 15:44:22 +0000 Subject: [PATCH 01/42] prices v3 upgrade --- .../abstract-token-prices-service.ts | 23 +++ .../src/token-prices-service/codefi-v2.ts | 154 +++++++++++++++++- 2 files changed, 175 insertions(+), 2 deletions(-) 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 ddc7a3e159b..01d022ec563 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 @@ -93,6 +93,29 @@ export type AbstractTokenPricesService< currency: Currency; }): Promise>>; + // fetchTokenPricesV3({ + // assets, + // currency, + // }: { + // assets: ( + // | { address: Hex; chainId: Hex } + // | { address: CaipAssetType; chainId: CaipChainId } + // )[]; + // currency: SupportedCurrency; + // }): (MarketData & { + // assetId: CaipAssetType; + // currency: SupportedCurrency; + // } & ( + // | { + // address: Hex; + // chainId: Hex; + // } + // | { + // address: CaipAssetType; + // chainId: CaipChainId; + // } + // ))[]; + /** * Retrieves exchange rates in the given currency. * 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 5ec2ddcee3c..422d83d8ec0 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -1,4 +1,5 @@ import { + ChainId, createServicePolicy, DEFAULT_CIRCUIT_BREAK_DURATION, DEFAULT_DEGRADED_THRESHOLD, @@ -7,8 +8,21 @@ import { handleFetch, } from '@metamask/controller-utils'; import type { ServicePolicy } from '@metamask/controller-utils'; -import type { Hex } from '@metamask/utils'; -import { hexToNumber } from '@metamask/utils'; +import type { + CaipAssetReference, + CaipAssetType, + CaipChainId, + Hex, +} from '@metamask/utils'; +import { + hexToNumber, + isCaipAssetId, + isCaipAssetType, + isHexString, + isStrictHexString, + KnownCaipNamespace, + toCaipChainId, +} from '@metamask/utils'; import type { AbstractTokenPricesService, @@ -16,6 +30,7 @@ import type { TokenPrice, TokenPricesByTokenAddress, } from './abstract-token-prices-service'; +import { accountAddressToCaipReference } from 'src/assetsUtil'; /** * The list of currencies that can be supplied as the `vsCurrency` parameter to @@ -174,6 +189,50 @@ const chainIdToNativeTokenAddress: Record = { export const getNativeTokenAddress = (chainId: Hex): Hex => chainIdToNativeTokenAddress[chainId] ?? ZERO_ADDRESS; +// We can only support PricesAPI V3 for EVM chains that have a CAIP-19 native asset mapping. +export const HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP: Record< + Hex, + CaipAssetType +> = { + '0x1': 'eip155:1/slip44:60', // Ethereum Mainnet + '0xa': 'eip155:10/slip44:60', // OP Mainnet + '0x19': 'eip155:25/slip44:394', // Cronos Mainnet + '0x38': 'eip155:56/slip44:714', // BNB Smart Chain Mainnet + '0x39': 'eip155:57/slip44:57', // Syscoin Mainnet + // '0x42': 'eip155:1/slip44:60', // OKXChain Mainnet + // '0x46': 'eip155:1/slip44:60', // Hoo Smart Chain + // '0x52': 'eip155:1/slip44:60', // Meter Mainnet + // '0x58': 'eip155:1/slip44:60', // TomoChain + // '0x64': 'eip155:1/slip44:60', // Gnosis + // '0x6a': 'eip155:1/slip44:60', // Velas EVM Mainnet + // '0x7a': 'eip155:1/slip44:60', // Fuse Mainnet + // '0x80': 'eip155:1/slip44:60', // Huobi ECO Chain Mainnet + '0x89': 'eip155:137/slip44:966', // Polygon Mainnet + '0x8f': 'eip155:143/slip44:268435779', // Monad Mainnet + // '0x92': 'eip155:1/slip44:60', // Sonic Mainnet + // '0xfa': 'eip155:1/slip44:60', // Fantom Opera + // '0x120': 'eip155:1/slip44:60', // Boba Network + // '0x141': 'eip155:1/slip44:60', // KCC Mainnet + // '0x144': 'eip155:1/slip44:60', // zkSync Era Mainnet + // '0x150': 'eip155:1/slip44:60', // Shiden + // '0x169': 'eip155:1/slip44:60', // Theta Mainnet + // '0x440': 'eip155:1/slip44:60', // Metis Andromeda Mainnet + // '0x504': 'eip155:1/slip44:60', // Moonbeam + // '0x505': 'eip155:1/slip44:60', // Moonriver + '0x531': 'eip155:1329/slip44:19000118', // Sei Mainnet + // '0x1388': 'eip155:1/slip44:60', // Mantle + '0x2105': 'eip155:8453/slip44:60', // Base + // '0x2710': 'eip155:1/slip44:60', // Smart Bitcoin Cash + '0xa4b1': 'eip155:42161/slip44:60', // Arbitrum One + // '0xa4ec': 'eip155:1/slip44:60', // Celo Mainnet + // '0xa516': 'eip155:1/slip44:60', // Oasis Emerald + '0xa86a': 'eip155:43114/slip44:9000', // Avalanche C-Chain + '0xe708': 'eip155:59144/slip44:60', // Linea Mainnet + // '0x518af': 'eip155:1/slip44:60', // Polis Mainnet + // '0x4e454152': 'eip155:1/slip44:60', // Aurora Mainnet + // '0x63564c40': 'eip155:1/slip44:60', // Harmony Mainnet Shard 0 +}; + /** * A currency that can be supplied as the `vsCurrency` parameter to * the `/spot-prices` endpoint. Covers both uppercase and lowercase versions. @@ -273,6 +332,16 @@ export const SUPPORTED_CHAIN_IDS = [ */ type SupportedChainId = (typeof SUPPORTED_CHAIN_IDS)[number]; +const SLIP44_CHAIN_MAP: Record = { + ETH: 'slip44:60', + POL: 'slip44:966', + BNB: 'slip44:714', + AVAX: 'slip44:9000', + TESTETH: 'slip44:60', + SEI: 'slip44:19000118', + MON: 'slip44:268435779', +}; + /** * All requests to V2 of the Price API start with this. */ @@ -280,6 +349,8 @@ const BASE_URL = 'https://price.api.cx.metamask.io/v2'; const BASE_URL_V1 = 'https://price.api.cx.metamask.io/v1'; +const BASE_URL_V3 = 'https://price.api.cx.metamask.io/v3'; + /** * The shape of the data that the /spot-prices endpoint returns. */ @@ -534,6 +605,85 @@ export class CodefiTokenPricesServiceV2 ) as Partial>; } + async fetchTokenPricesV3({ + assets, + currency, + }: { + assets: ( + | { address: Hex; chainId: Hex } + | { address: CaipAssetType; chainId: CaipChainId } + )[]; + currency: SupportedCurrency; + }) { + const assetsWithIds: ({ assetId: CaipAssetType } & ( + | { address: Hex; chainId: Hex } + | { address: CaipAssetType; chainId: CaipChainId } + ))[] = assets.map((asset) => { + if (isStrictHexString(asset.address)) { + const caipChainId = toCaipChainId( + KnownCaipNamespace.Eip155, + hexToNumber(asset.chainId).toString(), + ); + + const nativeAsset = + HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP[asset.chainId as Hex]; + + return { + assetId: nativeAsset ?? `${caipChainId}/erc20:${asset.address}`, + address: asset.address, + chainId: asset.chainId as Hex, + }; + } + + return { + assetId: asset.address, + address: asset.address, + chainId: asset.chainId as CaipChainId, + }; + }); + + const url = new URL(`${BASE_URL_V3}/spot-prices`); + url.searchParams.append( + 'assetIds', + assetsWithIds.map((asset) => asset.assetId).join(','), + ); + url.searchParams.append('vsCurrency', currency); + url.searchParams.append('includeMarketData', 'true'); + + const addressCryptoDataMap: { [assetId: CaipAssetType]: MarketData } = + await this.#policy.execute(() => + handleFetch(url, { headers: { 'Cache-Control': 'no-cache' } }), + ); + + return assetsWithIds + .map((assetWithId) => { + const marketData = addressCryptoDataMap[assetWithId.assetId]; + + if (!marketData) { + return undefined; + } + + return { + ...assetWithId, + currency, + ...marketData, + }; + }) + .filter(Boolean) as (MarketData & { + assetId: CaipAssetType; + currency: SupportedCurrency; + } & ( + | { + address: Hex; + chainId: Hex; + } + | { + address: CaipAssetType; + chainId: CaipChainId; + } + ))[]; + } + /** * Retrieves exchange rates in the given base currency. * From 6d1f9e50f9e2d250e8c3e05ce87c2c40c13a0882 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 11 Nov 2025 16:18:45 +0000 Subject: [PATCH 02/42] only support evm tokens --- .../abstract-token-prices-service.ts | 147 +++++++++++----- .../src/token-prices-service/codefi-v2.ts | 158 +++--------------- 2 files changed, 122 insertions(+), 183 deletions(-) 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 01d022ec563..3b430f3254b 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 @@ -1,11 +1,11 @@ import type { ServicePolicy } from '@metamask/controller-utils'; -import type { Hex } from '@metamask/utils'; +import type { CaipAssetType, Hex } from '@metamask/utils'; /** * Represents the price of a token in a currency. */ -export type TokenPrice = { - tokenAddress: TokenAddress; +export type TokenPrice = { + tokenAddress: Hex; currency: Currency; allTimeHigh: number; allTimeLow: number; @@ -41,11 +41,8 @@ export type ExchangeRate = { /** * A map of token address to its price. */ -export type TokenPricesByTokenAddress< - TokenAddress extends Hex, - Currency extends string, -> = { - [A in TokenAddress]: TokenPrice; +export type TokenPricesByTokenAddress = { + [A in Hex]: TokenPrice; }; /** @@ -55,22 +52,110 @@ export type ExchangeRatesByCurrency = { [C in Currency]: ExchangeRate; }; +export type EvmAssetAddressWithChain = { + address: Hex; + chainId: ChainId; +}; + +export type EvmAssetWithId = + EvmAssetAddressWithChain & { + assetId: CaipAssetType; + }; + +export type EvmAssetWithMarketData< + ChainId extends Hex, + Currency extends string, +> = EvmAssetWithId & MarketData & { currency: Currency }; + +/** + * The shape of the data that the /spot-prices endpoint returns. + */ +export type MarketData = { + /** + * The all-time highest price of the token. + */ + allTimeHigh: number; + /** + * The all-time lowest price of the token. + */ + allTimeLow: number; + /** + * The number of tokens currently in circulation. + */ + circulatingSupply: number; + /** + * The market cap calculated using the diluted supply. + */ + dilutedMarketCap: number; + /** + * The highest price of the token in the last 24 hours. + */ + high1d: number; + /** + * The lowest price of the token in the last 24 hours. + */ + low1d: number; + /** + * The current market capitalization of the token. + */ + marketCap: number; + /** + * The percentage change in market capitalization over the last 24 hours. + */ + marketCapPercentChange1d: number; + /** + * The current price of the token. + */ + price: number; + /** + * The absolute change in price over the last 24 hours. + */ + priceChange1d: number; + /** + * The percentage change in price over the last 24 hours. + */ + pricePercentChange1d: number; + /** + * The percentage change in price over the last hour. + */ + pricePercentChange1h: number; + /** + * The percentage change in price over the last year. + */ + pricePercentChange1y: number; + /** + * The percentage change in price over the last 7 days. + */ + pricePercentChange7d: number; + /** + * The percentage change in price over the last 14 days. + */ + pricePercentChange14d: number; + /** + * The percentage change in price over the last 30 days. + */ + pricePercentChange30d: number; + /** + * The percentage change in price over the last 200 days. + */ + pricePercentChange200d: number; + /** + * The total trading volume of the token in the last 24 hours. + */ + totalVolume: number; +}; + /** * An ideal token prices service. All implementations must confirm to this * interface. * * @template ChainId - A type union of valid arguments for the `chainId` * argument to `fetchTokenPrices`. - * @template TokenAddress - A type union of all token addresses. The reason this - * type parameter exists is so that we can guarantee that same addresses that - * `fetchTokenPrices` receives are the same addresses that shown up in the - * return value. * @template Currency - A type union of valid arguments for the `currency` * argument to `fetchTokenPrices`. */ export type AbstractTokenPricesService< ChainId extends Hex = Hex, - TokenAddress extends Hex = Hex, Currency extends string = string, > = Partial> & { /** @@ -78,43 +163,17 @@ export type AbstractTokenPricesService< * given addresses which are expected to live on the given chain. * * @param args - The arguments to this function. - * @param args.chainId - An EIP-155 chain ID. - * @param args.tokenAddresses - Addresses for tokens that live on the chain. + * @param args.assets - The assets to get prices for. * @param args.currency - The desired currency of the token prices. * @returns The prices for the requested tokens. */ - fetchTokenPrices({ - chainId, - tokenAddresses, + fetchTokenPricesV3({ + assets, currency, }: { - chainId: ChainId; - tokenAddresses: TokenAddress[]; + assets: EvmAssetAddressWithChain[]; currency: Currency; - }): Promise>>; - - // fetchTokenPricesV3({ - // assets, - // currency, - // }: { - // assets: ( - // | { address: Hex; chainId: Hex } - // | { address: CaipAssetType; chainId: CaipChainId } - // )[]; - // currency: SupportedCurrency; - // }): (MarketData & { - // assetId: CaipAssetType; - // currency: SupportedCurrency; - // } & ( - // | { - // address: Hex; - // chainId: Hex; - // } - // | { - // address: CaipAssetType; - // chainId: CaipChainId; - // } - // ))[]; + }): Promise[]>; /** * Retrieves exchange rates in the given currency. 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 422d83d8ec0..1c73eced586 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -1,5 +1,4 @@ import { - ChainId, createServicePolicy, DEFAULT_CIRCUIT_BREAK_DURATION, DEFAULT_DEGRADED_THRESHOLD, @@ -8,29 +7,23 @@ import { handleFetch, } from '@metamask/controller-utils'; import type { ServicePolicy } from '@metamask/controller-utils'; -import type { - CaipAssetReference, - CaipAssetType, - CaipChainId, - Hex, -} from '@metamask/utils'; +import type { CaipAssetType, Hex } from '@metamask/utils'; import { hexToNumber, - isCaipAssetId, - isCaipAssetType, - isHexString, - isStrictHexString, KnownCaipNamespace, toCaipChainId, } from '@metamask/utils'; import type { AbstractTokenPricesService, + EvmAssetAddressWithChain, + EvmAssetWithId, + EvmAssetWithMarketData, ExchangeRatesByCurrency, + MarketData, TokenPrice, TokenPricesByTokenAddress, } from './abstract-token-prices-service'; -import { accountAddressToCaipReference } from 'src/assetsUtil'; /** * The list of currencies that can be supplied as the `vsCurrency` parameter to @@ -332,16 +325,6 @@ export const SUPPORTED_CHAIN_IDS = [ */ type SupportedChainId = (typeof SUPPORTED_CHAIN_IDS)[number]; -const SLIP44_CHAIN_MAP: Record = { - ETH: 'slip44:60', - POL: 'slip44:966', - BNB: 'slip44:714', - AVAX: 'slip44:9000', - TESTETH: 'slip44:60', - SEI: 'slip44:19000118', - MON: 'slip44:268435779', -}; - /** * All requests to V2 of the Price API start with this. */ @@ -351,92 +334,13 @@ const BASE_URL_V1 = 'https://price.api.cx.metamask.io/v1'; const BASE_URL_V3 = 'https://price.api.cx.metamask.io/v3'; -/** - * The shape of the data that the /spot-prices endpoint returns. - */ -type MarketData = { - /** - * The all-time highest price of the token. - */ - allTimeHigh: number; - /** - * The all-time lowest price of the token. - */ - allTimeLow: number; - /** - * The number of tokens currently in circulation. - */ - circulatingSupply: number; - /** - * The market cap calculated using the diluted supply. - */ - dilutedMarketCap: number; - /** - * The highest price of the token in the last 24 hours. - */ - high1d: number; - /** - * The lowest price of the token in the last 24 hours. - */ - low1d: number; - /** - * The current market capitalization of the token. - */ - marketCap: number; - /** - * The percentage change in market capitalization over the last 24 hours. - */ - marketCapPercentChange1d: number; - /** - * The current price of the token. - */ - price: number; - /** - * The absolute change in price over the last 24 hours. - */ - priceChange1d: number; - /** - * The percentage change in price over the last 24 hours. - */ - pricePercentChange1d: number; - /** - * The percentage change in price over the last hour. - */ - pricePercentChange1h: number; - /** - * The percentage change in price over the last year. - */ - pricePercentChange1y: number; - /** - * The percentage change in price over the last 7 days. - */ - pricePercentChange7d: number; - /** - * The percentage change in price over the last 14 days. - */ - pricePercentChange14d: number; - /** - * The percentage change in price over the last 30 days. - */ - pricePercentChange30d: number; - /** - * The percentage change in price over the last 200 days. - */ - pricePercentChange200d: number; - /** - * The total trading volume of the token in the last 24 hours. - */ - totalVolume: number; -}; - type MarketDataByTokenAddress = { [address: Hex]: MarketData }; /** * This version of the token prices service uses V2 of the Codefi Price API to * fetch token prices. */ export class CodefiTokenPricesServiceV2 - implements - AbstractTokenPricesService + implements AbstractTokenPricesService { readonly #policy: ServicePolicy; @@ -558,7 +462,7 @@ export class CodefiTokenPricesServiceV2 chainId: SupportedChainId; tokenAddresses: Hex[]; currency: SupportedCurrency; - }): Promise>> { + }): Promise>> { const chainIdAsNumber = hexToNumber(chainId); const url = new URL(`${BASE_URL}/chains/${chainIdAsNumber}/spot-prices`); @@ -576,7 +480,7 @@ export class CodefiTokenPricesServiceV2 return [getNativeTokenAddress(chainId), ...tokenAddresses].reduce( ( - obj: Partial>, + obj: Partial>, tokenAddress, ) => { // The Price API lowercases both currency and token addresses, so we have @@ -590,7 +494,7 @@ export class CodefiTokenPricesServiceV2 return obj; } - const token: TokenPrice = { + const token: TokenPrice = { tokenAddress, currency, ...marketData, @@ -602,45 +506,33 @@ export class CodefiTokenPricesServiceV2 }; }, {}, - ) as Partial>; + ) as Partial>; } async fetchTokenPricesV3({ assets, currency, }: { - assets: ( - | { address: Hex; chainId: Hex } - | { address: CaipAssetType; chainId: CaipChainId } - )[]; + assets: EvmAssetAddressWithChain[]; currency: SupportedCurrency; - }) { - const assetsWithIds: ({ assetId: CaipAssetType } & ( - | { address: Hex; chainId: Hex } - | { address: CaipAssetType; chainId: CaipChainId } - ))[] = assets.map((asset) => { - if (isStrictHexString(asset.address)) { + }): Promise[]> { + const assetsWithIds: EvmAssetWithId[] = assets.map( + (asset) => { const caipChainId = toCaipChainId( KnownCaipNamespace.Eip155, hexToNumber(asset.chainId).toString(), ); const nativeAsset = - HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP[asset.chainId as Hex]; + HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP[asset.chainId]; return { assetId: nativeAsset ?? `${caipChainId}/erc20:${asset.address}`, address: asset.address, - chainId: asset.chainId as Hex, + chainId: asset.chainId, }; - } - - return { - assetId: asset.address, - address: asset.address, - chainId: asset.chainId as CaipChainId, - }; - }); + }, + ); const url = new URL(`${BASE_URL_V3}/spot-prices`); url.searchParams.append( @@ -669,19 +561,7 @@ export class CodefiTokenPricesServiceV2 ...marketData, }; }) - .filter(Boolean) as (MarketData & { - assetId: CaipAssetType; - currency: SupportedCurrency; - } & ( - | { - address: Hex; - chainId: Hex; - } - | { - address: CaipAssetType; - chainId: CaipChainId; - } - ))[]; + .filter((x) => x !== undefined); } /** From 30c4ebb62fb973029a8a43a34a8429fac50fdf27 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Wed, 12 Nov 2025 11:06:17 +0000 Subject: [PATCH 03/42] update references --- .../src/TokenRatesController.ts | 39 +++++++---- .../TokenSearchDiscoveryDataController.ts | 11 +--- .../types.ts | 4 +- packages/assets-controllers/src/assetsUtil.ts | 22 +++++-- .../abstract-token-prices-service.ts | 51 ++++----------- .../src/token-prices-service/codefi-v2.ts | 64 +------------------ 6 files changed, 64 insertions(+), 127 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 8163ab3be20..5e7421c705e 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -25,7 +25,10 @@ import { isEqual } from 'lodash'; import { reduceInBatchesSerially, TOKEN_PRICES_BATCH_SIZE } from './assetsUtil'; import { fetchExchangeRate as fetchNativeCurrencyExchangeRate } from './crypto-compare-service'; -import type { AbstractTokenPricesService } from './token-prices-service/abstract-token-prices-service'; +import type { + AbstractTokenPricesService, + EvmAssetWithMarketData, +} from './token-prices-service/abstract-token-prices-service'; import { getNativeTokenAddress } from './token-prices-service/codefi-v2'; import type { TokensControllerGetStateAction, @@ -703,17 +706,26 @@ export class TokenRatesController extends StaticIntervalPollingController> + Record >({ values: [...tokenAddresses].sort(), batchSize: TOKEN_PRICES_BATCH_SIZE, eachBatch: async (allTokenPricesByTokenAddress, batch) => { - const tokenPricesByTokenAddressForBatch = + const tokenPricesByTokenAddressForBatch = ( await this.#tokenPricesService.fetchTokenPrices({ - tokenAddresses: batch, - chainId, + assets: batch.map((address) => ({ + chainId, + address, + })), currency: nativeCurrency, - }); + }) + ).reduce( + (acc, tokenPrice) => { + acc[tokenPrice.address] = tokenPrice; + return acc; + }, + {} as Record, + ); return { ...allTokenPricesByTokenAddress, @@ -726,18 +738,19 @@ export class TokenRatesController extends StaticIntervalPollingController | null> { + async #fetchPriceData(chainId: Hex, address: string) { const { currentCurrency } = this.messenger.call( 'CurrencyRateController:getState', ); try { const pricesData = await this.#tokenPricesService.fetchTokenPrices({ - chainId, - tokenAddresses: [address as Hex], + assets: [{ chainId, address: address as Hex }], currency: currentCurrency, }); - return pricesData[address as Hex] ?? null; + return pricesData[0] ?? null; } catch (error) { console.error(error); return null; diff --git a/packages/assets-controllers/src/TokenSearchDiscoveryDataController/types.ts b/packages/assets-controllers/src/TokenSearchDiscoveryDataController/types.ts index 7f092b58bbe..26b482ef141 100644 --- a/packages/assets-controllers/src/TokenSearchDiscoveryDataController/types.ts +++ b/packages/assets-controllers/src/TokenSearchDiscoveryDataController/types.ts @@ -1,6 +1,6 @@ import type { Hex } from '@metamask/utils'; -import type { TokenPrice } from '../token-prices-service/abstract-token-prices-service'; +import type { EvmAssetWithMarketData } from '../token-prices-service/abstract-token-prices-service'; import type { Token } from '../TokenRatesController'; export type NotFoundTokenDisplayData = { @@ -16,7 +16,7 @@ export type FoundTokenDisplayData = { address: string; currency: string; token: Token; - price: TokenPrice | null; + price: EvmAssetWithMarketData | null; }; export type TokenDisplayData = NotFoundTokenDisplayData | FoundTokenDisplayData; diff --git a/packages/assets-controllers/src/assetsUtil.ts b/packages/assets-controllers/src/assetsUtil.ts index 6827486f631..8c2b22b1486 100644 --- a/packages/assets-controllers/src/assetsUtil.ts +++ b/packages/assets-controllers/src/assetsUtil.ts @@ -15,7 +15,8 @@ import { CID } from 'multiformats/cid'; import type { Nft, NftMetadata } from './NftController'; import type { AbstractTokenPricesService } from './token-prices-service'; -import { type ContractExchangeRates } from './TokenRatesController'; +import type { EvmAssetWithMarketData } from './token-prices-service/abstract-token-prices-service'; +import type { ContractExchangeRates } from './TokenRatesController'; /** * The maximum number of token addresses that should be sent to the Price API in @@ -370,17 +371,26 @@ export async function fetchTokenContractExchangeRates({ const tokenPricesByTokenAddress = await reduceInBatchesSerially< Hex, - Awaited> + Record >({ values: [...tokenAddresses].sort(), batchSize: TOKEN_PRICES_BATCH_SIZE, eachBatch: async (allTokenPricesByTokenAddress, batch) => { - const tokenPricesByTokenAddressForBatch = + const tokenPricesByTokenAddressForBatch = ( await tokenPricesService.fetchTokenPrices({ - tokenAddresses: batch, - chainId, + assets: batch.map((address) => ({ + chainId, + address, + })), currency: nativeCurrency, - }); + }) + ).reduce( + (acc, tokenPrice) => { + acc[tokenPrice.address] = tokenPrice; + return acc; + }, + {} as Record, + ); return { ...allTokenPricesByTokenAddress, 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 3b430f3254b..2fdd334392a 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 @@ -1,32 +1,6 @@ import type { ServicePolicy } from '@metamask/controller-utils'; import type { CaipAssetType, Hex } from '@metamask/utils'; -/** - * Represents the price of a token in a currency. - */ -export type TokenPrice = { - tokenAddress: Hex; - currency: Currency; - allTimeHigh: number; - allTimeLow: number; - circulatingSupply: number; - dilutedMarketCap: number; - high1d: number; - low1d: number; - marketCap: number; - marketCapPercentChange1d: number; - price: number; - priceChange1d: number; - pricePercentChange1d: number; - pricePercentChange1h: number; - pricePercentChange1y: number; - pricePercentChange7d: number; - pricePercentChange14d: number; - pricePercentChange30d: number; - pricePercentChange200d: number; - totalVolume: number; -}; - /** * Represents an exchange rate. */ @@ -38,12 +12,15 @@ export type ExchangeRate = { usd?: number; }; -/** - * A map of token address to its price. - */ -export type TokenPricesByTokenAddress = { - [A in Hex]: TokenPrice; -}; +// /** +// * A map of token address to its price. +// */ +// export type TokenPricesByTokenAddress< +// ChainId extends Hex = Hex, +// Currency extends string = string, +// > = { +// [A in Hex]: EvmAssetWithMarketData; +// }; /** * A map of currency to its exchange rate. @@ -52,19 +29,19 @@ export type ExchangeRatesByCurrency = { [C in Currency]: ExchangeRate; }; -export type EvmAssetAddressWithChain = { +export type EvmAssetAddressWithChain = { address: Hex; chainId: ChainId; }; -export type EvmAssetWithId = +export type EvmAssetWithId = EvmAssetAddressWithChain & { assetId: CaipAssetType; }; export type EvmAssetWithMarketData< - ChainId extends Hex, - Currency extends string, + ChainId extends Hex = Hex, + Currency extends string = string, > = EvmAssetWithId & MarketData & { currency: Currency }; /** @@ -167,7 +144,7 @@ export type AbstractTokenPricesService< * @param args.currency - The desired currency of the token prices. * @returns The prices for the requested tokens. */ - fetchTokenPricesV3({ + fetchTokenPrices({ assets, currency, }: { 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 1c73eced586..8e1755b3c52 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -21,8 +21,6 @@ import type { EvmAssetWithMarketData, ExchangeRatesByCurrency, MarketData, - TokenPrice, - TokenPricesByTokenAddress, } from './abstract-token-prices-service'; /** @@ -449,67 +447,11 @@ export class CodefiTokenPricesServiceV2 * given addresses which are expected to live on the given chain. * * @param args - The arguments to function. - * @param args.chainId - An EIP-155 chain ID. - * @param args.tokenAddresses - Addresses for tokens that live on the chain. + * @param args.assets - The assets to get prices for. * @param args.currency - The desired currency of the token prices. * @returns The prices for the requested tokens. */ async fetchTokenPrices({ - chainId, - tokenAddresses, - currency, - }: { - chainId: SupportedChainId; - tokenAddresses: Hex[]; - currency: SupportedCurrency; - }): Promise>> { - const chainIdAsNumber = hexToNumber(chainId); - - const url = new URL(`${BASE_URL}/chains/${chainIdAsNumber}/spot-prices`); - url.searchParams.append( - 'tokenAddresses', - [getNativeTokenAddress(chainId), ...tokenAddresses].join(','), - ); - url.searchParams.append('vsCurrency', currency); - url.searchParams.append('includeMarketData', 'true'); - - const addressCryptoDataMap: MarketDataByTokenAddress = - await this.#policy.execute(() => - handleFetch(url, { headers: { 'Cache-Control': 'no-cache' } }), - ); - - return [getNativeTokenAddress(chainId), ...tokenAddresses].reduce( - ( - obj: Partial>, - tokenAddress, - ) => { - // The Price API lowercases both currency and token addresses, so we have - // to keep track of them and make sure we return the original versions. - const lowercasedTokenAddress = - tokenAddress.toLowerCase() as Lowercase; - - const marketData = addressCryptoDataMap[lowercasedTokenAddress]; - - if (!marketData) { - return obj; - } - - const token: TokenPrice = { - tokenAddress, - currency, - ...marketData, - }; - - return { - ...obj, - [tokenAddress]: token, - }; - }, - {}, - ) as Partial>; - } - - async fetchTokenPricesV3({ assets, currency, }: { @@ -557,11 +499,11 @@ export class CodefiTokenPricesServiceV2 return { ...assetWithId, - currency, ...marketData, + currency, }; }) - .filter((x) => x !== undefined); + .filter((entry): entry is NonNullable => Boolean(entry)); } /** From 63b470f1ff064407e065de1e1bea82eabcce97db Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Wed, 12 Nov 2025 11:15:43 +0000 Subject: [PATCH 04/42] fix test for lint --- packages/assets-controllers/src/CurrencyRateController.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/assets-controllers/src/CurrencyRateController.test.ts b/packages/assets-controllers/src/CurrencyRateController.test.ts index 7f520926af2..36fd9280ddf 100644 --- a/packages/assets-controllers/src/CurrencyRateController.test.ts +++ b/packages/assets-controllers/src/CurrencyRateController.test.ts @@ -44,7 +44,7 @@ function buildMockTokenPricesService( ): AbstractTokenPricesService { return { async fetchTokenPrices() { - return {}; + return []; }, async fetchExchangeRates() { return {}; From 87ee9adaec9078fef9fb6fda47c08cc9702e2bf8 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Wed, 12 Nov 2025 11:24:46 +0000 Subject: [PATCH 05/42] remove unused vars --- .../assets-controllers/src/token-prices-service/codefi-v2.ts | 3 --- 1 file changed, 3 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 8e1755b3c52..e0ee4bc5c37 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -326,13 +326,10 @@ type SupportedChainId = (typeof SUPPORTED_CHAIN_IDS)[number]; /** * All requests to V2 of the Price API start with this. */ -const BASE_URL = 'https://price.api.cx.metamask.io/v2'; - const BASE_URL_V1 = 'https://price.api.cx.metamask.io/v1'; const BASE_URL_V3 = 'https://price.api.cx.metamask.io/v3'; -type MarketDataByTokenAddress = { [address: Hex]: MarketData }; /** * This version of the token prices service uses V2 of the Codefi Price API to * fetch token prices. From 6534f68d7695ad2db25373adb4a41f3b18a4f688 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Wed, 12 Nov 2025 13:39:04 +0000 Subject: [PATCH 06/42] correct logic --- .../src/token-prices-service/codefi-v2.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 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 e0ee4bc5c37..a7b8f2df438 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -462,11 +462,13 @@ export class CodefiTokenPricesServiceV2 hexToNumber(asset.chainId).toString(), ); - const nativeAsset = - HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP[asset.chainId]; + const nativeAddress = getNativeTokenAddress(asset.chainId); return { - assetId: nativeAsset ?? `${caipChainId}/erc20:${asset.address}`, + assetId: + nativeAddress.toLowerCase() === asset.address.toLowerCase() + ? HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP[asset.chainId] + : `${caipChainId}/erc20:${asset.address}`, address: asset.address, chainId: asset.chainId, }; From a0a67cfb35b98289691febeeaf7d12445dfb8401 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Wed, 12 Nov 2025 15:00:48 +0000 Subject: [PATCH 07/42] fixes --- .../src/TokenRatesController.ts | 39 +++++++++---------- .../src/token-prices-service/codefi-v2.ts | 2 +- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 5e7421c705e..af568b66b4d 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -703,12 +703,11 @@ export class TokenRatesController extends StaticIntervalPollingController { - let contractNativeInformations; const tokenPricesByTokenAddress = await reduceInBatchesSerially< Hex, Record >({ - values: [...tokenAddresses].sort(), + values: [...tokenAddresses, getNativeTokenAddress(chainId)].sort(), batchSize: TOKEN_PRICES_BATCH_SIZE, eachBatch: async (allTokenPricesByTokenAddress, batch) => { const tokenPricesByTokenAddressForBatch = ( @@ -734,26 +733,26 @@ export class TokenRatesController extends StaticIntervalPollingController { obj = { ...obj, 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 a7b8f2df438..13d0c264a60 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -468,7 +468,7 @@ export class CodefiTokenPricesServiceV2 assetId: nativeAddress.toLowerCase() === asset.address.toLowerCase() ? HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP[asset.chainId] - : `${caipChainId}/erc20:${asset.address}`, + : `${caipChainId}/erc20:${asset.address.toLowerCase()}`, address: asset.address, chainId: asset.chainId, }; From 3c9e94559e435322d52d1b6a4eec831f265d5ba0 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Thu, 13 Nov 2025 16:44:43 +0000 Subject: [PATCH 08/42] fetch all chains together --- .../src/TokenRatesController.ts | 155 ++++++++++++------ .../TokenSearchDiscoveryDataController.ts | 2 +- packages/assets-controllers/src/assetsUtil.ts | 6 +- .../abstract-token-prices-service.ts | 84 +--------- .../src/token-prices-service/codefi-v2.ts | 71 ++++++-- 5 files changed, 172 insertions(+), 146 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index af568b66b4d..e22d26fb76f 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -95,6 +95,11 @@ export type MarketDataDetails = { */ export type ContractMarketData = Record; +type ChainIdAndNativeCurrency = { + chainId: Hex; + nativeCurrency: string; +}; + enum PollState { Active = 'Active', Inactive = 'Inactive', @@ -253,6 +258,8 @@ export class TokenRatesController extends StaticIntervalPollingController string; + #allTokens: TokensControllerState['allTokens']; #allDetectedTokens: TokensControllerState['allDetectedTokens']; @@ -266,6 +273,7 @@ export class TokenRatesController extends StaticIntervalPollingController; + getSelectedCurrency: () => string; }) { super({ name: controllerName, @@ -291,6 +301,7 @@ export class TokenRatesController extends StaticIntervalPollingController { + if (this.#disabled) { + return; + } + + const marketData: Record> = {}; + + const [supportedChains, unsupportedChains] = chainIds.reduce( + ([supported, unsupported], chainId) => { + if (this.#tokenPricesService.validateChainIdSupported(chainId)) { + supported.push(chainId); + } else { + unsupported.push(chainId); + } + return [supported, unsupported]; + }, + [[], []] as [Hex[], Hex[]], + ); + + for (const chainId of unsupportedChains) { + marketData[chainId] = {}; + } + + // TODO Decide what to do with unsupported currencies + + const assets = supportedChains.flatMap((chainId) => { + const tokenAddresses = this.#getTokenAddresses(chainId); + + return tokenAddresses.map((tokenAddress) => { + return { + chainId, + tokenAddress, + }; + }); + }); + + await reduceInBatchesSerially< + { chainId: Hex; tokenAddress: Hex }, + Record> + >({ + values: assets, + batchSize: TOKEN_PRICES_BATCH_SIZE, + eachBatch: async (partialMarketData, assetsBatch) => { + const batchMarketData = await this.#tokenPricesService.fetchTokenPrices( + { + assets: assetsBatch, + currency, + }, + ); + + for (const tokenPrice of batchMarketData) { + (partialMarketData[tokenPrice.chainId] ??= {})[ + tokenPrice.tokenAddress + ] = tokenPrice; + } + + return partialMarketData; + }, + initialResult: marketData, + }); + + if (Object.keys(marketData).length > 0) { + this.update((state) => { + state.marketData = { + ...state.marketData, + ...marketData, + }; + }); + } + } + /** * Updates exchange rates for all tokens. * @@ -518,10 +609,7 @@ export class TokenRatesController extends StaticIntervalPollingController { if (this.#disabled) { return; @@ -658,28 +746,9 @@ export class TokenRatesController extends StaticIntervalPollingController { - const { networkConfigurationsByChainId } = this.messenger.call( - 'NetworkController:getState', - ); - - const chainIdAndNativeCurrency = chainIds.reduce< - { chainId: Hex; nativeCurrency: string }[] - >((acc, chainId) => { - const networkConfiguration = networkConfigurationsByChainId[chainId]; - if (!networkConfiguration) { - console.error( - `TokenRatesController: No network configuration found for chainId ${chainId}`, - ); - return acc; - } - acc.push({ - chainId, - nativeCurrency: networkConfiguration.nativeCurrency, - }); - return acc; - }, []); + const selectedCurrency = this.#getSelectedCurrency(); - await this.updateExchangeRatesByChainId(chainIdAndNativeCurrency); + await this.updateExchangeRatesV3({ chainIds, currency: selectedCurrency }); } /** @@ -712,15 +781,15 @@ export class TokenRatesController extends StaticIntervalPollingController { const tokenPricesByTokenAddressForBatch = ( await this.#tokenPricesService.fetchTokenPrices({ - assets: batch.map((address) => ({ + assets: batch.map((tokenAddress) => ({ chainId, - address, + tokenAddress, })), currency: nativeCurrency, }) ).reduce( (acc, tokenPrice) => { - acc[tokenPrice.address] = tokenPrice; + acc[tokenPrice.tokenAddress] = tokenPrice; return acc; }, {} as Record, @@ -733,25 +802,7 @@ export class TokenRatesController extends StaticIntervalPollingController { obj = { diff --git a/packages/assets-controllers/src/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.ts b/packages/assets-controllers/src/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.ts index 00d1a8e6fe6..6036f2650b9 100644 --- a/packages/assets-controllers/src/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.ts +++ b/packages/assets-controllers/src/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.ts @@ -178,7 +178,7 @@ export class TokenSearchDiscoveryDataController extends BaseController< try { const pricesData = await this.#tokenPricesService.fetchTokenPrices({ - assets: [{ chainId, address: address as Hex }], + assets: [{ chainId, tokenAddress: address as Hex }], currency: currentCurrency, }); diff --git a/packages/assets-controllers/src/assetsUtil.ts b/packages/assets-controllers/src/assetsUtil.ts index 8c2b22b1486..6def2505e45 100644 --- a/packages/assets-controllers/src/assetsUtil.ts +++ b/packages/assets-controllers/src/assetsUtil.ts @@ -378,15 +378,15 @@ export async function fetchTokenContractExchangeRates({ eachBatch: async (allTokenPricesByTokenAddress, batch) => { const tokenPricesByTokenAddressForBatch = ( await tokenPricesService.fetchTokenPrices({ - assets: batch.map((address) => ({ + assets: batch.map((tokenAddress) => ({ chainId, - address, + tokenAddress, })), currency: nativeCurrency, }) ).reduce( (acc, tokenPrice) => { - acc[tokenPrice.address] = tokenPrice; + acc[tokenPrice.tokenAddress] = tokenPrice; return acc; }, {} as Record, 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 2fdd334392a..473023e93e7 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 @@ -1,6 +1,8 @@ import type { ServicePolicy } from '@metamask/controller-utils'; import type { CaipAssetType, Hex } from '@metamask/utils'; +import type { MarketDataDetails } from '../TokenRatesController'; + /** * Represents an exchange rate. */ @@ -30,7 +32,7 @@ export type ExchangeRatesByCurrency = { }; export type EvmAssetAddressWithChain = { - address: Hex; + tokenAddress: Hex; chainId: ChainId; }; @@ -42,85 +44,7 @@ export type EvmAssetWithId = export type EvmAssetWithMarketData< ChainId extends Hex = Hex, Currency extends string = string, -> = EvmAssetWithId & MarketData & { currency: Currency }; - -/** - * The shape of the data that the /spot-prices endpoint returns. - */ -export type MarketData = { - /** - * The all-time highest price of the token. - */ - allTimeHigh: number; - /** - * The all-time lowest price of the token. - */ - allTimeLow: number; - /** - * The number of tokens currently in circulation. - */ - circulatingSupply: number; - /** - * The market cap calculated using the diluted supply. - */ - dilutedMarketCap: number; - /** - * The highest price of the token in the last 24 hours. - */ - high1d: number; - /** - * The lowest price of the token in the last 24 hours. - */ - low1d: number; - /** - * The current market capitalization of the token. - */ - marketCap: number; - /** - * The percentage change in market capitalization over the last 24 hours. - */ - marketCapPercentChange1d: number; - /** - * The current price of the token. - */ - price: number; - /** - * The absolute change in price over the last 24 hours. - */ - priceChange1d: number; - /** - * The percentage change in price over the last 24 hours. - */ - pricePercentChange1d: number; - /** - * The percentage change in price over the last hour. - */ - pricePercentChange1h: number; - /** - * The percentage change in price over the last year. - */ - pricePercentChange1y: number; - /** - * The percentage change in price over the last 7 days. - */ - pricePercentChange7d: number; - /** - * The percentage change in price over the last 14 days. - */ - pricePercentChange14d: number; - /** - * The percentage change in price over the last 30 days. - */ - pricePercentChange30d: number; - /** - * The percentage change in price over the last 200 days. - */ - pricePercentChange200d: number; - /** - * The total trading volume of the token in the last 24 hours. - */ - totalVolume: number; -}; +> = EvmAssetWithId & MarketDataDetails & { currency: Currency }; /** * An ideal token prices service. All implementations must confirm to this 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 13d0c264a60..acf31eaecd7 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -20,8 +20,8 @@ import type { EvmAssetWithId, EvmAssetWithMarketData, ExchangeRatesByCurrency, - MarketData, } from './abstract-token-prices-service'; +import type { MarketDataDetails } from '../TokenRatesController'; /** * The list of currencies that can be supplied as the `vsCurrency` parameter to @@ -82,6 +82,8 @@ export const SUPPORTED_CURRENCIES = [ 'eur', // British Pound Sterling 'gbp', + // Georgian Lari + 'gel', // Hong Kong Dollar 'hkd', // Hungarian Forint @@ -150,6 +152,52 @@ export const SUPPORTED_CURRENCIES = [ 'bits', // Satoshi 'sats', + // Colombian Peso + 'cop', + // Kenyan Shilling + 'kes', + // Romanian Leu + 'ron', + // Dominican Peso + 'dop', + // Costa Rican Colón + 'crc', + // Honduran Lempira + 'hnl', + // Zambian Kwacha + 'zmw', + // Salvadoran Colón + 'svc', + // Bosnia-Herzegovina Convertible Mark + 'bam', + // Peruvian Sol + 'pen', + // Guatemalan Quetzal + 'gtq', + // Lebanese Pound + 'lbp', + // Armenian Dram + 'amd', + // Solana + 'sol', + // Sei + 'sei', + // Sonic + 'sonic', + // Tron + 'trx', + // Taiko + 'taiko', + // Pepu + 'pepu', + // Polygon + 'pol', + // Mantle + 'mnt', + // Onomy + 'nom', + // Avalanche + 'avax', ] as const; /** @@ -465,12 +513,11 @@ export class CodefiTokenPricesServiceV2 const nativeAddress = getNativeTokenAddress(asset.chainId); return { + ...asset, assetId: - nativeAddress.toLowerCase() === asset.address.toLowerCase() + nativeAddress.toLowerCase() === asset.tokenAddress.toLowerCase() ? HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP[asset.chainId] - : `${caipChainId}/erc20:${asset.address.toLowerCase()}`, - address: asset.address, - chainId: asset.chainId, + : `${caipChainId}/erc20:${asset.tokenAddress.toLowerCase()}`, }; }, ); @@ -483,10 +530,14 @@ export class CodefiTokenPricesServiceV2 url.searchParams.append('vsCurrency', currency); url.searchParams.append('includeMarketData', 'true'); - const addressCryptoDataMap: { [assetId: CaipAssetType]: MarketData } = - await this.#policy.execute(() => - handleFetch(url, { headers: { 'Cache-Control': 'no-cache' } }), - ); + const addressCryptoDataMap: { + [assetId: CaipAssetType]: Omit< + MarketDataDetails, + 'currency' | 'tokenAddress' + >; + } = await this.#policy.execute(() => + handleFetch(url, { headers: { 'Cache-Control': 'no-cache' } }), + ); return assetsWithIds .map((assetWithId) => { @@ -497,8 +548,8 @@ export class CodefiTokenPricesServiceV2 } return { - ...assetWithId, ...marketData, + ...assetWithId, currency, }; }) From 3a3fca81ee3699f92d1798cce801c58048950e7a Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Fri, 14 Nov 2025 09:31:04 +0000 Subject: [PATCH 09/42] update --- .../src/TokenRatesController.ts | 143 +++++++++++------- 1 file changed, 92 insertions(+), 51 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index e22d26fb76f..b185e9274d4 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -520,48 +520,31 @@ export class TokenRatesController extends StaticIntervalPollingController { + async updateExchangeRatesToCurrency(chainIds: Hex[]): Promise { if (this.#disabled) { return; } - const marketData: Record> = {}; + const currency = this.#getSelectedCurrency(); - const [supportedChains, unsupportedChains] = chainIds.reduce( - ([supported, unsupported], chainId) => { - if (this.#tokenPricesService.validateChainIdSupported(chainId)) { - supported.push(chainId); - } else { - unsupported.push(chainId); - } - return [supported, unsupported]; - }, - [[], []] as [Hex[], Hex[]], - ); - - for (const chainId of unsupportedChains) { - marketData[chainId] = {}; + const marketData: Record> = {}; + const assets: { + chainId: Hex; + tokenAddress: Hex; + }[] = []; + for (const chainId of chainIds) { + if (this.#tokenPricesService.validateChainIdSupported(chainId)) { + this.#getTokenAddresses(chainId).forEach((tokenAddress) => { + assets.push({ + chainId, + tokenAddress, + }); + }); + } else { + marketData[chainId] = {}; + } } - // TODO Decide what to do with unsupported currencies - - const assets = supportedChains.flatMap((chainId) => { - const tokenAddresses = this.#getTokenAddresses(chainId); - - return tokenAddresses.map((tokenAddress) => { - return { - chainId, - tokenAddress, - }; - }); - }); - await reduceInBatchesSerially< { chainId: Hex; tokenAddress: Hex }, Record> @@ -597,6 +580,78 @@ export class TokenRatesController extends StaticIntervalPollingController { + if (this.#disabled) { + return; + } + + const { networkConfigurationsByChainId } = this.messenger.call( + 'NetworkController:getState', + ); + + const marketData: Record> = {}; + const assetsByNativeCurrency: Record< + string, + { + chainId: Hex; + tokenAddress: Hex; + }[] + > = {}; + for (const chainId of chainIds) { + if (this.#tokenPricesService.validateChainIdSupported(chainId)) { + const { nativeCurrency } = networkConfigurationsByChainId[chainId]; + + assetsByNativeCurrency[nativeCurrency] = []; + + this.#getTokenAddresses(chainId).forEach((tokenAddress) => { + assetsByNativeCurrency[nativeCurrency].push({ + chainId, + tokenAddress, + }); + }); + } else { + marketData[chainId] = {}; + } + } + + for (const [nativeCurrency, assets] of Object.entries( + assetsByNativeCurrency, + )) { + await reduceInBatchesSerially< + { chainId: Hex; tokenAddress: Hex }, + Record> + >({ + values: assets, + batchSize: TOKEN_PRICES_BATCH_SIZE, + eachBatch: async (partialMarketData, assetsBatch) => { + const batchMarketData = + await this.#tokenPricesService.fetchTokenPrices({ + assets: assetsBatch, + currency: nativeCurrency, + }); + + for (const tokenPrice of batchMarketData) { + (partialMarketData[tokenPrice.chainId] ??= {})[ + tokenPrice.tokenAddress + ] = tokenPrice; + } + + return partialMarketData; + }, + initialResult: marketData, + }); + } + + if (Object.keys(marketData).length > 0) { + this.update((state) => { + state.marketData = { + ...state.marketData, + ...marketData, + }; + }); + } + } + /** * Updates exchange rates for all tokens. * @@ -746,9 +801,7 @@ export class TokenRatesController extends StaticIntervalPollingController { - const selectedCurrency = this.#getSelectedCurrency(); - - await this.updateExchangeRatesV3({ chainIds, currency: selectedCurrency }); + await this.updateExchangeRatesToCurrency(chainIds); } /** @@ -772,7 +825,7 @@ export class TokenRatesController extends StaticIntervalPollingController { - const tokenPricesByTokenAddress = await reduceInBatchesSerially< + return await reduceInBatchesSerially< Hex, Record >({ @@ -802,18 +855,6 @@ export class TokenRatesController extends StaticIntervalPollingController { - obj = { - ...obj, - [tokenAddress]: { ...token }, - }; - - return obj; - }, - {}, - ); } /** From 4dee204755eddbd272a424d41179e40cf27f4ba2 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Fri, 14 Nov 2025 09:45:11 +0000 Subject: [PATCH 10/42] update references --- packages/assets-controllers/src/balances.ts | 23 +----------------- .../src/selectors/token-selectors.ts | 21 ++-------------- packages/bridge-controller/src/selectors.ts | 1 + .../src/utils/token.ts | 24 ++----------------- 4 files changed, 6 insertions(+), 63 deletions(-) diff --git a/packages/assets-controllers/src/balances.ts b/packages/assets-controllers/src/balances.ts index a1dbade67ea..856c3cc4a59 100644 --- a/packages/assets-controllers/src/balances.ts +++ b/packages/assets-controllers/src/balances.ts @@ -113,7 +113,6 @@ const isNonNaNNumber = (value: unknown): value is number => * @param tokenBalancesState - Token balances state. * @param tokensState - Tokens state. * @param tokenRatesState - Token rates state. - * @param currencyRateState - Currency rate state. * @param isEvmChainEnabled - Predicate to check EVM chain enablement. * @returns token calculation data */ @@ -122,7 +121,6 @@ function getEvmTokenBalances( tokenBalancesState: TokenBalancesControllerState, tokensState: TokensControllerState, tokenRatesState: TokenRatesControllerState, - currencyRateState: CurrencyRateState, isEvmChainEnabled: (chainId: Hex) => boolean, ) { const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' as Hex; @@ -171,14 +169,6 @@ function getEvmTokenBalances( return null; } - // Get conversion rate - const nativeToUserRate = - currencyRateState.currencyRates[tokenMarketData.currency] - ?.conversionRate; - if (!nativeToUserRate) { - return null; - } - // Calculate values let decimals = 18; if (!isNative && !isStakedNative) { @@ -195,9 +185,7 @@ function getEvmTokenBalances( } const userCurrencyValue = - (decimalBalance / Math.pow(10, decimals)) * - tokenMarketData.price * - nativeToUserRate; + (decimalBalance / Math.pow(10, decimals)) * tokenMarketData.price; return { userCurrencyValue, @@ -269,7 +257,6 @@ function getNonEvmAssetBalances( * @param tokenBalancesState - Token balances state. * @param tokensState - Tokens state. * @param tokenRatesState - Token rates state. - * @param currencyRateState - Currency rate state. * @param isEvmChainEnabled - Predicate to check EVM chain enablement. * @returns Total value in user currency. */ @@ -278,7 +265,6 @@ function sumEvmAccountBalanceInUserCurrency( tokenBalancesState: TokenBalancesControllerState, tokensState: TokensControllerState, tokenRatesState: TokenRatesControllerState, - currencyRateState: CurrencyRateState, isEvmChainEnabled: (chainId: Hex) => boolean, ): number { const tokenBalances = getEvmTokenBalances( @@ -286,7 +272,6 @@ function sumEvmAccountBalanceInUserCurrency( tokenBalancesState, tokensState, tokenRatesState, - currencyRateState, isEvmChainEnabled, ); return tokenBalances.reduce((a, b) => a + b.userCurrencyValue, 0); @@ -361,7 +346,6 @@ export function calculateBalanceForAllWallets( tokenBalancesState, tokensState, tokenRatesState, - currencyRateState, isEvmChainEnabled, ), nonEvm: (account: InternalAccount) => @@ -513,7 +497,6 @@ export function calculateBalanceChangeForAllWallets( tokenBalancesState, tokensState, tokenRatesState, - currencyRateState, isEvmChainEnabled, ), nonEvm: (account: InternalAccount) => @@ -598,7 +581,6 @@ export function calculateBalanceChangeForAllWallets( * @param tokenBalancesState - Token balances controller state. * @param tokensState - Tokens controller state. * @param tokenRatesState - Token rates controller state. - * @param currencyRateState - Currency rate controller state. * @param isEvmChainEnabled - Predicate that returns true if the EVM chain is enabled. * @returns Object with current and previous totals in user currency. */ @@ -608,7 +590,6 @@ function sumEvmAccountChangeForPeriod( tokenBalancesState: TokenBalancesControllerState, tokensState: TokensControllerState, tokenRatesState: TokenRatesControllerState, - currencyRateState: CurrencyRateState, isEvmChainEnabled: (chainId: Hex) => boolean, ): { current: number; previous: number } { const tokenBalances = getEvmTokenBalances( @@ -616,7 +597,6 @@ function sumEvmAccountChangeForPeriod( tokenBalancesState, tokensState, tokenRatesState, - currencyRateState, isEvmChainEnabled, ); @@ -756,7 +736,6 @@ export function calculateBalanceChangeForAccountGroup( tokenBalancesState, tokensState, tokenRatesState, - currencyRateState, isEvmChainEnabled, ), nonEvm: (account: InternalAccount) => diff --git a/packages/assets-controllers/src/selectors/token-selectors.ts b/packages/assets-controllers/src/selectors/token-selectors.ts index 123c26bb027..832c44b62ba 100644 --- a/packages/assets-controllers/src/selectors/token-selectors.ts +++ b/packages/assets-controllers/src/selectors/token-selectors.ts @@ -71,7 +71,6 @@ export type AssetListState = { allIgnoredTokens: TokensControllerState['allIgnoredTokens']; tokenBalances: TokenBalancesControllerState['tokenBalances']; marketData: TokenRatesControllerState['marketData']; - currencyRates: CurrencyRateState['currencyRates']; accountsAssets: MultichainAssetsControllerState['accountsAssets']; allIgnoredAssets: MultichainAssetsControllerState['allIgnoredAssets']; assetsMetadata: MultichainAssetsControllerState['assetsMetadata']; @@ -131,7 +130,6 @@ const selectAllEvmAccountNativeBalances = createAssetListSelector( selectAccountsToGroupIdMap, (state) => state.accountsByChainId, (state) => state.marketData, - (state) => state.currencyRates, (state) => state.currentCurrency, (state) => state.networkConfigurationsByChainId, ], @@ -139,7 +137,6 @@ const selectAllEvmAccountNativeBalances = createAssetListSelector( accountsMap, accountsByChainId, marketData, - currencyRates, currentCurrency, networkConfigurationsByChainId, ) => { @@ -181,7 +178,6 @@ const selectAllEvmAccountNativeBalances = createAssetListSelector( rawBalance, nativeToken.decimals, marketData, - currencyRates, chainId, nativeToken.address, ); @@ -224,7 +220,6 @@ const selectAllEvmAssets = createAssetListSelector( (state) => state.allIgnoredTokens, (state) => state.tokenBalances, (state) => state.marketData, - (state) => state.currencyRates, (state) => state.currentCurrency, ], ( @@ -233,7 +228,6 @@ const selectAllEvmAssets = createAssetListSelector( ignoredEvmTokens, tokenBalances, marketData, - currencyRates, currentCurrency, ) => { const groupAssets: AssetsByAccountGroup = {}; @@ -275,7 +269,6 @@ const selectAllEvmAssets = createAssetListSelector( rawBalance, token.decimals, marketData, - currencyRates, chainId, tokenAddress, ); @@ -484,7 +477,6 @@ function mergeAssets( * @param rawBalance - The balance of the token * @param decimals - The decimals of the token * @param marketData - The market data for the token - * @param currencyRates - The currency rates for the token * @param chainId - The chain id of the token * @param tokenAddress - The address of the token * @returns The price and currency of the token in the current currency. Returns undefined if the asset is not found in the market data or currency rates. @@ -493,7 +485,6 @@ function getFiatBalanceForEvmToken( rawBalance: Hex, decimals: number, marketData: TokenRatesControllerState['marketData'], - currencyRates: CurrencyRateState['currencyRates'], chainId: Hex, tokenAddress: Hex, ) { @@ -503,20 +494,12 @@ function getFiatBalanceForEvmToken( return undefined; } - const currencyRate = currencyRates[tokenMarketData.currency]; - - if (!currencyRate?.conversionRate) { - return undefined; - } - const fiatBalance = - (convertHexToDecimal(rawBalance) / 10 ** decimals) * - tokenMarketData.price * - currencyRate.conversionRate; + (convertHexToDecimal(rawBalance) / 10 ** decimals) * tokenMarketData.price; return { balance: fiatBalance, - conversionRate: currencyRate.conversionRate, + conversionRate: tokenMarketData.price, }; } diff --git a/packages/bridge-controller/src/selectors.ts b/packages/bridge-controller/src/selectors.ts index 904c5ad44a7..519f9e7bc08 100644 --- a/packages/bridge-controller/src/selectors.ts +++ b/packages/bridge-controller/src/selectors.ts @@ -158,6 +158,7 @@ const getExchangeRateByChainIdAndAddress = ( const evmNativeExchangeRate = currencyRates?.[symbol]; if (evmNativeExchangeRate) { return { + // TODO This needs to be updated to use market data instead of currency rates exchangeRate: evmNativeExchangeRate?.conversionRate?.toString(), usdExchangeRate: evmNativeExchangeRate?.usdConversionRate?.toString(), }; diff --git a/packages/transaction-pay-controller/src/utils/token.ts b/packages/transaction-pay-controller/src/utils/token.ts index 6883ca5fd9c..9034c616903 100644 --- a/packages/transaction-pay-controller/src/utils/token.ts +++ b/packages/transaction-pay-controller/src/utils/token.ts @@ -161,10 +161,6 @@ export function getTokenFiatRate( const rateControllerState = messenger.call('TokenRatesController:getState'); - const currencyRateControllerState = messenger.call( - 'CurrencyRateController:getState', - ); - const normalizedTokenAddress = toChecksumHexAddress(tokenAddress) as Hex; const isNative = normalizedTokenAddress === getNativeToken(chainId); @@ -175,25 +171,9 @@ export function getTokenFiatRate( return undefined; } - const { - conversionRate: nativeToFiatRate, - usdConversionRate: nativeToUsdRate, - } = currencyRateControllerState.currencyRates?.[ticker] ?? { - conversionRate: null, - usdConversionRate: null, - }; - - if (nativeToFiatRate === null || nativeToUsdRate === null) { - return undefined; - } - - const usdRate = new BigNumber(tokenToNativeRate ?? 1) - .multipliedBy(nativeToUsdRate) - .toString(10); + const usdRate = new BigNumber(tokenToNativeRate ?? 1).toString(10); - const fiatRate = new BigNumber(tokenToNativeRate ?? 1) - .multipliedBy(nativeToFiatRate) - .toString(10); + const fiatRate = new BigNumber(tokenToNativeRate ?? 1).toString(10); return { usdRate, fiatRate }; } From 06da73addfc5bc5b63f80885d0dc775332cc0d6d Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Fri, 14 Nov 2025 09:56:53 +0000 Subject: [PATCH 11/42] update --- packages/bridge-controller/src/selectors.ts | 29 +++++++++------------ 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/packages/bridge-controller/src/selectors.ts b/packages/bridge-controller/src/selectors.ts index 519f9e7bc08..e074f559d4f 100644 --- a/packages/bridge-controller/src/selectors.ts +++ b/packages/bridge-controller/src/selectors.ts @@ -5,7 +5,7 @@ import type { TokenRatesControllerState, } from '@metamask/assets-controllers'; import type { GasFeeEstimates } from '@metamask/gas-fee-controller'; -import type { CaipAssetType } from '@metamask/utils'; +import type { CaipAssetType, Hex } from '@metamask/utils'; import { isStrictHexString } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; import { orderBy } from 'lodash'; @@ -154,34 +154,29 @@ const getExchangeRateByChainIdAndAddress = ( } // If the chain is an EVM chain, use the conversion rate from the currency rates controller if (isNativeAddress(address)) { - const { symbol } = getNativeAssetForChainId(chainId); - const evmNativeExchangeRate = currencyRates?.[symbol]; + const evmExchangeRates = marketData?.[formatChainIdToHex(chainId)]; + const evmNativeExchangeRate = isStrictHexString(address) + ? evmExchangeRates?.[address] + : null; if (evmNativeExchangeRate) { return { // TODO This needs to be updated to use market data instead of currency rates - exchangeRate: evmNativeExchangeRate?.conversionRate?.toString(), - usdExchangeRate: evmNativeExchangeRate?.usdConversionRate?.toString(), + exchangeRate: evmNativeExchangeRate?.price?.toString(), + usdExchangeRate: undefined, }; } return {}; } // If the chain is an EVM chain and the asset is not the native asset, use the conversion rate from the token rates controller if (!isNonEvmChainId(chainId)) { - const evmTokenExchangeRates = marketData?.[formatChainIdToHex(chainId)]; + const evmExchangeRates = marketData?.[formatChainIdToHex(chainId)]; const evmTokenExchangeRateForAddress = isStrictHexString(address) - ? evmTokenExchangeRates?.[address] + ? evmExchangeRates?.[address] : null; - const nativeCurrencyRate = evmTokenExchangeRateForAddress - ? currencyRates[evmTokenExchangeRateForAddress?.currency] - : undefined; - if (evmTokenExchangeRateForAddress && nativeCurrencyRate) { + if (evmTokenExchangeRateForAddress) { return { - exchangeRate: new BigNumber(evmTokenExchangeRateForAddress.price) - .multipliedBy(nativeCurrencyRate.conversionRate ?? 0) - .toString(), - usdExchangeRate: new BigNumber(evmTokenExchangeRateForAddress.price) - .multipliedBy(nativeCurrencyRate.usdConversionRate ?? 0) - .toString(), + exchangeRate: evmTokenExchangeRateForAddress?.price?.toString(), + usdExchangeRate: undefined, }; } } From 0b0654e2413e3576a8c6236c86a66b29945aa44e Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Fri, 14 Nov 2025 10:11:11 +0000 Subject: [PATCH 12/42] linting --- packages/bridge-controller/src/selectors.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/bridge-controller/src/selectors.ts b/packages/bridge-controller/src/selectors.ts index e074f559d4f..f79f610c007 100644 --- a/packages/bridge-controller/src/selectors.ts +++ b/packages/bridge-controller/src/selectors.ts @@ -5,7 +5,7 @@ import type { TokenRatesControllerState, } from '@metamask/assets-controllers'; import type { GasFeeEstimates } from '@metamask/gas-fee-controller'; -import type { CaipAssetType, Hex } from '@metamask/utils'; +import type { CaipAssetType } from '@metamask/utils'; import { isStrictHexString } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; import { orderBy } from 'lodash'; @@ -24,7 +24,6 @@ import type { } from './types'; import { RequestStatus, SortOrder } from './types'; import { - getNativeAssetForChainId, isEvmQuoteResponse, isNativeAddress, isNonEvmChainId, @@ -130,7 +129,7 @@ const getExchangeRateByChainIdAndAddress = ( return {}; } - const { assetExchangeRates, currencyRates, marketData, conversionRates } = + const { assetExchangeRates, marketData, conversionRates } = exchangeRateSources; // If the asset exchange rate is available in the bridge controller, use it From 23a05752e370fcc87a4f9f3880246e58dbc34f93 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Fri, 14 Nov 2025 13:19:20 +0000 Subject: [PATCH 13/42] revert breaking changes --- .../src/TokenRatesController.ts | 56 ++++++++++--------- packages/assets-controllers/src/balances.ts | 23 +++++++- .../src/selectors/token-selectors.ts | 21 ++++++- packages/bridge-controller/src/selectors.ts | 31 +++++----- .../src/utils/token.ts | 24 +++++++- 5 files changed, 110 insertions(+), 45 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index b185e9274d4..28b6f224433 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -614,33 +614,35 @@ export class TokenRatesController extends StaticIntervalPollingController> - >({ - values: assets, - batchSize: TOKEN_PRICES_BATCH_SIZE, - eachBatch: async (partialMarketData, assetsBatch) => { - const batchMarketData = - await this.#tokenPricesService.fetchTokenPrices({ - assets: assetsBatch, - currency: nativeCurrency, - }); - - for (const tokenPrice of batchMarketData) { - (partialMarketData[tokenPrice.chainId] ??= {})[ - tokenPrice.tokenAddress - ] = tokenPrice; - } - - return partialMarketData; + await Promise.allSettled( + Object.entries(assetsByNativeCurrency).map( + async ([nativeCurrency, assets]) => { + return await reduceInBatchesSerially< + { chainId: Hex; tokenAddress: Hex }, + Record> + >({ + values: assets, + batchSize: TOKEN_PRICES_BATCH_SIZE, + eachBatch: async (partialMarketData, assetsBatch) => { + const batchMarketData = + await this.#tokenPricesService.fetchTokenPrices({ + assets: assetsBatch, + currency: nativeCurrency, + }); + + for (const tokenPrice of batchMarketData) { + (partialMarketData[tokenPrice.chainId] ??= {})[ + tokenPrice.tokenAddress + ] = tokenPrice; + } + + return partialMarketData; + }, + initialResult: marketData, + }); }, - initialResult: marketData, - }); - } + ), + ); if (Object.keys(marketData).length > 0) { this.update((state) => { @@ -801,7 +803,7 @@ export class TokenRatesController extends StaticIntervalPollingController { - await this.updateExchangeRatesToCurrency(chainIds); + await this.updateExchangeRatesToNative(chainIds); } /** diff --git a/packages/assets-controllers/src/balances.ts b/packages/assets-controllers/src/balances.ts index 856c3cc4a59..a1dbade67ea 100644 --- a/packages/assets-controllers/src/balances.ts +++ b/packages/assets-controllers/src/balances.ts @@ -113,6 +113,7 @@ const isNonNaNNumber = (value: unknown): value is number => * @param tokenBalancesState - Token balances state. * @param tokensState - Tokens state. * @param tokenRatesState - Token rates state. + * @param currencyRateState - Currency rate state. * @param isEvmChainEnabled - Predicate to check EVM chain enablement. * @returns token calculation data */ @@ -121,6 +122,7 @@ function getEvmTokenBalances( tokenBalancesState: TokenBalancesControllerState, tokensState: TokensControllerState, tokenRatesState: TokenRatesControllerState, + currencyRateState: CurrencyRateState, isEvmChainEnabled: (chainId: Hex) => boolean, ) { const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' as Hex; @@ -169,6 +171,14 @@ function getEvmTokenBalances( return null; } + // Get conversion rate + const nativeToUserRate = + currencyRateState.currencyRates[tokenMarketData.currency] + ?.conversionRate; + if (!nativeToUserRate) { + return null; + } + // Calculate values let decimals = 18; if (!isNative && !isStakedNative) { @@ -185,7 +195,9 @@ function getEvmTokenBalances( } const userCurrencyValue = - (decimalBalance / Math.pow(10, decimals)) * tokenMarketData.price; + (decimalBalance / Math.pow(10, decimals)) * + tokenMarketData.price * + nativeToUserRate; return { userCurrencyValue, @@ -257,6 +269,7 @@ function getNonEvmAssetBalances( * @param tokenBalancesState - Token balances state. * @param tokensState - Tokens state. * @param tokenRatesState - Token rates state. + * @param currencyRateState - Currency rate state. * @param isEvmChainEnabled - Predicate to check EVM chain enablement. * @returns Total value in user currency. */ @@ -265,6 +278,7 @@ function sumEvmAccountBalanceInUserCurrency( tokenBalancesState: TokenBalancesControllerState, tokensState: TokensControllerState, tokenRatesState: TokenRatesControllerState, + currencyRateState: CurrencyRateState, isEvmChainEnabled: (chainId: Hex) => boolean, ): number { const tokenBalances = getEvmTokenBalances( @@ -272,6 +286,7 @@ function sumEvmAccountBalanceInUserCurrency( tokenBalancesState, tokensState, tokenRatesState, + currencyRateState, isEvmChainEnabled, ); return tokenBalances.reduce((a, b) => a + b.userCurrencyValue, 0); @@ -346,6 +361,7 @@ export function calculateBalanceForAllWallets( tokenBalancesState, tokensState, tokenRatesState, + currencyRateState, isEvmChainEnabled, ), nonEvm: (account: InternalAccount) => @@ -497,6 +513,7 @@ export function calculateBalanceChangeForAllWallets( tokenBalancesState, tokensState, tokenRatesState, + currencyRateState, isEvmChainEnabled, ), nonEvm: (account: InternalAccount) => @@ -581,6 +598,7 @@ export function calculateBalanceChangeForAllWallets( * @param tokenBalancesState - Token balances controller state. * @param tokensState - Tokens controller state. * @param tokenRatesState - Token rates controller state. + * @param currencyRateState - Currency rate controller state. * @param isEvmChainEnabled - Predicate that returns true if the EVM chain is enabled. * @returns Object with current and previous totals in user currency. */ @@ -590,6 +608,7 @@ function sumEvmAccountChangeForPeriod( tokenBalancesState: TokenBalancesControllerState, tokensState: TokensControllerState, tokenRatesState: TokenRatesControllerState, + currencyRateState: CurrencyRateState, isEvmChainEnabled: (chainId: Hex) => boolean, ): { current: number; previous: number } { const tokenBalances = getEvmTokenBalances( @@ -597,6 +616,7 @@ function sumEvmAccountChangeForPeriod( tokenBalancesState, tokensState, tokenRatesState, + currencyRateState, isEvmChainEnabled, ); @@ -736,6 +756,7 @@ export function calculateBalanceChangeForAccountGroup( tokenBalancesState, tokensState, tokenRatesState, + currencyRateState, isEvmChainEnabled, ), nonEvm: (account: InternalAccount) => diff --git a/packages/assets-controllers/src/selectors/token-selectors.ts b/packages/assets-controllers/src/selectors/token-selectors.ts index 832c44b62ba..123c26bb027 100644 --- a/packages/assets-controllers/src/selectors/token-selectors.ts +++ b/packages/assets-controllers/src/selectors/token-selectors.ts @@ -71,6 +71,7 @@ export type AssetListState = { allIgnoredTokens: TokensControllerState['allIgnoredTokens']; tokenBalances: TokenBalancesControllerState['tokenBalances']; marketData: TokenRatesControllerState['marketData']; + currencyRates: CurrencyRateState['currencyRates']; accountsAssets: MultichainAssetsControllerState['accountsAssets']; allIgnoredAssets: MultichainAssetsControllerState['allIgnoredAssets']; assetsMetadata: MultichainAssetsControllerState['assetsMetadata']; @@ -130,6 +131,7 @@ const selectAllEvmAccountNativeBalances = createAssetListSelector( selectAccountsToGroupIdMap, (state) => state.accountsByChainId, (state) => state.marketData, + (state) => state.currencyRates, (state) => state.currentCurrency, (state) => state.networkConfigurationsByChainId, ], @@ -137,6 +139,7 @@ const selectAllEvmAccountNativeBalances = createAssetListSelector( accountsMap, accountsByChainId, marketData, + currencyRates, currentCurrency, networkConfigurationsByChainId, ) => { @@ -178,6 +181,7 @@ const selectAllEvmAccountNativeBalances = createAssetListSelector( rawBalance, nativeToken.decimals, marketData, + currencyRates, chainId, nativeToken.address, ); @@ -220,6 +224,7 @@ const selectAllEvmAssets = createAssetListSelector( (state) => state.allIgnoredTokens, (state) => state.tokenBalances, (state) => state.marketData, + (state) => state.currencyRates, (state) => state.currentCurrency, ], ( @@ -228,6 +233,7 @@ const selectAllEvmAssets = createAssetListSelector( ignoredEvmTokens, tokenBalances, marketData, + currencyRates, currentCurrency, ) => { const groupAssets: AssetsByAccountGroup = {}; @@ -269,6 +275,7 @@ const selectAllEvmAssets = createAssetListSelector( rawBalance, token.decimals, marketData, + currencyRates, chainId, tokenAddress, ); @@ -477,6 +484,7 @@ function mergeAssets( * @param rawBalance - The balance of the token * @param decimals - The decimals of the token * @param marketData - The market data for the token + * @param currencyRates - The currency rates for the token * @param chainId - The chain id of the token * @param tokenAddress - The address of the token * @returns The price and currency of the token in the current currency. Returns undefined if the asset is not found in the market data or currency rates. @@ -485,6 +493,7 @@ function getFiatBalanceForEvmToken( rawBalance: Hex, decimals: number, marketData: TokenRatesControllerState['marketData'], + currencyRates: CurrencyRateState['currencyRates'], chainId: Hex, tokenAddress: Hex, ) { @@ -494,12 +503,20 @@ function getFiatBalanceForEvmToken( return undefined; } + const currencyRate = currencyRates[tokenMarketData.currency]; + + if (!currencyRate?.conversionRate) { + return undefined; + } + const fiatBalance = - (convertHexToDecimal(rawBalance) / 10 ** decimals) * tokenMarketData.price; + (convertHexToDecimal(rawBalance) / 10 ** decimals) * + tokenMarketData.price * + currencyRate.conversionRate; return { balance: fiatBalance, - conversionRate: tokenMarketData.price, + conversionRate: currencyRate.conversionRate, }; } diff --git a/packages/bridge-controller/src/selectors.ts b/packages/bridge-controller/src/selectors.ts index f79f610c007..904c5ad44a7 100644 --- a/packages/bridge-controller/src/selectors.ts +++ b/packages/bridge-controller/src/selectors.ts @@ -24,6 +24,7 @@ import type { } from './types'; import { RequestStatus, SortOrder } from './types'; import { + getNativeAssetForChainId, isEvmQuoteResponse, isNativeAddress, isNonEvmChainId, @@ -129,7 +130,7 @@ const getExchangeRateByChainIdAndAddress = ( return {}; } - const { assetExchangeRates, marketData, conversionRates } = + const { assetExchangeRates, currencyRates, marketData, conversionRates } = exchangeRateSources; // If the asset exchange rate is available in the bridge controller, use it @@ -153,29 +154,33 @@ const getExchangeRateByChainIdAndAddress = ( } // If the chain is an EVM chain, use the conversion rate from the currency rates controller if (isNativeAddress(address)) { - const evmExchangeRates = marketData?.[formatChainIdToHex(chainId)]; - const evmNativeExchangeRate = isStrictHexString(address) - ? evmExchangeRates?.[address] - : null; + const { symbol } = getNativeAssetForChainId(chainId); + const evmNativeExchangeRate = currencyRates?.[symbol]; if (evmNativeExchangeRate) { return { - // TODO This needs to be updated to use market data instead of currency rates - exchangeRate: evmNativeExchangeRate?.price?.toString(), - usdExchangeRate: undefined, + exchangeRate: evmNativeExchangeRate?.conversionRate?.toString(), + usdExchangeRate: evmNativeExchangeRate?.usdConversionRate?.toString(), }; } return {}; } // If the chain is an EVM chain and the asset is not the native asset, use the conversion rate from the token rates controller if (!isNonEvmChainId(chainId)) { - const evmExchangeRates = marketData?.[formatChainIdToHex(chainId)]; + const evmTokenExchangeRates = marketData?.[formatChainIdToHex(chainId)]; const evmTokenExchangeRateForAddress = isStrictHexString(address) - ? evmExchangeRates?.[address] + ? evmTokenExchangeRates?.[address] : null; - if (evmTokenExchangeRateForAddress) { + const nativeCurrencyRate = evmTokenExchangeRateForAddress + ? currencyRates[evmTokenExchangeRateForAddress?.currency] + : undefined; + if (evmTokenExchangeRateForAddress && nativeCurrencyRate) { return { - exchangeRate: evmTokenExchangeRateForAddress?.price?.toString(), - usdExchangeRate: undefined, + exchangeRate: new BigNumber(evmTokenExchangeRateForAddress.price) + .multipliedBy(nativeCurrencyRate.conversionRate ?? 0) + .toString(), + usdExchangeRate: new BigNumber(evmTokenExchangeRateForAddress.price) + .multipliedBy(nativeCurrencyRate.usdConversionRate ?? 0) + .toString(), }; } } diff --git a/packages/transaction-pay-controller/src/utils/token.ts b/packages/transaction-pay-controller/src/utils/token.ts index 9034c616903..6883ca5fd9c 100644 --- a/packages/transaction-pay-controller/src/utils/token.ts +++ b/packages/transaction-pay-controller/src/utils/token.ts @@ -161,6 +161,10 @@ export function getTokenFiatRate( const rateControllerState = messenger.call('TokenRatesController:getState'); + const currencyRateControllerState = messenger.call( + 'CurrencyRateController:getState', + ); + const normalizedTokenAddress = toChecksumHexAddress(tokenAddress) as Hex; const isNative = normalizedTokenAddress === getNativeToken(chainId); @@ -171,9 +175,25 @@ export function getTokenFiatRate( return undefined; } - const usdRate = new BigNumber(tokenToNativeRate ?? 1).toString(10); + const { + conversionRate: nativeToFiatRate, + usdConversionRate: nativeToUsdRate, + } = currencyRateControllerState.currencyRates?.[ticker] ?? { + conversionRate: null, + usdConversionRate: null, + }; + + if (nativeToFiatRate === null || nativeToUsdRate === null) { + return undefined; + } + + const usdRate = new BigNumber(tokenToNativeRate ?? 1) + .multipliedBy(nativeToUsdRate) + .toString(10); - const fiatRate = new BigNumber(tokenToNativeRate ?? 1).toString(10); + const fiatRate = new BigNumber(tokenToNativeRate ?? 1) + .multipliedBy(nativeToFiatRate) + .toString(10); return { usdRate, fiatRate }; } From 8949840c21a266069d9f0b7fa8ab2db5b51fea44 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Fri, 14 Nov 2025 13:26:08 +0000 Subject: [PATCH 14/42] add native token --- packages/assets-controllers/src/assetsUtil.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/assets-controllers/src/assetsUtil.ts b/packages/assets-controllers/src/assetsUtil.ts index 6def2505e45..8cfa5dd2b0f 100644 --- a/packages/assets-controllers/src/assetsUtil.ts +++ b/packages/assets-controllers/src/assetsUtil.ts @@ -14,7 +14,10 @@ import BN from 'bn.js'; import { CID } from 'multiformats/cid'; import type { Nft, NftMetadata } from './NftController'; -import type { AbstractTokenPricesService } from './token-prices-service'; +import { + getNativeTokenAddress, + type AbstractTokenPricesService, +} from './token-prices-service'; import type { EvmAssetWithMarketData } from './token-prices-service/abstract-token-prices-service'; import type { ContractExchangeRates } from './TokenRatesController'; @@ -373,7 +376,7 @@ export async function fetchTokenContractExchangeRates({ Hex, Record >({ - values: [...tokenAddresses].sort(), + values: [...tokenAddresses, getNativeTokenAddress(chainId)].sort(), batchSize: TOKEN_PRICES_BATCH_SIZE, eachBatch: async (allTokenPricesByTokenAddress, batch) => { const tokenPricesByTokenAddressForBatch = ( From 87cde78708141af5a4ff5c400303e59c752a0e70 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Fri, 14 Nov 2025 14:57:48 +0000 Subject: [PATCH 15/42] fix --- packages/assets-controllers/src/TokenRatesController.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 28b6f224433..3024190b6ed 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -601,10 +601,8 @@ export class TokenRatesController extends StaticIntervalPollingController { - assetsByNativeCurrency[nativeCurrency].push({ + (assetsByNativeCurrency[nativeCurrency] ??= []).push({ chainId, tokenAddress, }); From d816194507db5b718be5beeda0970656e76834d7 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Mon, 17 Nov 2025 11:19:45 +0000 Subject: [PATCH 16/42] refactor code --- .../src/TokenRatesController.ts | 414 ++---------------- 1 file changed, 28 insertions(+), 386 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 3024190b6ed..07cf5c23809 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -11,7 +11,6 @@ import type { import { safelyExecute, toChecksumHexAddress, - FALL_BACK_VS_CURRENCY, } from '@metamask/controller-utils'; import type { Messenger } from '@metamask/messenger'; import type { @@ -20,15 +19,11 @@ import type { NetworkControllerStateChangeEvent, } from '@metamask/network-controller'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; -import { createDeferredPromise, type Hex } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; import { isEqual } from 'lodash'; import { reduceInBatchesSerially, TOKEN_PRICES_BATCH_SIZE } from './assetsUtil'; -import { fetchExchangeRate as fetchNativeCurrencyExchangeRate } from './crypto-compare-service'; -import type { - AbstractTokenPricesService, - EvmAssetWithMarketData, -} from './token-prices-service/abstract-token-prices-service'; +import type { AbstractTokenPricesService } from './token-prices-service/abstract-token-prices-service'; import { getNativeTokenAddress } from './token-prices-service/codefi-v2'; import type { TokensControllerGetStateAction, @@ -174,43 +169,6 @@ export type TokenRatesControllerMessenger = Messenger< TokenRatesControllerEvents | AllowedEvents >; -/** - * Uses the CryptoCompare API to fetch the exchange rate between one currency - * and another, i.e., the multiplier to apply the amount of one currency in - * order to convert it to another. - * - * @param args - The arguments to this function. - * @param args.from - The currency to convert from. - * @param args.to - The currency to convert to. - * @returns The exchange rate between `fromCurrency` to `toCurrency` if one - * exists, or null if one does not. - */ -async function getCurrencyConversionRate({ - from, - to, -}: { - from: string; - to: string; -}) { - const includeUSDRate = false; - try { - const result = await fetchNativeCurrencyExchangeRate( - to, - from, - includeUSDRate, - ); - return result.conversionRate; - } catch (error) { - if ( - error instanceof Error && - error.message.includes('market does not exist for this coin pair') - ) { - return null; - } - throw error; - } -} - const tokenRatesControllerMetadata: StateMetadata = { marketData: { includeInStateLogs: false, @@ -252,14 +210,10 @@ export class TokenRatesController extends StaticIntervalPollingController> = {}; - #disabled: boolean; readonly #interval: number; - readonly #getSelectedCurrency: () => string; - #allTokens: TokensControllerState['allTokens']; #allDetectedTokens: TokensControllerState['allDetectedTokens']; @@ -273,7 +227,6 @@ export class TokenRatesController extends StaticIntervalPollingController; - getSelectedCurrency: () => string; }) { super({ name: controllerName, @@ -301,7 +252,6 @@ export class TokenRatesController extends StaticIntervalPollingController { - if (this.#disabled) { - return; - } - - const currency = this.#getSelectedCurrency(); - - const marketData: Record> = {}; - const assets: { - chainId: Hex; - tokenAddress: Hex; - }[] = []; - for (const chainId of chainIds) { - if (this.#tokenPricesService.validateChainIdSupported(chainId)) { - this.#getTokenAddresses(chainId).forEach((tokenAddress) => { - assets.push({ - chainId, - tokenAddress, - }); - }); - } else { - marketData[chainId] = {}; - } - } - - await reduceInBatchesSerially< - { chainId: Hex; tokenAddress: Hex }, - Record> - >({ - values: assets, - batchSize: TOKEN_PRICES_BATCH_SIZE, - eachBatch: async (partialMarketData, assetsBatch) => { - const batchMarketData = await this.#tokenPricesService.fetchTokenPrices( - { - assets: assetsBatch, - currency, - }, - ); - - for (const tokenPrice of batchMarketData) { - (partialMarketData[tokenPrice.chainId] ??= {})[ - tokenPrice.tokenAddress - ] = tokenPrice; - } - - return partialMarketData; - }, - initialResult: marketData, - }); - - if (Object.keys(marketData).length > 0) { - this.update((state) => { - state.marketData = { - ...state.marketData, - ...marketData, - }; - }); - } - } - - async updateExchangeRatesToNative(chainIds: Hex[]): Promise { - if (this.#disabled) { - return; - } - - const { networkConfigurationsByChainId } = this.messenger.call( - 'NetworkController:getState', - ); - + /** + * Updates exchange rates for all tokens. + * + * @param chainIdAndNativeCurrency - The chain ID and native currency. + */ + async updateExchangeRatesByChainId( + chainIdAndNativeCurrency: ChainIdAndNativeCurrency[], + ): Promise { const marketData: Record> = {}; const assetsByNativeCurrency: Record< string, @@ -597,10 +486,8 @@ export class TokenRatesController extends StaticIntervalPollingController = {}; - for (const chainId of chainIds) { + for (const { chainId, nativeCurrency } of chainIdAndNativeCurrency) { if (this.#tokenPricesService.validateChainIdSupported(chainId)) { - const { nativeCurrency } = networkConfigurationsByChainId[chainId]; - this.#getTokenAddresses(chainId).forEach((tokenAddress) => { (assetsByNativeCurrency[nativeCurrency] ??= []).push({ chainId, @@ -652,148 +539,6 @@ export class TokenRatesController extends StaticIntervalPollingController { - if (this.#disabled) { - return; - } - - // Create a promise for each chainId to fetch exchange rates. - const updatePromises = chainIdAndNativeCurrency.map( - async ({ chainId, nativeCurrency }) => { - const tokenAddresses = this.#getTokenAddresses(chainId); - // Build a unique key based on chainId, nativeCurrency, and the number of token addresses. - const updateKey: `${Hex}:${string}` = `${chainId}:${nativeCurrency}:${tokenAddresses.length}`; - - if (updateKey in this.#inProcessExchangeRateUpdates) { - // Await any ongoing update to avoid redundant work. - await this.#inProcessExchangeRateUpdates[updateKey]; - return null; - } - - // Create a deferred promise to track this update. - const { - promise: inProgressUpdate, - resolve: updateSucceeded, - reject: updateFailed, - } = createDeferredPromise({ suppressUnhandledRejection: true }); - this.#inProcessExchangeRateUpdates[updateKey] = inProgressUpdate; - - try { - const contractInformations = await this.#fetchAndMapExchangeRates({ - tokenAddresses, - chainId, - nativeCurrency, - }); - - // Each promise returns an object with the market data for the chain. - const marketData = { - [chainId]: { - ...(contractInformations ?? {}), - }, - }; - - updateSucceeded(); - return marketData; - } catch (error: unknown) { - updateFailed(error); - throw error; - } finally { - // Cleanup the tracking for this update. - delete this.#inProcessExchangeRateUpdates[updateKey]; - } - }, - ); - - // Wait for all update promises to settle. - const results = await Promise.allSettled(updatePromises); - - // Merge all successful market data updates into one object. - const combinedMarketData = results.reduce((acc, result) => { - if (result.status === 'fulfilled' && result.value) { - acc = { ...acc, ...result.value }; - } - return acc; - }, {}); - - // Call this.update only once with the combined market data to reduce the number of state changes and re-renders - if (Object.keys(combinedMarketData).length > 0) { - this.update((state) => { - state.marketData = { - ...state.marketData, - ...combinedMarketData, - }; - }); - } - } - - /** - * Uses the token prices service to retrieve exchange rates for tokens in a - * particular currency. - * - * If the price API does not support the given chain ID, returns an empty - * object. - * - * If the price API does not support the given currency, retrieves exchange - * rates in a known currency instead, then converts those rates using the - * exchange rate between the known currency and desired currency. - * - * @param args - The arguments to this function. - * @param args.tokenAddresses - Addresses for tokens. - * @param args.chainId - The EIP-155 ID of the chain where the tokens live. - * @param args.nativeCurrency - The native currency in which to request - * exchange rates. - * @returns A map from token address to its exchange rate in the native - * currency, or an empty map if no exchange rates can be obtained for the - * chain ID. - */ - async #fetchAndMapExchangeRates({ - tokenAddresses, - chainId, - nativeCurrency, - }: { - tokenAddresses: Hex[]; - chainId: Hex; - nativeCurrency: string; - }): Promise { - if (!this.#tokenPricesService.validateChainIdSupported(chainId)) { - return tokenAddresses.reduce((obj, tokenAddress) => { - obj = { - ...obj, - [tokenAddress]: undefined, - }; - - return obj; - }, {}); - } - - if (this.#tokenPricesService.validateCurrencySupported(nativeCurrency)) { - return await this.#fetchAndMapExchangeRatesForSupportedNativeCurrency({ - tokenAddresses, - chainId, - nativeCurrency, - }); - } - - return await this.#fetchAndMapExchangeRatesForUnsupportedNativeCurrency({ - chainId, - tokenAddresses, - nativeCurrency, - }); - } - /** * Updates token rates for the given networkClientId * @@ -801,131 +546,28 @@ export class TokenRatesController extends StaticIntervalPollingController { - await this.updateExchangeRatesToNative(chainIds); - } + const { networkConfigurationsByChainId } = this.messenger.call( + 'NetworkController:getState', + ); - /** - * Retrieves prices in the given currency for the given tokens on the given - * chain. Ensures that token addresses are checksum addresses. - * - * @param args - The arguments to this function. - * @param args.tokenAddresses - Addresses for tokens. - * @param args.chainId - The EIP-155 ID of the chain where the tokens live. - * @param args.nativeCurrency - The native currency in which to request - * prices. - * @returns A map of the token addresses (as checksums) to their prices in the - * native currency. - */ - async #fetchAndMapExchangeRatesForSupportedNativeCurrency({ - tokenAddresses, - chainId, - nativeCurrency, - }: { - tokenAddresses: Hex[]; - chainId: Hex; - nativeCurrency: string; - }): Promise { - return await reduceInBatchesSerially< - Hex, - Record - >({ - values: [...tokenAddresses, getNativeTokenAddress(chainId)].sort(), - batchSize: TOKEN_PRICES_BATCH_SIZE, - eachBatch: async (allTokenPricesByTokenAddress, batch) => { - const tokenPricesByTokenAddressForBatch = ( - await this.#tokenPricesService.fetchTokenPrices({ - assets: batch.map((tokenAddress) => ({ - chainId, - tokenAddress, - })), - currency: nativeCurrency, - }) - ).reduce( - (acc, tokenPrice) => { - acc[tokenPrice.tokenAddress] = tokenPrice; - return acc; - }, - {} as Record, + const chainIdAndNativeCurrency = chainIds.reduce< + { chainId: Hex; nativeCurrency: string }[] + >((acc, chainId) => { + const networkConfiguration = networkConfigurationsByChainId[chainId]; + if (!networkConfiguration) { + console.error( + `TokenRatesController: No network configuration found for chainId ${chainId}`, ); - - return { - ...allTokenPricesByTokenAddress, - ...tokenPricesByTokenAddressForBatch, - }; - }, - initialResult: {}, - }); - } - - /** - * If the price API does not support a given native currency, then we need to - * convert it to a fallback currency and feed that currency into the price - * API, then convert the prices to our desired native currency. - * - * @param args - The arguments to this function. - * @param args.chainId - The chain id to fetch prices for. - * @param args.tokenAddresses - Addresses for tokens. - * @param args.nativeCurrency - The native currency in which to request - * prices. - * @returns A map of the token addresses (as checksums) to their prices in the - * native currency. - */ - async #fetchAndMapExchangeRatesForUnsupportedNativeCurrency({ - chainId, - tokenAddresses, - nativeCurrency, - }: { - chainId: Hex; - tokenAddresses: Hex[]; - nativeCurrency: string; - }): Promise { - const [ - contractExchangeInformations, - fallbackCurrencyToNativeCurrencyConversionRate, - ] = await Promise.all([ - this.#fetchAndMapExchangeRatesForSupportedNativeCurrency({ - tokenAddresses, + return acc; + } + acc.push({ chainId, - nativeCurrency: FALL_BACK_VS_CURRENCY, - }), - getCurrencyConversionRate({ - from: FALL_BACK_VS_CURRENCY, - to: nativeCurrency, - }), - ]); - - if (fallbackCurrencyToNativeCurrencyConversionRate === null) { - return {}; - } - - // Converts the price in the fallback currency to the native currency - const convertFallbackToNative = (value: number | undefined) => - value !== undefined && value !== null - ? value * fallbackCurrencyToNativeCurrencyConversionRate - : undefined; - - const updatedContractExchangeRates = Object.entries( - contractExchangeInformations, - ).reduce((acc, [tokenAddress, token]) => { - acc = { - ...acc, - [tokenAddress]: { - ...token, - currency: nativeCurrency, - price: convertFallbackToNative(token.price), - marketCap: convertFallbackToNative(token.marketCap), - allTimeHigh: convertFallbackToNative(token.allTimeHigh), - allTimeLow: convertFallbackToNative(token.allTimeLow), - totalVolume: convertFallbackToNative(token.totalVolume), - high1d: convertFallbackToNative(token.high1d), - low1d: convertFallbackToNative(token.low1d), - dilutedMarketCap: convertFallbackToNative(token.dilutedMarketCap), - }, - }; + nativeCurrency: networkConfiguration.nativeCurrency, + }); return acc; - }, {}); + }, []); - return updatedContractExchangeRates; + await this.updateExchangeRatesByChainId(chainIdAndNativeCurrency); } /** From 82ca47dec6663974ab7c640077bca6631652dd7a Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Mon, 17 Nov 2025 11:23:46 +0000 Subject: [PATCH 17/42] remove comment --- .../abstract-token-prices-service.ts | 10 ---------- 1 file changed, 10 deletions(-) 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 473023e93e7..195bd3a6133 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 @@ -14,16 +14,6 @@ export type ExchangeRate = { usd?: number; }; -// /** -// * A map of token address to its price. -// */ -// export type TokenPricesByTokenAddress< -// ChainId extends Hex = Hex, -// Currency extends string = string, -// > = { -// [A in Hex]: EvmAssetWithMarketData; -// }; - /** * A map of currency to its exchange rate. */ From e7540ee41c3c9911d895f4442fa07ddcd7d462f2 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Mon, 17 Nov 2025 11:59:24 +0000 Subject: [PATCH 18/42] fix test --- .../assets-controllers/src/assetsUtil.test.ts | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/packages/assets-controllers/src/assetsUtil.test.ts b/packages/assets-controllers/src/assetsUtil.test.ts index e67bbb89280..2fc34104133 100644 --- a/packages/assets-controllers/src/assetsUtil.test.ts +++ b/packages/assets-controllers/src/assetsUtil.test.ts @@ -5,12 +5,15 @@ import { toHex, toChecksumHexAddress, } from '@metamask/controller-utils'; -import { add0x, type Hex } from '@metamask/utils'; +import { add0x, KnownCaipNamespace, type Hex } from '@metamask/utils'; 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 { + getNativeTokenAddress, + type AbstractTokenPricesService, +} from './token-prices-service'; const DEFAULT_IPFS_URL_FORMAT = 'ipfs://'; const ALTERNATIVE_IPFS_URL_FORMAT = 'ipfs://ipfs/'; @@ -622,9 +625,11 @@ describe('assetsUtil', () => { const testChainId = '0x1'; const mockPriceService = createMockPriceService(); - jest.spyOn(mockPriceService, 'fetchTokenPrices').mockResolvedValue({ - [testTokenAddress]: { + jest.spyOn(mockPriceService, 'fetchTokenPrices').mockResolvedValue([ + { tokenAddress: testTokenAddress, + chainId: testChainId, + assetId: `${KnownCaipNamespace.Eip155}:${testChainId}/erc20:${testTokenAddress}`, currency: testNativeCurrency, allTimeHigh: 4000, allTimeLow: 900, @@ -645,7 +650,7 @@ describe('assetsUtil', () => { priceChange1d: 100, pricePercentChange1d: 100, }, - }); + ]); const result = await assetsUtil.fetchTokenContractExchangeRates({ tokenPricesService: mockPriceService, @@ -685,13 +690,21 @@ describe('assetsUtil', () => { ); expect(fetchTokenPricesSpy).toHaveBeenCalledTimes(numBatches); + const tokenAddressesWithNativeToken = [ + getNativeTokenAddress(testChainId), + ...tokenAddresses, + ]; for (let i = 1; i <= numBatches; i++) { expect(fetchTokenPricesSpy).toHaveBeenNthCalledWith(i, { - chainId: testChainId, - tokenAddresses: tokenAddresses.slice( - (i - 1) * TOKEN_PRICES_BATCH_SIZE, - i * TOKEN_PRICES_BATCH_SIZE, - ), + assets: tokenAddressesWithNativeToken + .slice( + (i - 1) * TOKEN_PRICES_BATCH_SIZE, + i * TOKEN_PRICES_BATCH_SIZE, + ) + .map((tokenAddress) => ({ + chainId: testChainId, + tokenAddress, + })), currency: testNativeCurrency, }); } @@ -729,13 +742,21 @@ describe('assetsUtil', () => { ); expect(fetchTokenPricesSpy).toHaveBeenCalledTimes(numBatches); + const tokenAddressesWithNativeToken = [ + getNativeTokenAddress(testChainId), + ...tokenAddresses, + ]; for (let i = 1; i <= numBatches; i++) { expect(fetchTokenPricesSpy).toHaveBeenNthCalledWith(i, { - chainId: testChainId, - tokenAddresses: tokenAddresses.slice( - (i - 1) * TOKEN_PRICES_BATCH_SIZE, - i * TOKEN_PRICES_BATCH_SIZE, - ), + assets: tokenAddressesWithNativeToken + .slice( + (i - 1) * TOKEN_PRICES_BATCH_SIZE, + i * TOKEN_PRICES_BATCH_SIZE, + ) + .map((tokenAddress) => ({ + chainId: testChainId, + tokenAddress, + })), currency: testNativeCurrency, }); } @@ -779,7 +800,7 @@ function createMockPriceService(): AbstractTokenPricesService { return true; }, async fetchTokenPrices() { - return {}; + return []; }, async fetchExchangeRates() { return {}; From 00fbbadec80459320292015eb49e92c07bebba0f Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Mon, 17 Nov 2025 12:51:11 +0000 Subject: [PATCH 19/42] tests --- .../token-prices-service/codefi-v2.test.ts | 642 +++++++++--------- 1 file changed, 303 insertions(+), 339 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 964dbe669bc..e26a8a77985 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,3 +1,4 @@ +import { type Hex, KnownCaipNamespace } from '@metamask/utils'; import nock from 'nock'; import { useFakeTimers } from 'sinon'; @@ -31,10 +32,9 @@ describe('CodefiTokenPricesServiceV2', () => { const maximumConsecutiveFailures = (1 + retries) * 3; // Initial interceptor for failing requests nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') + .get('/v3/spot-prices') .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', + assetIds: buildMultipleAssetIds(['0xAAA', '0xBBB', '0xCCC']), vsCurrency: 'ETH', includeMarketData: 'true', }) @@ -42,36 +42,14 @@ describe('CodefiTokenPricesServiceV2', () => { .replyWithError('Failed to fetch'); // This interceptor should not be used nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') + .get('/v3/spot-prices') .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', + assetIds: buildMultipleAssetIds(['0xAAA', '0xBBB', '0xCCC']), vsCurrency: 'ETH', includeMarketData: 'true', }) .reply(200, { - '0x0000000000000000000000000000000000000000': { - price: 14, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xaaa': { + [buildTokenAssetId('0xAAA')]: { price: 148.17205755299946, currency: 'ETH', pricePercentChange1d: 1, @@ -92,7 +70,7 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - '0xbbb': { + [buildTokenAssetId('0xBBB')]: { price: 33689.98134554716, currency: 'ETH', pricePercentChange1d: 1, @@ -113,7 +91,7 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - '0xccc': { + [buildTokenAssetId('0xCCC')]: { price: 148.1344197578456, currency: 'ETH', pricePercentChange1d: 1, @@ -146,8 +124,20 @@ describe('CodefiTokenPricesServiceV2', () => { service.onBreak(onBreakHandler); const fetchTokenPrices = () => service.fetchTokenPrices({ - chainId: '0x1', - tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], + assets: [ + { + chainId: '0x1', + tokenAddress: '0xAAA', + }, + { + chainId: '0x1', + tokenAddress: '0xBBB', + }, + { + chainId: '0x1', + tokenAddress: '0xCCC', + }, + ], currency: 'ETH', }); expect(onBreakHandler).not.toHaveBeenCalled(); @@ -184,34 +174,27 @@ describe('CodefiTokenPricesServiceV2', () => { const degradedThreshold = 1000; const retries = 0; nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') + .get('/v3/spot-prices') .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', + assetIds: buildMultipleAssetIds(['0xAAA', '0xBBB', '0xCCC']), vsCurrency: 'ETH', includeMarketData: 'true', }) .delay(degradedThreshold * 2) .reply(200, { - '0x0000000000000000000000000000000000000000': { - price: 14, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - }, - '0xaaa': { + [buildTokenAssetId('0xAAA')]: { price: 148.17205755299946, currency: 'ETH', pricePercentChange1d: 1, priceChange1d: 1, }, - '0xbbb': { + [buildTokenAssetId('0xBBB')]: { price: 33689.98134554716, currency: 'ETH', pricePercentChange1d: 1, priceChange1d: 1, }, - '0xccc': { + [buildTokenAssetId('0xCCC')]: { price: 148.1344197578456, currency: 'ETH', pricePercentChange1d: 1, @@ -229,8 +212,20 @@ describe('CodefiTokenPricesServiceV2', () => { clock, fetchTokenPrices: () => service.fetchTokenPrices({ - chainId: '0x1', - tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], + assets: [ + { + chainId: '0x1', + tokenAddress: '0xAAA', + }, + { + chainId: '0x1', + tokenAddress: '0xBBB', + }, + { + chainId: '0x1', + tokenAddress: '0xCCC', + }, + ], currency: 'ETH', }), retries, @@ -243,36 +238,14 @@ describe('CodefiTokenPricesServiceV2', () => { describe('fetchTokenPrices', () => { it('uses the /spot-prices endpoint of the Codefi Price API to gather prices for the given tokens', async () => { nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') + .get('/v3/spot-prices') .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', + assetIds: buildMultipleAssetIds(['0xAAA', '0xBBB', '0xCCC']), vsCurrency: 'ETH', includeMarketData: 'true', }) .reply(200, { - '0x0000000000000000000000000000000000000000': { - price: 14, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xaaa': { + [buildTokenAssetId('0xAAA')]: { price: 148.17205755299946, currency: 'ETH', pricePercentChange1d: 1, @@ -293,7 +266,7 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - '0xbbb': { + [buildTokenAssetId('0xBBB')]: { price: 33689.98134554716, currency: 'ETH', pricePercentChange1d: 1, @@ -314,7 +287,7 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - '0xccc': { + [buildTokenAssetId('0xCCC')]: { price: 148.1344197578456, currency: 'ETH', pricePercentChange1d: 1, @@ -339,36 +312,28 @@ describe('CodefiTokenPricesServiceV2', () => { const marketDataTokensByAddress = await new CodefiTokenPricesServiceV2().fetchTokenPrices({ - chainId: '0x1', - tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], + assets: [ + { + chainId: '0x1', + tokenAddress: '0xAAA', + }, + { + chainId: '0x1', + tokenAddress: '0xBBB', + }, + { + chainId: '0x1', + tokenAddress: '0xCCC', + }, + ], currency: 'ETH', }); - expect(marketDataTokensByAddress).toStrictEqual({ - '0x0000000000000000000000000000000000000000': { - tokenAddress: '0x0000000000000000000000000000000000000000', - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - price: 14, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xAAA': { + expect(marketDataTokensByAddress).toStrictEqual([ + { tokenAddress: '0xAAA', + assetId: buildTokenAssetId('0xAAA'), + chainId: '0x1', currency: 'ETH', pricePercentChange1d: 1, priceChange1d: 1, @@ -389,8 +354,10 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - '0xBBB': { + { tokenAddress: '0xBBB', + assetId: buildTokenAssetId('0xBBB'), + chainId: '0x1', currency: 'ETH', pricePercentChange1d: 1, priceChange1d: 1, @@ -411,8 +378,10 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - '0xCCC': { + { tokenAddress: '0xCCC', + assetId: buildTokenAssetId('0xCCC'), + chainId: '0x1', currency: 'ETH', pricePercentChange1d: 1, priceChange1d: 1, @@ -433,86 +402,55 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - }); + ]); }); - it('calls the /spot-prices endpoint using the correct native token address', async () => { - const mockPriceAPI = nock('https://price.api.cx.metamask.io') - .get('/v2/chains/137/spot-prices') + it('handles native token addresses', async () => { + nock('https://price.api.cx.metamask.io') + .get('/v3/spot-prices') .query({ - tokenAddresses: '0x0000000000000000000000000000000000001010', + assetIds: buildMultipleAssetIds([ZERO_ADDRESS]), vsCurrency: 'ETH', includeMarketData: 'true', }) .reply(200, { - '0x0000000000000000000000000000000000001010': { - price: 14, + [buildTokenAssetId(ZERO_ADDRESS)]: { + price: 33689.98134554716, currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, }, }); - const marketData = - await new CodefiTokenPricesServiceV2().fetchTokenPrices({ - chainId: '0x89', - tokenAddresses: [], - currency: 'ETH', - }); + const result = await new CodefiTokenPricesServiceV2().fetchTokenPrices({ + assets: [ + { + chainId: '0x1', + tokenAddress: ZERO_ADDRESS, + }, + ], + currency: 'ETH', + }); - expect(mockPriceAPI.isDone()).toBe(true); - expect( - marketData['0x0000000000000000000000000000000000001010'], - ).toBeDefined(); + expect(result).toStrictEqual([ + { + tokenAddress: ZERO_ADDRESS, + assetId: buildTokenAssetId(ZERO_ADDRESS), + chainId: '0x1', + currency: 'ETH', + price: 33689.98134554716, + }, + ]); }); it('should not include token price object for token address when token price in not included the response data', async () => { nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') + .get('/v3/spot-prices') .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', + assetIds: buildMultipleAssetIds(['0xAAA', '0xBBB', '0xCCC']), vsCurrency: 'ETH', includeMarketData: 'true', }) .reply(200, { - '0x0000000000000000000000000000000000000000': { - price: 14, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xbbb': { + [buildTokenAssetId('0xBBB')]: { price: 33689.98134554716, currency: 'ETH', pricePercentChange1d: 1, @@ -533,7 +471,7 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - '0xccc': { + [buildTokenAssetId('0xCCC')]: { price: 148.1344197578456, currency: 'ETH', pricePercentChange1d: 1, @@ -557,35 +495,27 @@ describe('CodefiTokenPricesServiceV2', () => { }); const result = await new CodefiTokenPricesServiceV2().fetchTokenPrices({ - chainId: '0x1', - tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], + assets: [ + { + chainId: '0x1', + tokenAddress: '0xAAA', + }, + { + chainId: '0x1', + tokenAddress: '0xBBB', + }, + { + chainId: '0x1', + tokenAddress: '0xCCC', + }, + ], currency: 'ETH', }); - expect(result).toStrictEqual({ - '0x0000000000000000000000000000000000000000': { - tokenAddress: '0x0000000000000000000000000000000000000000', - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - price: 14, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xBBB': { + expect(result).toStrictEqual([ + { tokenAddress: '0xBBB', + assetId: buildTokenAssetId('0xBBB'), + chainId: '0x1', currency: 'ETH', pricePercentChange1d: 1, priceChange1d: 1, @@ -606,8 +536,10 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - '0xCCC': { + { tokenAddress: '0xCCC', + assetId: buildTokenAssetId('0xCCC'), + chainId: '0x1', currency: 'ETH', pricePercentChange1d: 1, priceChange1d: 1, @@ -628,21 +560,20 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - }); + ]); }); it('should not include token price object for token address when price is undefined for token response data', async () => { nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') + .get('/v3/spot-prices') .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', + assetIds: buildMultipleAssetIds(['0xAAA', '0xBBB', '0xCCC']), vsCurrency: 'ETH', includeMarketData: 'true', }) .reply(200, { - '0xaaa': {}, - '0xbbb': { + [buildTokenAssetId('0xAAA')]: {}, + [buildTokenAssetId('0xBBB')]: { price: 33689.98134554716, pricePercentChange1d: 1, priceChange1d: 1, @@ -662,7 +593,7 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - '0xccc': { + [buildTokenAssetId('0xCCC')]: { price: 148.1344197578456, pricePercentChange1d: 1, priceChange1d: 1, @@ -685,18 +616,34 @@ describe('CodefiTokenPricesServiceV2', () => { }); const result = await new CodefiTokenPricesServiceV2().fetchTokenPrices({ - chainId: '0x1', - tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], + assets: [ + { + chainId: '0x1', + tokenAddress: '0xAAA', + }, + { + chainId: '0x1', + tokenAddress: '0xBBB', + }, + { + chainId: '0x1', + tokenAddress: '0xCCC', + }, + ], currency: 'ETH', }); - expect(result).toStrictEqual({ - '0xAAA': { - currency: 'ETH', + expect(result).toStrictEqual([ + { tokenAddress: '0xAAA', + assetId: buildTokenAssetId('0xAAA'), + chainId: '0x1', + currency: 'ETH', }, - '0xBBB': { + { tokenAddress: '0xBBB', + assetId: buildTokenAssetId('0xBBB'), + chainId: '0x1', currency: 'ETH', pricePercentChange1d: 1, priceChange1d: 1, @@ -717,8 +664,10 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - '0xCCC': { + { tokenAddress: '0xCCC', + assetId: buildTokenAssetId('0xCCC'), + chainId: '0x1', currency: 'ETH', pricePercentChange1d: 1, priceChange1d: 1, @@ -739,65 +688,70 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - }); + ]); }); it('should correctly handle null market data for a token address', async () => { nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') + .get('/v3/spot-prices') .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', + assetIds: buildMultipleAssetIds(['0xAAA', '0xBBB', '0xCCC']), vsCurrency: 'ETH', includeMarketData: 'true', }) .reply(200, { - '0x0000000000000000000000000000000000000000': { - price: 14, - currency: 'ETH', - }, - '0xaaa': null, // Simulating API returning null for market data - '0xbbb': { + [buildTokenAssetId('0xAAA')]: null, // Simulating API returning null for market data + [buildTokenAssetId('0xBBB')]: { price: 33689.98134554716, currency: 'ETH', }, - '0xccc': { + [buildTokenAssetId('0xCCC')]: { price: 148.1344197578456, currency: 'ETH', }, }); const result = await new CodefiTokenPricesServiceV2().fetchTokenPrices({ - chainId: '0x1', - tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], + assets: [ + { + chainId: '0x1', + tokenAddress: '0xAAA', + }, + { + chainId: '0x1', + tokenAddress: '0xBBB', + }, + { + chainId: '0x1', + tokenAddress: '0xCCC', + }, + ], currency: 'ETH', }); - expect(result).toStrictEqual({ - '0x0000000000000000000000000000000000000000': { - tokenAddress: '0x0000000000000000000000000000000000000000', - currency: 'ETH', - price: 14, - }, - '0xBBB': { + expect(result).toStrictEqual([ + { tokenAddress: '0xBBB', + assetId: buildTokenAssetId('0xBBB'), + chainId: '0x1', currency: 'ETH', price: 33689.98134554716, }, - '0xCCC': { + { tokenAddress: '0xCCC', + assetId: buildTokenAssetId('0xCCC'), + chainId: '0x1', currency: 'ETH', price: 148.1344197578456, }, - }); + ]); }); it('throws if the request fails consistently', async () => { nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') + .get('/v3/spot-prices') .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', + assetIds: buildMultipleAssetIds(['0xAAA', '0xBBB', '0xCCC']), vsCurrency: 'ETH', includeMarketData: 'true', }) @@ -806,8 +760,20 @@ describe('CodefiTokenPricesServiceV2', () => { await expect( new CodefiTokenPricesServiceV2().fetchTokenPrices({ - chainId: '0x1', - tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], + assets: [ + { + chainId: '0x1', + tokenAddress: '0xAAA', + }, + { + chainId: '0x1', + tokenAddress: '0xBBB', + }, + { + chainId: '0x1', + tokenAddress: '0xCCC', + }, + ], currency: 'ETH', }), ).rejects.toThrow('Failed to fetch'); @@ -816,10 +782,9 @@ describe('CodefiTokenPricesServiceV2', () => { it('throws if the initial request and all retries fail', async () => { const retries = 3; nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') + .get('/v3/spot-prices') .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', + assetIds: buildMultipleAssetIds(['0xAAA', '0xBBB', '0xCCC']), vsCurrency: 'ETH', includeMarketData: 'true', }) @@ -828,8 +793,20 @@ describe('CodefiTokenPricesServiceV2', () => { await expect( new CodefiTokenPricesServiceV2({ retries }).fetchTokenPrices({ - chainId: '0x1', - tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], + assets: [ + { + chainId: '0x1', + tokenAddress: '0xAAA', + }, + { + chainId: '0x1', + tokenAddress: '0xBBB', + }, + { + chainId: '0x1', + tokenAddress: '0xCCC', + }, + ], currency: 'ETH', }), ).rejects.toThrow('Failed to fetch'); @@ -839,10 +816,9 @@ describe('CodefiTokenPricesServiceV2', () => { const retries = 3; // Initial interceptor for failing requests nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') + .get('/v3/spot-prices') .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', + assetIds: buildMultipleAssetIds(['0xAAA', '0xBBB', '0xCCC']), vsCurrency: 'ETH', includeMarketData: 'true', }) @@ -850,36 +826,14 @@ describe('CodefiTokenPricesServiceV2', () => { .replyWithError('Failed to fetch'); // Interceptor for successful request nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') + .get('/v3/spot-prices') .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', + assetIds: buildMultipleAssetIds(['0xAAA', '0xBBB', '0xCCC']), vsCurrency: 'ETH', includeMarketData: 'true', }) .reply(200, { - '0x0000000000000000000000000000000000000000': { - price: 14, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xaaa': { + [buildTokenAssetId('0xAAA')]: { price: 148.17205755299946, currency: 'ETH', pricePercentChange1d: 1, @@ -900,7 +854,7 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - '0xbbb': { + [buildTokenAssetId('0xBBB')]: { price: 33689.98134554716, currency: 'ETH', pricePercentChange1d: 1, @@ -921,7 +875,7 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - '0xccc': { + [buildTokenAssetId('0xCCC')]: { price: 148.1344197578456, currency: 'ETH', pricePercentChange1d: 1, @@ -947,36 +901,28 @@ describe('CodefiTokenPricesServiceV2', () => { const marketDataTokensByAddress = await new CodefiTokenPricesServiceV2({ retries, }).fetchTokenPrices({ - chainId: '0x1', - tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], + assets: [ + { + chainId: '0x1', + tokenAddress: '0xAAA', + }, + { + chainId: '0x1', + tokenAddress: '0xBBB', + }, + { + chainId: '0x1', + tokenAddress: '0xCCC', + }, + ], currency: 'ETH', }); - expect(marketDataTokensByAddress).toStrictEqual({ - '0x0000000000000000000000000000000000000000': { - tokenAddress: '0x0000000000000000000000000000000000000000', - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - price: 14, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xAAA': { + expect(marketDataTokensByAddress).toStrictEqual([ + { tokenAddress: '0xAAA', + assetId: buildTokenAssetId('0xAAA'), + chainId: '0x1', currency: 'ETH', pricePercentChange1d: 1, priceChange1d: 1, @@ -997,8 +943,10 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - '0xBBB': { + { tokenAddress: '0xBBB', + assetId: buildTokenAssetId('0xBBB'), + chainId: '0x1', currency: 'ETH', pricePercentChange1d: 1, priceChange1d: 1, @@ -1019,8 +967,10 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - '0xCCC': { + { tokenAddress: '0xCCC', + assetId: buildTokenAssetId('0xCCC'), + chainId: '0x1', currency: 'ETH', pricePercentChange1d: 1, priceChange1d: 1, @@ -1041,7 +991,7 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - }); + ]); }); describe('before circuit break', () => { @@ -1059,34 +1009,27 @@ describe('CodefiTokenPricesServiceV2', () => { const degradedThreshold = 1000; const retries = 0; nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') + .get('/v3/spot-prices') .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', + assetIds: buildMultipleAssetIds(['0xAAA', '0xBBB', '0xCCC']), vsCurrency: 'ETH', includeMarketData: 'true', }) .delay(degradedThreshold * 2) .reply(200, { - '0x0000000000000000000000000000000000000000': { - price: 14, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - }, - '0xaaa': { + [buildTokenAssetId('0xAAA')]: { price: 148.17205755299946, currency: 'ETH', pricePercentChange1d: 1, priceChange1d: 1, }, - '0xbbb': { + [buildTokenAssetId('0xBBB')]: { price: 33689.98134554716, currency: 'ETH', pricePercentChange1d: 1, priceChange1d: 1, }, - '0xccc': { + [buildTokenAssetId('0xCCC')]: { price: 148.1344197578456, currency: 'ETH', pricePercentChange1d: 1, @@ -1104,8 +1047,20 @@ describe('CodefiTokenPricesServiceV2', () => { clock, fetchTokenPrices: () => service.fetchTokenPrices({ - chainId: '0x1', - tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], + assets: [ + { + chainId: '0x1', + tokenAddress: '0xAAA', + }, + { + chainId: '0x1', + tokenAddress: '0xBBB', + }, + { + chainId: '0x1', + tokenAddress: '0xCCC', + }, + ], currency: 'ETH', }), retries, @@ -1132,10 +1087,9 @@ describe('CodefiTokenPricesServiceV2', () => { const maximumConsecutiveFailures = (1 + retries) * 3; // Initial interceptor for failing requests nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') + .get('/v3/spot-prices') .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', + assetIds: buildMultipleAssetIds(['0xAAA', '0xBBB', '0xCCC']), vsCurrency: 'ETH', includeMarketData: 'true', }) @@ -1143,36 +1097,14 @@ describe('CodefiTokenPricesServiceV2', () => { .replyWithError('Failed to fetch'); // This interceptor should not be used nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') + .get('/v3/spot-prices') .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', + assetIds: buildMultipleAssetIds(['0xAAA', '0xBBB', '0xCCC']), vsCurrency: 'ETH', includeMarketData: 'true', }) .reply(200, { - '0x0000000000000000000000000000000000000000': { - price: 14, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xaaa': { + [buildTokenAssetId('0xAAA')]: { price: 148.17205755299946, currency: 'ETH', pricePercentChange1d: 1, @@ -1193,7 +1125,7 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - '0xbbb': { + [buildTokenAssetId('0xBBB')]: { price: 33689.98134554716, currency: 'ETH', pricePercentChange1d: 1, @@ -1214,7 +1146,7 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange200d: 46.091571238599165, pricePercentChange1y: -2.2992517267242754, }, - '0xccc': { + [buildTokenAssetId('0xCCC')]: { price: 148.1344197578456, currency: 'ETH', pricePercentChange1d: 1, @@ -1247,8 +1179,20 @@ describe('CodefiTokenPricesServiceV2', () => { }); const fetchTokenPrices = () => service.fetchTokenPrices({ - chainId: '0x1', - tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], + assets: [ + { + chainId: '0x1', + tokenAddress: '0xAAA', + }, + { + chainId: '0x1', + tokenAddress: '0xBBB', + }, + { + chainId: '0x1', + tokenAddress: '0xCCC', + }, + ], currency: 'ETH', }); expect(onBreakHandler).not.toHaveBeenCalled(); @@ -1892,3 +1836,23 @@ async function fetchExchangeRatesWithFakeTimers({ return await pendingUpdate; } + +/** + * + * @param tokenAddress - The token address. + * @returns The token asset id. + */ +function buildTokenAssetId(tokenAddress: Hex): string { + return tokenAddress === ZERO_ADDRESS + ? `${KnownCaipNamespace.Eip155}:1/slip44:60` + : `${KnownCaipNamespace.Eip155}:1/erc20:${tokenAddress.toLowerCase()}`; +} + +/** + * + * @param tokenAddresses - The token addresses. + * @returns The token asset ids. + */ +function buildMultipleAssetIds(tokenAddresses: Hex[]): string { + return tokenAddresses.map(buildTokenAssetId).join(','); +} From 03466513ad8d8d063a108e1caa023f671b053cc4 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Mon, 17 Nov 2025 13:42:19 +0000 Subject: [PATCH 20/42] fix test --- ...TokenSearchDiscoveryDataController.test.ts | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/assets-controllers/src/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.test.ts b/packages/assets-controllers/src/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.test.ts index 3a0e8d5bbd0..1ab0ad94796 100644 --- a/packages/assets-controllers/src/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.test.ts +++ b/packages/assets-controllers/src/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.test.ts @@ -7,7 +7,7 @@ import { type MessengerEvents, type MockAnyNamespace, } from '@metamask/messenger'; -import type { Hex } from '@metamask/utils'; +import { KnownCaipNamespace, type Hex } from '@metamask/utils'; import assert from 'assert'; import { useFakeTimers } from 'sinon'; @@ -23,8 +23,7 @@ import type { NotFoundTokenDisplayData, FoundTokenDisplayData } from './types'; import { advanceTime } from '../../../../tests/helpers'; import type { AbstractTokenPricesService, - TokenPrice, - TokenPricesByTokenAddress, + EvmAssetWithMarketData, } from '../token-prices-service/abstract-token-prices-service'; import { fetchTokenMetadata } from '../token-service'; import type { Token } from '../TokenRatesController'; @@ -79,10 +78,12 @@ function buildFoundTokenDisplayData( name: 'Test Token', }; - const priceData: TokenPrice = { + const priceData: EvmAssetWithMarketData = { price: 10.5, currency: 'USD', tokenAddress: tokenAddress as Hex, + chainId: '0x1', + assetId: `${KnownCaipNamespace.Eip155}:1/erc20:${tokenAddress.toLowerCase()}`, allTimeHigh: 20, allTimeLow: 5, circulatingSupply: 1000000, @@ -151,7 +152,7 @@ function buildMockTokenPricesService( return {}; }, async fetchTokenPrices() { - return {}; + return []; }, validateChainIdSupported(_chainId: unknown): _chainId is Hex { return true; @@ -492,10 +493,12 @@ describe('TokenSearchDiscoveryDataController', () => { Promise.resolve(tokenMetadata), ); - const mockPriceData: TokenPrice = { + const mockPriceData: EvmAssetWithMarketData = { price: 10.5, currency: 'USD', tokenAddress: tokenAddress as Hex, + chainId: '0x1', + assetId: `${KnownCaipNamespace.Eip155}:1/erc20:${tokenAddress.toLowerCase()}`, allTimeHigh: 20, allTimeLow: 5, circulatingSupply: 1000000, @@ -516,9 +519,7 @@ describe('TokenSearchDiscoveryDataController', () => { }; const mockTokenPricesService = { - fetchTokenPrices: jest.fn().mockResolvedValue({ - [tokenAddress as Hex]: mockPriceData, - }), + fetchTokenPrices: jest.fn().mockResolvedValue([mockPriceData]), }; await withController( @@ -643,12 +644,14 @@ describe('TokenSearchDiscoveryDataController', () => { currency, }: { currency: string; - }): Promise> { + }): Promise[]> { const basePrice: Omit< - TokenPrice, + EvmAssetWithMarketData, 'price' | 'currency' > = { tokenAddress: tokenAddress as Hex, + chainId: '0x1', + assetId: `${KnownCaipNamespace.Eip155}:1/erc20:${tokenAddress.toLowerCase()}`, allTimeHigh: 20, allTimeLow: 5, circulatingSupply: 1000000, @@ -668,13 +671,13 @@ describe('TokenSearchDiscoveryDataController', () => { totalVolume: 500000, }; - return { - [tokenAddress as Hex]: { + return [ + { ...basePrice, price: currency === 'USD' ? 10.5 : 9.5, currency, }, - }; + ]; }, }; @@ -713,10 +716,12 @@ describe('TokenSearchDiscoveryDataController', () => { decimals: 18, }); - const mockTokenPrice: TokenPrice = { + const mockTokenPrice: EvmAssetWithMarketData = { price: 10.5, currency: 'USD', tokenAddress: tokenAddress as Hex, + chainId: '0x1', + assetId: `${KnownCaipNamespace.Eip155}:1/erc20:${tokenAddress.toLowerCase()}`, allTimeHigh: 20, allTimeLow: 5, circulatingSupply: 1000000, From bdb817946599aa57d1ddf3428ac65d14d41ff46f Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 18 Nov 2025 00:09:06 +0000 Subject: [PATCH 21/42] tests --- .../src/TokenRatesController.test.ts | 5169 +++++++++-------- .../src/TokenRatesController.ts | 109 +- 2 files changed, 2698 insertions(+), 2580 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index 813789d7bac..ff8edbbb307 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -20,8 +20,8 @@ import type { NetworkState, } from '@metamask/network-controller'; import { getDefaultNetworkControllerState } from '@metamask/network-controller'; -import type { Hex } from '@metamask/utils'; -import { add0x } from '@metamask/utils'; +import type { CaipAssetType, Hex } from '@metamask/utils'; +import { add0x, KnownCaipNamespace } from '@metamask/utils'; import assert from 'assert'; import type { Patch } from 'immer'; import nock from 'nock'; @@ -30,9 +30,9 @@ import { useFakeTimers } from 'sinon'; import { TOKEN_PRICES_BATCH_SIZE } from './assetsUtil'; import type { AbstractTokenPricesService, - TokenPrice, - TokenPricesByTokenAddress, + EvmAssetWithMarketData, } from './token-prices-service/abstract-token-prices-service'; +import { ZERO_ADDRESS } from './token-prices-service/codefi-v2'; import { controllerName, TokenRatesController } from './TokenRatesController'; import type { Token, @@ -41,15 +41,14 @@ import type { } from './TokenRatesController'; import { getDefaultTokensState } from './TokensController'; import type { TokensControllerState } from './TokensController'; -import { advanceTime } from '../../../tests/helpers'; +import { advanceTime, flushPromises } from '../../../tests/helpers'; import { createMockInternalAccount } from '../../accounts-controller/src/tests/mocks'; import { buildCustomNetworkClientConfiguration, buildMockGetNetworkClientById, - buildNetworkConfiguration, } from '../../network-controller/tests/helpers'; -const defaultSelectedAddress = '0x0000000000000000000000000000000000000001'; +const defaultSelectedAddress = '0x1111111111111111111111111111111111111111'; const defaultSelectedAccount = createMockInternalAccount({ address: defaultSelectedAddress, }); @@ -94,26 +93,22 @@ function buildTokenRatesControllerMessenger( 'AccountsController:getAccount', 'AccountsController:getSelectedAccount', ], - events: [ - 'TokensController:stateChange', - 'NetworkController:stateChange', - 'AccountsController:selectedEvmAccountChange', - ], + events: ['TokensController:stateChange', 'NetworkController:stateChange'], }); return tokenRatesControllerMessenger; } describe('TokenRatesController', () => { describe('constructor', () => { - let clock: sinon.SinonFakeTimers; + // let clock: sinon.SinonFakeTimers; - beforeEach(() => { - clock = useFakeTimers({ now: Date.now() }); - }); + // beforeEach(() => { + // clock = useFakeTimers({ now: Date.now() }); + // }); - afterEach(() => { - clock.restore(); - }); + // afterEach(() => { + // clock.restore(); + // }); it('should set default state', async () => { await withController(async ({ controller }) => { @@ -122,2537 +117,2749 @@ describe('TokenRatesController', () => { }); }); }); - - it('should not poll by default', async () => { - const fetchSpy = jest.spyOn(globalThis, 'fetch'); - await withController( - { - options: { - interval: 100, - }, - }, - async ({ controller }) => { - expect(controller.state).toStrictEqual({ - marketData: {}, - }); - }, - ); - await advanceTime({ clock, duration: 500 }); - - expect(fetchSpy).not.toHaveBeenCalled(); - }); }); - describe('TokensController::stateChange', () => { - let clock: sinon.SinonFakeTimers; - - beforeEach(() => { - clock = useFakeTimers({ now: Date.now() }); - }); - - afterEach(() => { - clock.restore(); - }); - - describe('when legacy polling is active', () => { - it('should update exchange rates when any of the addresses in the "all tokens" collection change', async () => { - const tokenAddresses = ['0xE1', '0xE2']; - await withController( - { - mockTokensControllerState: { - allTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: tokenAddresses[0], - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }, - }, - async ({ controller, triggerTokensStateChange }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRatesByChainId') - .mockResolvedValue(); - await controller.start(ChainId.mainnet, 'ETH'); - triggerTokensStateChange({ - ...getDefaultTokensState(), - allTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: tokenAddresses[1], - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }); + describe('updateExchangeRates', () => { + it('fetches rates for tokens in one batch', async () => { + const chainId = '0x1'; + const nativeCurrency = 'ETH'; - // Once when starting, and another when tokens state changes - expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(2); - }, - ); - }); - - it('should update exchange rates when any of the addresses in the "all tokens" collection change with invalid addresses', async () => { - const tokenAddresses = ['0xinvalidAddress']; - await withController( - { - mockTokensControllerState: { - allTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: tokenAddresses[0], - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }, - }, - async ({ controller, triggerTokensStateChange }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRatesByChainId') - .mockResolvedValue(); - await controller.start(ChainId.mainnet, 'ETH'); - triggerTokensStateChange({ - ...getDefaultTokensState(), - allTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: tokenAddresses[1], - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }); - - // Once when starting, and another when tokens state changes - expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(2); - }, - ); + const tokenPricesService = buildMockTokenPricesService({ + fetchTokenPrices: fetchTokenPricesWithIncreasingPriceForEachToken, }); + jest.spyOn(tokenPricesService, 'fetchTokenPrices'); - it('should update exchange rates when any of the addresses in the "all detected tokens" collection change', async () => { - const tokenAddresses = ['0xE1', '0xE2']; - await withController( - { - mockTokensControllerState: { - allDetectedTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: tokenAddresses[0], - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }, + await withController( + { + options: { + tokenPricesService, }, - async ({ controller, triggerTokensStateChange }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRatesByChainId') - .mockResolvedValue(); - await controller.start(ChainId.mainnet, 'ETH'); - triggerTokensStateChange({ - ...getDefaultTokensState(), - allDetectedTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: tokenAddresses[1], - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, + mockTokensControllerState: { + allTokens: { + [chainId]: { + [defaultSelectedAddress]: [ + { + address: '0x0000000000000000000000000000000000000001', + decimals: 0, + symbol: 'TOK1', + }, + ], }, - }); - // Once when starting, and another when tokens state changes - expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(2); - }, - ); - }); - - it('should not update exchange rates if both the "all tokens" or "all detected tokens" are exactly the same', async () => { - const tokensState = { - allTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: mockTokenAddress, - decimals: 0, - symbol: '', - aggregators: [], - }, - ], }, }, - }; - await withController( - { - mockTokensControllerState: { - ...tokensState, - }, - }, - async ({ controller, triggerTokensStateChange }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - await controller.start(ChainId.mainnet, 'ETH'); - triggerTokensStateChange({ - ...getDefaultTokensState(), - ...tokensState, - }); - - // Once when starting, and that's it - expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); - }, - ); - }); - - it('should not update exchange rates if all of the tokens in "all tokens" just move to "all detected tokens"', async () => { - const tokens = { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: mockTokenAddress, - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, - }; - await withController( - { - mockTokensControllerState: { - allTokens: tokens, + }, + async ({ controller }) => { + await controller.updateExchangeRates([ + { + chainId, + nativeCurrency, }, - }, - async ({ controller, triggerTokensStateChange }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - await controller.start(ChainId.mainnet, 'ETH'); - triggerTokensStateChange({ - ...getDefaultTokensState(), - allDetectedTokens: tokens, - }); - - // Once when starting, and that's it - expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); - }, - ); - }); + ]); - it('should not update exchange rates if a new token is added to "all detected tokens" but is already present in "all tokens"', async () => { - const tokens = { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledWith({ + assets: [ { - address: mockTokenAddress, - decimals: 0, - symbol: '', - aggregators: [], + chainId, + tokenAddress: '0x0000000000000000000000000000000000000000', }, - ], - }, - }; - await withController( - { - mockTokensControllerState: { - allTokens: tokens, - }, - }, - async ({ controller, triggerTokensStateChange }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - await controller.start(ChainId.mainnet, 'ETH'); - triggerTokensStateChange({ - ...getDefaultTokensState(), - allTokens: tokens, - allDetectedTokens: tokens, - }); - - // Once when starting, and that's it - expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); - }, - ); - }); - - it('should not update exchange rates if a new token is added to "all tokens" but is already present in "all detected tokens"', async () => { - const tokens = { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ { - address: mockTokenAddress, - decimals: 0, - symbol: '', - aggregators: [], + chainId, + tokenAddress: '0x0000000000000000000000000000000000000001', }, ], - }, - }; - await withController( - { - mockTokensControllerState: { - allDetectedTokens: tokens, - }, - }, - async ({ controller, triggerTokensStateChange }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - await controller.start(ChainId.mainnet, 'ETH'); - triggerTokensStateChange({ - ...getDefaultTokensState(), - allTokens: tokens, - allDetectedTokens: tokens, - }); - - // Once when starting, and that's it - expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); - }, - ); - }); - - it('should not update exchange rates if none of the addresses in "all tokens" or "all detected tokens" change, even if other parts of the token change', async () => { - await withController( - { - mockTokensControllerState: { - ...getDefaultTokensState(), - allDetectedTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: mockTokenAddress, - decimals: 3, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }, - }, - async ({ controller, triggerTokensStateChange }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - await controller.start(ChainId.mainnet, 'ETH'); - triggerTokensStateChange({ - ...getDefaultTokensState(), - allDetectedTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: mockTokenAddress, - decimals: 7, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }); - - // Once when starting, and that's it - expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); - }, - ); - }); - - it('should not update exchange rates if none of the addresses in "all tokens" or "all detected tokens" change, when normalized to checksum addresses', async () => { - await withController( - { - mockTokensControllerState: { - allDetectedTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: '0x0EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE2', - decimals: 3, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }, - }, - async ({ controller, triggerTokensStateChange }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - await controller.start(ChainId.mainnet, 'ETH'); - triggerTokensStateChange({ - ...getDefaultTokensState(), - allDetectedTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: '0x0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2', - decimals: 7, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }); - - // Once when starting, and that's it - expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); - }, - ); - }); + currency: nativeCurrency, + }); - it('should not update exchange rates if any of the addresses in "all tokens" or "all detected tokens" merely change order', async () => { - await withController( - { - mockTokensControllerState: { - allDetectedTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: '0xE1', - decimals: 0, - symbol: '', - aggregators: [], - }, - { - address: '0xE2', - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, - }, + expect(controller.state.marketData).toStrictEqual({ + '0x1': { + '0x0000000000000000000000000000000000000000': + expect.objectContaining({ + currency: nativeCurrency, + price: 0.001, + }), + '0x0000000000000000000000000000000000000001': + expect.objectContaining({ + currency: nativeCurrency, + price: 0.002, + }), }, - }, - async ({ controller, triggerTokensStateChange }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - await controller.start(ChainId.mainnet, 'ETH'); - triggerTokensStateChange({ - ...getDefaultTokensState(), - allDetectedTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: '0xE2', - decimals: 0, - symbol: '', - aggregators: [], - }, - { - address: '0xE1', - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }); - - // Once when starting, and that's it - expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); - }, - ); - }); + }); + }, + ); }); - describe('when legacy polling is inactive', () => { - it('should not update exchange rates when any of the addresses in the "all tokens" collection change', async () => { - const tokenAddresses = ['0xE1', '0xE2']; - await withController( - { - mockTokensControllerState: { - allTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: tokenAddresses[0], - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }, - }, - async ({ controller, triggerTokensStateChange }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - triggerTokensStateChange({ - ...getDefaultTokensState(), - allTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: tokenAddresses[1], - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }); + it('fetches rates for all tokens in batches', async () => { + const chainId = '0x1'; + const nativeCurrency = 'ETH'; - expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); - }, - ); + const tokenPricesService = buildMockTokenPricesService({ + fetchTokenPrices: fetchTokenPricesWithIncreasingPriceForEachToken, }); + jest.spyOn(tokenPricesService, 'fetchTokenPrices'); - it('should not update exchange rates when any of the addresses in the "all detected tokens" collection change', async () => { - const tokenAddresses = ['0xE1', '0xE2']; - await withController( - { - mockTokensControllerState: { - allDetectedTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: tokenAddresses[0], - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }, - }, - async ({ controller, triggerTokensStateChange }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - triggerTokensStateChange({ - ...getDefaultTokensState(), - allDetectedTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: tokenAddresses[1], - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }); - - expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); - }, - ); - }); - }); - }); - - describe('NetworkController::stateChange', () => { - let clock: sinon.SinonFakeTimers; - - beforeEach(() => { - clock = useFakeTimers({ now: Date.now() }); - }); - - afterEach(() => { - clock.restore(); - }); - - describe('when polling is active', () => { - it('should update exchange rates when ticker changes', async () => { - await withController( - { - options: { - interval: 100, - }, - mockNetworkClientConfigurationsByNetworkClientId: { - 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - chainId: toHex(1337), - ticker: 'NEW', - }), - }, - }, - async ({ controller, triggerNetworkStateChange }) => { - await controller.start(ChainId.mainnet, 'ETH'); - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - triggerNetworkStateChange({ - ...getDefaultNetworkControllerState(), - selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', - }); - - expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); - }, - ); + const tokenAddresses = [...new Array(200).keys()] + .map(buildAddress) + .sort(); + const tokens = tokenAddresses.map((tokenAddress) => { + return buildToken({ address: tokenAddress }); }); - - it('should not update exchange rates when chain ID changes', async () => { - await withController( - { - options: { - interval: 100, - }, - mockNetworkClientConfigurationsByNetworkClientId: { - 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - chainId: toHex(1338), - ticker: 'TEST', - }), - }, - }, - async ({ controller, triggerNetworkStateChange }) => { - await controller.start(ChainId.mainnet, 'ETH'); - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - triggerNetworkStateChange({ - ...getDefaultNetworkControllerState(), - selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', - }); - - expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); + await withController( + { + options: { + tokenPricesService, }, - ); - }); - - it('should clear marketData in state when ticker changes', async () => { - await withController( - { - options: { - interval: 100, - state: { - marketData: { - [ChainId.mainnet]: { - '0x02': { - currency: 'ETH', - priceChange1d: 0, - pricePercentChange1d: 0, - tokenAddress: '0x02', - allTimeHigh: 4000, - allTimeLow: 900, - circulatingSupply: 2000, - dilutedMarketCap: 100, - high1d: 200, - low1d: 100, - marketCap: 1000, - marketCapPercentChange1d: 100, - price: 0.001, - pricePercentChange14d: 100, - pricePercentChange1h: 1, - pricePercentChange1y: 200, - pricePercentChange200d: 300, - pricePercentChange30d: 200, - pricePercentChange7d: 100, - totalVolume: 100, - }, - }, - }, + mockTokensControllerState: { + allTokens: { + [chainId]: { + [defaultSelectedAddress]: tokens, }, }, - mockNetworkClientConfigurationsByNetworkClientId: { - 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - chainId: toHex(1337), - ticker: 'NEW', - }), - }, - }, - async ({ controller, triggerNetworkStateChange }) => { - await controller.start(ChainId.mainnet, 'ETH'); - jest.spyOn(controller, 'updateExchangeRates').mockResolvedValue(); - triggerNetworkStateChange({ - ...getDefaultNetworkControllerState(), - selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', - }); - - expect(controller.state.marketData).toStrictEqual({ - '0x1': { - '0x0000000000000000000000000000000000000000': { - currency: 'ETH', - }, - }, - }); }, - ); - }); - - it('should not clear marketData state when chain ID changes', async () => { - await withController( - { - options: { - interval: 100, - state: { - marketData: { - [ChainId.mainnet]: { - '0x02': { - currency: 'ETH', - priceChange1d: 0, - pricePercentChange1d: 0, - tokenAddress: '0x02', - allTimeHigh: 4000, - allTimeLow: 900, - circulatingSupply: 2000, - dilutedMarketCap: 100, - high1d: 200, - low1d: 100, - marketCap: 1000, - marketCapPercentChange1d: 100, - price: 0.001, - pricePercentChange14d: 100, - pricePercentChange1h: 1, - pricePercentChange1y: 200, - pricePercentChange200d: 300, - pricePercentChange30d: 200, - pricePercentChange7d: 100, - totalVolume: 100, - }, - }, - }, - }, - }, - mockNetworkClientConfigurationsByNetworkClientId: { - 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - chainId: toHex(1338), - ticker: 'TEST', - }), + }, + async ({ controller }) => { + await controller.updateExchangeRates([ + { + chainId, + nativeCurrency, }, - }, - async ({ controller, triggerNetworkStateChange }) => { - await controller.start(ChainId.mainnet, 'ETH'); - jest.spyOn(controller, 'updateExchangeRates').mockResolvedValue(); - triggerNetworkStateChange({ - ...getDefaultNetworkControllerState(), - selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', - }); - - expect(controller.state.marketData).toStrictEqual({ - '0x1': { - '0x0000000000000000000000000000000000000000': { - currency: 'ETH', - }, - }, - }); - }, - ); - }); + ]); + const numBatches = Math.ceil( + tokenAddresses.length / TOKEN_PRICES_BATCH_SIZE, + ); + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( + numBatches, + ); - it('should update exchange rates when network state changes without adding a new network', async () => { - await withController( - { - options: { - interval: 100, - }, - mockNetworkClientConfigurationsByNetworkClientId: { - 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - chainId: ChainId.mainnet, - ticker: NetworksTicker.mainnet, - }), - }, - }, - async ({ controller, triggerNetworkStateChange }) => { - await controller.start(ChainId.mainnet, 'ETH'); - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - triggerNetworkStateChange( + for (let i = 1; i <= numBatches; i++) { + expect(tokenPricesService.fetchTokenPrices).toHaveBeenNthCalledWith( + i, { - ...getDefaultNetworkControllerState(), - selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', + assets: tokenAddresses + .slice( + (i - 1) * TOKEN_PRICES_BATCH_SIZE, + i * TOKEN_PRICES_BATCH_SIZE, + ) + .map((tokenAddress) => ({ + chainId, + tokenAddress, + })), + currency: nativeCurrency, }, - [ - { - op: 'add', - path: ['networkConfigurationsByChainId', ChainId.mainnet], - }, - ], ); - expect(updateExchangeRatesSpy).toHaveBeenCalled(); - }, - ); - }); + } + }, + ); }); - describe('when polling is inactive', () => { - it('should not update exchange rates when ticker changes', async () => { - await withController( - { - options: { - interval: 100, - }, - mockNetworkClientConfigurationsByNetworkClientId: { - 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - chainId: toHex(1337), - ticker: 'NEW', - }), - }, - }, - async ({ controller, triggerNetworkStateChange }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - triggerNetworkStateChange({ - ...getDefaultNetworkControllerState(), - selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', - }); - - expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); - }, - ); - }); + it('leaves unsupported chain state keys empty', async () => { + const chainId = '0x1'; + const nativeCurrency = 'ETH'; - it('should not update exchange rates when chain ID changes', async () => { - await withController( - { - options: { - interval: 100, - }, - mockNetworkClientConfigurationsByNetworkClientId: { - 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - chainId: toHex(1338), - ticker: 'TEST', - }), - }, - }, - async ({ controller, triggerNetworkStateChange }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - triggerNetworkStateChange({ - ...getDefaultNetworkControllerState(), - selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', - }); - - expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); - }, - ); - }); - - it('should not clear marketData state when ticker changes', async () => { - await withController( - { - options: { - interval: 100, - state: { - marketData: { - [ChainId.mainnet]: { - '0x02': { - currency: 'ETH', - priceChange1d: 0, - pricePercentChange1d: 0, - tokenAddress: '0x02', - allTimeHigh: 4000, - allTimeLow: 900, - circulatingSupply: 2000, - dilutedMarketCap: 100, - high1d: 200, - low1d: 100, - marketCap: 1000, - marketCapPercentChange1d: 100, - price: 0.001, - pricePercentChange14d: 100, - pricePercentChange1h: 1, - pricePercentChange1y: 200, - pricePercentChange200d: 300, - pricePercentChange30d: 200, - pricePercentChange7d: 100, - totalVolume: 100, - }, - }, - }, - }, - }, - mockNetworkClientConfigurationsByNetworkClientId: { - 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - chainId: toHex(1337), - ticker: 'NEW', - }), - }, - }, - async ({ controller, triggerNetworkStateChange }) => { - jest.spyOn(controller, 'updateExchangeRates').mockResolvedValue(); - triggerNetworkStateChange({ - ...getDefaultNetworkControllerState(), - selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', - }); - - expect(controller.state.marketData).toStrictEqual({ - '0x1': { - '0x02': { - currency: 'ETH', - priceChange1d: 0, - pricePercentChange1d: 0, - tokenAddress: '0x02', - allTimeHigh: 4000, - allTimeLow: 900, - circulatingSupply: 2000, - dilutedMarketCap: 100, - high1d: 200, - low1d: 100, - marketCap: 1000, - marketCapPercentChange1d: 100, - price: 0.001, - pricePercentChange14d: 100, - pricePercentChange1h: 1, - pricePercentChange1y: 200, - pricePercentChange200d: 300, - pricePercentChange30d: 200, - pricePercentChange7d: 100, - totalVolume: 100, - }, - }, - }); - }, - ); - }); - - it('should not clear marketData state when chain ID changes', async () => { - await withController( - { - options: { - interval: 100, - state: { - marketData: { - [ChainId.mainnet]: { - '0x02': { - currency: 'ETH', - priceChange1d: 0, - pricePercentChange1d: 0, - tokenAddress: '0x02', - allTimeHigh: 4000, - allTimeLow: 900, - circulatingSupply: 2000, - dilutedMarketCap: 100, - high1d: 200, - low1d: 100, - marketCap: 1000, - marketCapPercentChange1d: 100, - price: 0.001, - pricePercentChange14d: 100, - pricePercentChange1h: 1, - pricePercentChange1y: 200, - pricePercentChange200d: 300, - pricePercentChange30d: 200, - pricePercentChange7d: 100, - totalVolume: 100, - }, - }, - }, - }, - }, - mockNetworkClientConfigurationsByNetworkClientId: { - 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - chainId: toHex(1338), - ticker: 'TEST', - }), - }, - }, - async ({ controller, triggerNetworkStateChange }) => { - jest.spyOn(controller, 'updateExchangeRates').mockResolvedValue(); - triggerNetworkStateChange({ - ...getDefaultNetworkControllerState(), - selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', - }); - - expect(controller.state.marketData).toStrictEqual({ - '0x1': { - '0x02': { - currency: 'ETH', - priceChange1d: 0, - pricePercentChange1d: 0, - tokenAddress: '0x02', - allTimeHigh: 4000, - allTimeLow: 900, - circulatingSupply: 2000, - dilutedMarketCap: 100, - high1d: 200, - low1d: 100, - marketCap: 1000, - marketCapPercentChange1d: 100, - price: 0.001, - pricePercentChange14d: 100, - pricePercentChange1h: 1, - pricePercentChange1y: 200, - pricePercentChange200d: 300, - pricePercentChange30d: 200, - pricePercentChange7d: 100, - totalVolume: 100, - }, - }, - }); - }, - ); + const tokenPricesService = buildMockTokenPricesService({ + fetchTokenPrices: fetchTokenPricesWithIncreasingPriceForEachToken, + validateChainIdSupported: (_chainId: unknown): _chainId is Hex => false, }); - }); - - it('removes state when networks are deleted', async () => { - const marketData = { - [ChainId.mainnet]: { - '0x123456': { - currency: 'ETH', - priceChange1d: 0, - pricePercentChange1d: 0, - tokenAddress: '0x02', - allTimeHigh: 4000, - allTimeLow: 900, - circulatingSupply: 2000, - dilutedMarketCap: 100, - high1d: 200, - low1d: 100, - marketCap: 1000, - marketCapPercentChange1d: 100, - price: 0.001, - pricePercentChange14d: 100, - pricePercentChange1h: 1, - pricePercentChange1y: 200, - pricePercentChange200d: 300, - pricePercentChange30d: 200, - pricePercentChange7d: 100, - totalVolume: 100, - }, - }, - [ChainId['linea-mainnet']]: { - '0x789': { - currency: 'ETH', - priceChange1d: 0, - pricePercentChange1d: 0, - tokenAddress: '0x02', - allTimeHigh: 4000, - allTimeLow: 900, - circulatingSupply: 2000, - dilutedMarketCap: 100, - high1d: 200, - low1d: 100, - marketCap: 1000, - marketCapPercentChange1d: 100, - price: 0.001, - pricePercentChange14d: 100, - pricePercentChange1h: 1, - pricePercentChange1y: 200, - pricePercentChange200d: 300, - pricePercentChange30d: 200, - pricePercentChange7d: 100, - totalVolume: 100, - }, - }, - } as const; + jest.spyOn(tokenPricesService, 'fetchTokenPrices'); await withController( { options: { - state: { - marketData, - }, + tokenPricesService, }, }, - async ({ controller, triggerNetworkStateChange }) => { - // Verify initial state with both networks - expect(controller.state.marketData).toStrictEqual(marketData); - - triggerNetworkStateChange( + async ({ controller }) => { + await controller.updateExchangeRates([ { - selectedNetworkClientId: 'mainnet', - networkConfigurationsByChainId: {}, - } as NetworkState, - [ - { - op: 'remove', - path: [ - 'networkConfigurationsByChainId', - ChainId['linea-mainnet'], - ], - }, - ], - ); + chainId, + nativeCurrency, + }, + ]); - // Verify linea removed + expect(tokenPricesService.fetchTokenPrices).not.toHaveBeenCalled(); expect(controller.state.marketData).toStrictEqual({ - [ChainId.mainnet]: marketData[ChainId.mainnet], + [chainId]: {}, }); }, ); }); }); - describe('PreferencesController::stateChange', () => { - let clock: sinon.SinonFakeTimers; + describe('_executePoll', () => { + it('fetches rates for the given chains', async () => { + await withController({}, async ({ controller }) => { + jest.spyOn(controller, 'updateExchangeRates'); - beforeEach(() => { - clock = useFakeTimers({ now: Date.now() }); - }); - - afterEach(() => { - clock.restore(); - }); + await controller._executePoll({ chainIds: ['0x1'] }); - describe('when polling is active', () => { - it('should not update exchange rates when selected address changes', async () => { - const alternateSelectedAddress = - '0x0000000000000000000000000000000000000002'; - const alternateSelectedAccount = createMockInternalAccount({ - address: alternateSelectedAddress, - }); - await withController( + expect(controller.updateExchangeRates).toHaveBeenCalledWith([ { - options: { - interval: 100, - }, - mockTokensControllerState: { - allTokens: { - '0x1': { - [alternateSelectedAddress]: [ - { - address: '0x02', - decimals: 0, - symbol: '', - aggregators: [], - }, - { - address: '0x03', - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }, - }, - async ({ controller, triggerSelectedAccountChange }) => { - await controller.start(ChainId.mainnet, 'ETH'); - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - triggerSelectedAccountChange(alternateSelectedAccount); - - expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); - }, - ); - }); - }); - - describe('when polling is inactive', () => { - it('does not update exchange rates when selected account changes', async () => { - const alternateSelectedAddress = - '0x0000000000000000000000000000000000000002'; - const alternateSelectedAccount = createMockInternalAccount({ - address: alternateSelectedAddress, - }); - await withController( - { - options: { - interval: 100, - }, - mockTokensControllerState: { - allTokens: { - '0x1': { - [alternateSelectedAddress]: [ - { - address: '0x02', - decimals: 0, - symbol: '', - aggregators: [], - }, - { - address: '0x03', - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }, - }, - async ({ controller, triggerSelectedAccountChange }) => { - const updateExchangeRatesSpy = jest - .spyOn(controller, 'updateExchangeRates') - .mockResolvedValue(); - triggerSelectedAccountChange(alternateSelectedAccount); - - expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); - }, - ); - }); - }); - }); - - describe('legacy polling', () => { - let clock: sinon.SinonFakeTimers; - - beforeEach(() => { - clock = useFakeTimers({ now: Date.now() }); - }); - - afterEach(() => { - clock.restore(); - }); - - describe('start', () => { - it('should poll and update rate in the right interval', async () => { - const interval = 100; - const tokenPricesService = buildMockTokenPricesService(); - jest.spyOn(tokenPricesService, 'fetchTokenPrices'); - await withController( - { - options: { - interval, - tokenPricesService, - }, - mockTokensControllerState: { - allTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: mockTokenAddress, - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }, - }, - async ({ controller }) => { - await controller.start(ChainId.mainnet, 'ETH'); - - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( - 1, - ); - - await advanceTime({ clock, duration: interval }); - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( - 2, - ); - - await advanceTime({ clock, duration: interval }); - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( - 3, - ); - }, - ); - }); - }); - - describe('stop', () => { - it('should stop polling', async () => { - const interval = 100; - const tokenPricesService = buildMockTokenPricesService(); - jest.spyOn(tokenPricesService, 'fetchTokenPrices'); - await withController( - { - options: { - interval, - tokenPricesService, - }, - mockTokensControllerState: { - allTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: mockTokenAddress, - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }, - }, - async ({ controller }) => { - await controller.start(ChainId.mainnet, 'ETH'); - - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( - 1, - ); - - controller.stop(); - - await advanceTime({ clock, duration: interval }); - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( - 1, - ); + chainId: '0x1', + nativeCurrency: 'ETH', }, - ); + ]); }); }); - }); - describe('polling by networkClientId', () => { - let clock: sinon.SinonFakeTimers; + it('does not include chains with no network configuration', async () => { + await withController( + { + mockNetworkState: { + networkConfigurationsByChainId: {}, + }, + }, + async ({ controller }) => { + jest.spyOn(controller, 'updateExchangeRates'); - beforeEach(() => { - clock = useFakeTimers({ now: Date.now() }); - }); + await controller._executePoll({ chainIds: ['0x1'] }); - afterEach(() => { - clock.restore(); + expect(controller.updateExchangeRates).toHaveBeenCalledWith([]); + }, + ); }); + }); - it('should poll on the right interval', async () => { - const interval = 100; - const tokenPricesService = buildMockTokenPricesService(); + describe('TokensController:stateChange', () => { + it('fetches rates for all updated chains', async () => { + jest.useFakeTimers(); + const chainId = '0x1'; + const nativeCurrency = 'ETH'; + + const tokenPricesService = buildMockTokenPricesService({ + fetchTokenPrices: fetchTokenPricesWithIncreasingPriceForEachToken, + }); jest.spyOn(tokenPricesService, 'fetchTokenPrices'); + await withController( { options: { - interval, tokenPricesService, }, - mockTokensControllerState: { + }, + async ({ controller, triggerTokensStateChange }) => { + triggerTokensStateChange({ allTokens: { - [ChainId.mainnet]: { + [chainId]: { [defaultSelectedAddress]: [ { - address: mockTokenAddress, + address: '0x0000000000000000000000000000000000000001', decimals: 0, - symbol: '', - aggregators: [], + symbol: 'TOK1', }, ], }, }, - }, - }, - async ({ controller }) => { - controller.startPolling({ - chainIds: [ChainId.mainnet], - }); - - await advanceTime({ clock, duration: 0 }); - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); - - await advanceTime({ clock, duration: interval }); - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(2); - - await advanceTime({ clock, duration: interval }); - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(3); - }, - ); - }); - - describe('updating state on poll', () => { - describe('when the native currency is supported', () => { - it('returns the exchange rates directly', async () => { - const tokenPricesService = buildMockTokenPricesService({ - fetchTokenPrices: fetchTokenPricesWithIncreasingPriceForEachToken, - validateCurrencySupported(currency: unknown): currency is string { - return currency === 'ETH'; - }, - }); - const interval = 100; - await withController( - { - options: { - interval, - tokenPricesService, - }, - mockTokensControllerState: { - allTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: '0x02', - decimals: 0, - symbol: '', - aggregators: [], - }, - { - address: '0x03', - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }, - }, - async ({ controller }) => { - controller.startPolling({ - chainIds: [ChainId.mainnet], - }); - await advanceTime({ clock, duration: 0 }); - - expect(controller.state).toStrictEqual({ - marketData: { - [ChainId.mainnet]: { - '0x02': { - currency: 'ETH', - priceChange1d: 0, - pricePercentChange1d: 0, - tokenAddress: '0x02', - allTimeHigh: 4000, - allTimeLow: 900, - circulatingSupply: 2000, - dilutedMarketCap: 100, - high1d: 200, - low1d: 100, - marketCap: 1000, - marketCapPercentChange1d: 100, - price: 0.001, - pricePercentChange14d: 100, - pricePercentChange1h: 1, - pricePercentChange1y: 200, - pricePercentChange200d: 300, - pricePercentChange30d: 200, - pricePercentChange7d: 100, - totalVolume: 100, - }, - '0x03': { - currency: 'ETH', - priceChange1d: 0, - pricePercentChange1d: 0, - tokenAddress: '0x03', - allTimeHigh: 4000, - allTimeLow: 900, - circulatingSupply: 2000, - dilutedMarketCap: 100, - high1d: 200, - low1d: 100, - marketCap: 1000, - marketCapPercentChange1d: 100, - price: 0.002, - pricePercentChange14d: 100, - pricePercentChange1h: 1, - pricePercentChange1y: 200, - pricePercentChange200d: 300, - pricePercentChange30d: 200, - pricePercentChange7d: 100, - totalVolume: 100, - }, - }, - }, - }); - }, - ); - }); - - describe('when the native currency is not supported', () => { - const fallbackRate = 0.5; - it('returns the exchange rates using ETH as a fallback currency', async () => { - nock('https://min-api.cryptocompare.com') - .get('/data/price?fsym=ETH&tsyms=LOL') - .reply(200, { LOL: fallbackRate }); - const tokenPricesService = buildMockTokenPricesService({ - fetchTokenPrices: fetchTokenPricesWithIncreasingPriceForEachToken, - validateCurrencySupported(currency: unknown): currency is string { - return currency !== 'LOL'; - }, - }); - await withController( - { - options: { - tokenPricesService, - }, - mockNetworkState: { - networkConfigurationsByChainId: { - [ChainId.mainnet]: buildNetworkConfiguration({ - nativeCurrency: 'LOL', - }), - }, - }, - mockTokensControllerState: { - allTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: '0x02', - decimals: 0, - symbol: '', - aggregators: [], - }, - { - address: '0x03', - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, + allDetectedTokens: { + [chainId]: { + [defaultSelectedAddress]: [ + { + address: '0x0000000000000000000000000000000000000001', + decimals: 0, + symbol: 'TOK1', }, - }, - }, - async ({ controller }) => { - controller.startPolling({ - chainIds: [ChainId.mainnet], - }); - // flush promises and advance setTimeouts they enqueue 3 times - // needed because fetch() doesn't resolve immediately, so any - // downstream promises aren't flushed until the next advanceTime loop - await advanceTime({ clock, duration: 1, stepSize: 1 / 3 }); - expect(controller.state.marketData).toStrictEqual({ - [ChainId.mainnet]: { - // token price in LOL = (token price in ETH) * (ETH value in LOL) - '0x02': { - tokenAddress: '0x02', - currency: 'LOL', - pricePercentChange1d: 0, - priceChange1d: 0, - allTimeHigh: 4000 * fallbackRate, - allTimeLow: 900 * fallbackRate, - circulatingSupply: 2000, - dilutedMarketCap: 100 * fallbackRate, - high1d: 200 * fallbackRate, - low1d: 100 * fallbackRate, - marketCap: 1000 * fallbackRate, - marketCapPercentChange1d: 100, - price: (1 / 1000) * fallbackRate, - pricePercentChange14d: 100, - pricePercentChange1h: 1, - pricePercentChange1y: 200, - pricePercentChange200d: 300, - pricePercentChange30d: 200, - pricePercentChange7d: 100, - totalVolume: 100 * fallbackRate, - }, - '0x03': { - tokenAddress: '0x03', - currency: 'LOL', - pricePercentChange1d: 0, - priceChange1d: 0, - allTimeHigh: 4000 * fallbackRate, - allTimeLow: 900 * fallbackRate, - circulatingSupply: 2000, - dilutedMarketCap: 100 * fallbackRate, - high1d: 200 * fallbackRate, - low1d: 100 * fallbackRate, - marketCap: 1000 * fallbackRate, - marketCapPercentChange1d: 100, - price: (2 / 1000) * fallbackRate, - pricePercentChange14d: 100, - pricePercentChange1h: 1, - pricePercentChange1y: 200, - pricePercentChange200d: 300, - pricePercentChange30d: 200, - pricePercentChange7d: 100, - totalVolume: 100 * fallbackRate, - }, + { + address: '0x0000000000000000000000000000000000000002', + decimals: 0, + symbol: 'TOK2', }, - }); - controller.stopAllPolling(); + ], }, - ); + }, + allIgnoredTokens: {}, }); - it('returns the an empty object when market does not exist for pair', async () => { - nock('https://min-api.cryptocompare.com') - .get('/data/price?fsym=ETH&tsyms=LOL') - .replyWithError( - new Error('market does not exist for this coin pair'), - ); + jest.advanceTimersToNextTimer(); + await flushPromises(); - const tokenPricesService = buildMockTokenPricesService(); - await withController( + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledWith({ + assets: [ { - options: { - tokenPricesService, - }, - mockNetworkClientConfigurationsByNetworkClientId: { - 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - chainId: ChainId.mainnet, - ticker: 'LOL', - }), - }, - mockTokensControllerState: { - allTokens: { - '0x1': { - [defaultSelectedAddress]: [ - { - address: '0x02', - decimals: 0, - symbol: '', - aggregators: [], - }, - { - address: '0x03', - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, - }, - }, + chainId, + tokenAddress: '0x0000000000000000000000000000000000000000', + }, + { + chainId, + tokenAddress: '0x0000000000000000000000000000000000000001', }, - async ({ controller }) => { - controller.startPolling({ - chainIds: [ChainId.mainnet], - }); - // flush promises and advance setTimeouts they enqueue 3 times - // needed because fetch() doesn't resolve immediately, so any - // downstream promises aren't flushed until the next advanceTime loop - await advanceTime({ clock, duration: 1, stepSize: 1 / 3 }); - - expect(controller.state.marketData).toStrictEqual({ - [ChainId.mainnet]: {}, - }); - controller.stopAllPolling(); + { + chainId, + tokenAddress: '0x0000000000000000000000000000000000000002', }, - ); + ], + currency: nativeCurrency, }); - }); - }); - it('should stop polling', async () => { - const interval = 100; - const tokenPricesService = buildMockTokenPricesService(); - jest.spyOn(tokenPricesService, 'fetchTokenPrices'); - await withController( - { - options: { - tokenPricesService, + expect(controller.state.marketData).toStrictEqual({ + [chainId]: { + '0x0000000000000000000000000000000000000000': + expect.objectContaining({ + currency: nativeCurrency, + price: 0.001, + }), + '0x0000000000000000000000000000000000000001': + expect.objectContaining({ + currency: nativeCurrency, + price: 0.002, + }), + '0x0000000000000000000000000000000000000002': + expect.objectContaining({ + currency: nativeCurrency, + price: 0.003, + }), }, - mockTokensControllerState: { - allTokens: { - '0x1': { - [defaultSelectedAddress]: [ - { - address: mockTokenAddress, - decimals: 0, - symbol: '', - aggregators: [], - }, - ], - }, + }); + }, + ); + }); + + it('does not fetch when disabled', async () => { + jest.useFakeTimers(); + const chainId = '0x1'; + + await withController( + { + options: { + disabled: true, + }, + }, + async ({ controller, triggerTokensStateChange }) => { + jest.spyOn(controller, 'updateExchangeRates'); + + triggerTokensStateChange({ + allTokens: { + [chainId]: { + [defaultSelectedAddress]: [ + { + address: '0x0000000000000000000000000000000000000001', + decimals: 0, + symbol: 'TOK1', + }, + ], }, }, - }, - async ({ controller }) => { - const pollingToken = controller.startPolling({ - chainIds: [ChainId.mainnet], - }); - await advanceTime({ clock, duration: 0 }); - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( - 1, - ); + allDetectedTokens: {}, + allIgnoredTokens: {}, + }); - controller.stopPollingByPollingToken(pollingToken); + jest.advanceTimersToNextTimer(); + await flushPromises(); - await advanceTime({ clock, duration: interval }); - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( - 1, - ); - }, - ); - }); + expect(controller.updateExchangeRates).not.toHaveBeenCalled(); + }, + ); }); - // The TokenRatesController has two methods for updating exchange rates: - // `updateExchangeRates` and `updateExchangeRatesByChainId`. They are the same - // except in how the inputs are specified. `updateExchangeRates` gets the - // inputs from controller configuration, whereas `updateExchangeRatesByChainId` - // accepts the inputs as parameters. - // - // Here we test both of these methods using the same test cases. The - // differences between them are abstracted away by the helper function - // `callUpdateExchangeRatesMethod`. - describe.each([ - 'updateExchangeRates' as const, - 'updateExchangeRatesByChainId' as const, - ])('%s', (method) => { - it('does not update state when disabled', async () => { - await withController( - {}, - async ({ - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - }) => { - const tokenAddress = '0x0000000000000000000000000000000000000001'; - controller.disable(); - await callUpdateExchangeRatesMethod({ - allTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: tokenAddress, - decimals: 18, - symbol: 'TST', - aggregators: [], - }, - ], - }, - }, - chainId: ChainId.mainnet, - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - method, - nativeCurrency: 'ETH', - selectedNetworkClientId: InfuraNetworkType.mainnet, - }); + it('does not include chains with no network configuration', async () => { + jest.useFakeTimers(); + const chainId = '0x1'; - expect(controller.state.marketData).toStrictEqual({}); + await withController( + { + mockNetworkState: { + networkConfigurationsByChainId: {}, }, - ); - }); + }, + async ({ controller, triggerTokensStateChange }) => { + jest.spyOn(controller, 'updateExchangeRates'); - it('does not update state if there are no tokens for the given chain', async () => { - await withController( - async ({ - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - }) => { - const tokenAddress = '0x0000000000000000000000000000000000000001'; - controller.enable(); - await callUpdateExchangeRatesMethod({ - allTokens: { - // These tokens are on a different chain - [toHex(2)]: { - [defaultSelectedAddress]: [ - { - address: tokenAddress, - decimals: 18, - symbol: 'TST', - aggregators: [], - }, - ], - }, - }, - chainId: ChainId.mainnet, - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - method, - nativeCurrency: 'ETH', - selectedNetworkClientId: InfuraNetworkType.mainnet, - }); - - expect(controller.state).toStrictEqual({ - marketData: { - [ChainId.mainnet]: { - '0x0000000000000000000000000000000000000000': { - currency: 'ETH', + triggerTokensStateChange({ + allTokens: { + [chainId]: { + [defaultSelectedAddress]: [ + { + address: '0x0000000000000000000000000000000000000001', + decimals: 0, + symbol: 'TOK1', }, - }, + ], }, - }); - }, - ); - }); + }, + allDetectedTokens: {}, + allIgnoredTokens: {}, + }); - it('does not update state if the price update fails', async () => { - const tokenPricesService = buildMockTokenPricesService({ - fetchTokenPrices: jest - .fn() - .mockRejectedValue(new Error('Failed to fetch')), - }); - await withController( - { options: { tokenPricesService } }, - async ({ - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - }) => { - const tokenAddress = '0x0000000000000000000000000000000000000001'; - - const updateExchangeRates = await callUpdateExchangeRatesMethod({ - allTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: tokenAddress, - decimals: 18, - symbol: 'TST', - aggregators: [], - }, - ], - }, - }, - chainId: ChainId.mainnet, - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - method, - nativeCurrency: 'ETH', - selectedNetworkClientId: InfuraNetworkType.mainnet, - }); + jest.advanceTimersToNextTimer(); + await flushPromises(); - expect(updateExchangeRates).toBeUndefined(); - expect(controller.state.marketData).toStrictEqual({}); - }, - ); - }); + expect(controller.updateExchangeRates).toHaveBeenCalledWith([]); + }, + ); + }); + }); - it('fetches rates for all tokens in batches', async () => { - const chainId = ChainId.mainnet; - const ticker = NetworksTicker.mainnet; - const tokenAddresses = [...new Array(200).keys()] - .map(buildAddress) - .sort(); - const tokenPricesService = buildMockTokenPricesService({ - fetchTokenPrices: fetchTokenPricesWithIncreasingPriceForEachToken, - }); - const fetchTokenPricesSpy = jest.spyOn( - tokenPricesService, - 'fetchTokenPrices', - ); - const tokens = tokenAddresses.map((tokenAddress) => { - return buildToken({ address: tokenAddress }); - }); - await withController( - { - options: { - tokenPricesService, - }, - }, - async ({ - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - }) => { - await callUpdateExchangeRatesMethod({ - allTokens: { - [chainId]: { - [defaultSelectedAddress]: tokens.slice(0, 100), - // Include tokens from non selected addresses - '0x0000000000000000000000000000000000000123': - tokens.slice(100), - }, - }, - chainId, - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - method, - nativeCurrency: ticker, - selectedNetworkClientId: InfuraNetworkType.mainnet, - }); - - const numBatches = Math.ceil( - tokenAddresses.length / TOKEN_PRICES_BATCH_SIZE, - ); - expect(fetchTokenPricesSpy).toHaveBeenCalledTimes(numBatches); + describe('enable', () => { + it('enables events', async () => { + jest.useFakeTimers(); - for (let i = 1; i <= numBatches; i++) { - expect(fetchTokenPricesSpy).toHaveBeenNthCalledWith(i, { - chainId, - tokenAddresses: tokenAddresses.slice( - (i - 1) * TOKEN_PRICES_BATCH_SIZE, - i * TOKEN_PRICES_BATCH_SIZE, - ), - currency: ticker, - }); - } + const chainId = '0x1'; + await withController( + { + options: { + disabled: true, }, - ); - }); + }, + async ({ controller, triggerTokensStateChange }) => { + jest.spyOn(controller, 'updateExchangeRates'); - it('updates all rates', async () => { - const tokenAddresses = [ - '0x0000000000000000000000000000000000000001', - '0x0000000000000000000000000000000000000002', - '0x0000000000000000000000000000000000000003', - ]; - const tokenPricesService = buildMockTokenPricesService({ - fetchTokenPrices: jest.fn().mockResolvedValue({ - [tokenAddresses[0]]: { - currency: 'ETH', - tokenAddress: tokenAddresses[0], - value: 0.001, - }, - [tokenAddresses[1]]: { - currency: 'ETH', - tokenAddress: tokenAddresses[1], - value: 0.002, - }, - [tokenAddresses[2]]: { - currency: 'ETH', - tokenAddress: tokenAddresses[2], - value: 0.003, - }, - }), - }); - await withController( - { options: { tokenPricesService } }, - async ({ - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - }) => { - await callUpdateExchangeRatesMethod({ - allTokens: { - [ChainId.mainnet]: { - [defaultSelectedAddress]: [ - { - address: tokenAddresses[0], - decimals: 18, - symbol: 'TST1', - aggregators: [], - }, - { - address: tokenAddresses[1], - decimals: 18, - symbol: 'TST2', - aggregators: [], - }, - ], - // Include tokens from non selected addresses - '0x0000000000000000000000000000000000000123': [ - { - address: tokenAddresses[2], - decimals: 18, - symbol: 'TST1', - aggregators: [], - }, - ], - }, - }, - chainId: ChainId.mainnet, - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - method, - nativeCurrency: 'ETH', - 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, - }, - }, - }, - } - `); - }, - ); - }); + controller.enable(); - if (method === 'updateExchangeRatesByChainId') { - it('updates rates only for a non-selected chain', async () => { - const tokenAddresses = [ - '0x0000000000000000000000000000000000000001', - '0x0000000000000000000000000000000000000002', - ]; - const tokenPricesService = buildMockTokenPricesService({ - fetchTokenPrices: jest.fn().mockResolvedValue({ - [tokenAddresses[0]]: { - currency: 'ETH', - tokenAddress: tokenAddresses[0], - value: 0.001, - }, - [tokenAddresses[1]]: { - currency: 'ETH', - tokenAddress: tokenAddresses[1], - value: 0.002, - }, - }), - }); - await withController( - { options: { tokenPricesService } }, - async ({ - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - }) => { - await callUpdateExchangeRatesMethod({ - allTokens: { - [toHex(2)]: { - [defaultSelectedAddress]: [ - { - address: tokenAddresses[0], - decimals: 18, - symbol: 'TST1', - aggregators: [], - }, - { - address: tokenAddresses[1], - decimals: 18, - symbol: 'TST2', - aggregators: [], - }, - ], + triggerTokensStateChange({ + allTokens: { + [chainId]: { + [defaultSelectedAddress]: [ + { + address: '0x0000000000000000000000000000000000000001', + decimals: 0, + symbol: 'TOK1', }, - }, - chainId: toHex(2), - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - method, - nativeCurrency: 'ETH', - 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, - }, - }, - }, - } - `); + ], + }, }, - ); - }); - } - - it('updates exchange rates when native currency is not supported by the Price API', async () => { - const selectedNetworkClientId = 'AAAA-BBBB-CCCC-DDDD'; - const selectedNetworkClientConfiguration = - buildCustomNetworkClientConfiguration({ - chainId: toHex(137), - ticker: 'UNSUPPORTED', + allDetectedTokens: {}, + allIgnoredTokens: {}, }); - const tokenAddresses = [ - '0x0000000000000000000000000000000000000001', - '0x0000000000000000000000000000000000000002', - ]; - const tokenPricesService = buildMockTokenPricesService({ - fetchTokenPrices: jest.fn().mockResolvedValue({ - [tokenAddresses[0]]: { - currency: 'ETH', - tokenAddress: tokenAddresses[0], - price: 0.001, - }, - [tokenAddresses[1]]: { - currency: 'ETH', - tokenAddress: tokenAddresses[1], - price: 0.002, - }, - }), - validateCurrencySupported(_currency: unknown): _currency is string { - return false; - }, - }); - nock('https://min-api.cryptocompare.com') - .get('/data/price') - .query({ - fsym: 'ETH', - tsyms: selectedNetworkClientConfiguration.ticker, - }) - .reply(200, { [selectedNetworkClientConfiguration.ticker]: 0.5 }); // .5 eth to 1 matic - - await withController( - { - options: { tokenPricesService }, - mockNetworkClientConfigurationsByNetworkClientId: { - [selectedNetworkClientId]: selectedNetworkClientConfiguration, - }, - }, - async ({ - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - }) => { - await callUpdateExchangeRatesMethod({ - allTokens: { - [selectedNetworkClientConfiguration.chainId]: { - [defaultSelectedAddress]: [ - { - address: tokenAddresses[0], - decimals: 18, - symbol: 'TST1', - aggregators: [], - }, - { - address: tokenAddresses[1], - decimals: 18, - symbol: 'TST2', - aggregators: [], - }, - ], - }, - }, - chainId: selectedNetworkClientConfiguration.chainId, - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - method, - nativeCurrency: selectedNetworkClientConfiguration.ticker, - selectedNetworkClientId, - }); - - // 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, - }, - }, - }, - } - `); - }, - ); - }); - it('fetches rates for all tokens in batches when native currency is not supported by the Price API', async () => { - const selectedNetworkClientId = 'AAAA-BBBB-CCCC-DDDD'; - const selectedNetworkClientConfiguration = - buildCustomNetworkClientConfiguration({ - chainId: toHex(999), - ticker: 'UNSUPPORTED', - }); - const tokenAddresses = [...new Array(200).keys()] - .map(buildAddress) - .sort(); - const tokenPricesService = buildMockTokenPricesService({ - fetchTokenPrices: fetchTokenPricesWithIncreasingPriceForEachToken, - validateCurrencySupported: ( - currency: unknown, - ): currency is string => { - return currency !== selectedNetworkClientConfiguration.ticker; - }, - }); - const fetchTokenPricesSpy = jest.spyOn( - tokenPricesService, - 'fetchTokenPrices', - ); - const tokens = tokenAddresses.map((tokenAddress) => { - return buildToken({ address: tokenAddress }); - }); - nock('https://min-api.cryptocompare.com') - .get('/data/price') - .query({ - fsym: 'ETH', - tsyms: selectedNetworkClientConfiguration.ticker, - }) - .reply(200, { [selectedNetworkClientConfiguration.ticker]: 0.5 }); - await withController( - { - options: { - tokenPricesService, - }, - mockNetworkClientConfigurationsByNetworkClientId: { - [selectedNetworkClientId]: selectedNetworkClientConfiguration, - }, - mockNetworkState: { - networkConfigurationsByChainId: { - [selectedNetworkClientConfiguration.chainId]: { - nativeCurrency: selectedNetworkClientConfiguration.ticker, - chainId: selectedNetworkClientConfiguration.chainId, - name: 'UNSUPPORTED', - rpcEndpoints: [], - blockExplorerUrls: [], - defaultRpcEndpointIndex: 0, - }, - }, - selectedNetworkClientId, - }, - }, - async ({ - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - }) => { - await callUpdateExchangeRatesMethod({ - allTokens: { - [selectedNetworkClientConfiguration.chainId]: { - [defaultSelectedAddress]: tokens, - }, - }, - chainId: selectedNetworkClientConfiguration.chainId, - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - method, - nativeCurrency: selectedNetworkClientConfiguration.ticker, - selectedNetworkClientId, - }); - - const numBatches = Math.ceil( - tokenAddresses.length / TOKEN_PRICES_BATCH_SIZE, - ); - expect(fetchTokenPricesSpy).toHaveBeenCalledTimes(numBatches); - - for (let i = 1; i <= numBatches; i++) { - expect(fetchTokenPricesSpy).toHaveBeenNthCalledWith(i, { - chainId: selectedNetworkClientConfiguration.chainId, - tokenAddresses: tokenAddresses.slice( - (i - 1) * TOKEN_PRICES_BATCH_SIZE, - i * TOKEN_PRICES_BATCH_SIZE, - ), - currency: 'ETH', - }); - } - }, - ); - }); + jest.advanceTimersToNextTimer(); + await flushPromises(); - it('sets rates to undefined when chain is not supported by the Price API', async () => { - const selectedNetworkClientId = 'AAAA-BBBB-CCCC-DDDD'; - const selectedNetworkClientConfiguration = - buildCustomNetworkClientConfiguration({ - chainId: toHex(999), - ticker: 'TST', - }); - const tokenAddresses = [ - '0x0000000000000000000000000000000000000001', - '0x0000000000000000000000000000000000000002', - ]; - const tokenPricesService = buildMockTokenPricesService({ - fetchTokenPrices: jest.fn().mockResolvedValue({ - [tokenAddresses[0]]: { - currency: 'ETH', - tokenAddress: tokenAddresses[0], - value: 0.001, - }, - [tokenAddresses[1]]: { - currency: 'ETH', - tokenAddress: tokenAddresses[1], - value: 0.002, - }, - }), - validateChainIdSupported(_chainId: unknown): _chainId is Hex { - return false; - }, - }); - await withController( - { - options: { tokenPricesService }, - mockNetworkClientConfigurationsByNetworkClientId: { - [selectedNetworkClientId]: selectedNetworkClientConfiguration, + expect(controller.updateExchangeRates).toHaveBeenCalledWith([ + { + chainId, + nativeCurrency: 'ETH', }, - }, - async ({ - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - }) => { - await callUpdateExchangeRatesMethod({ - allTokens: { - [toHex(999)]: { - [defaultSelectedAddress]: [ - { - address: tokenAddresses[0], - decimals: 18, - symbol: 'TST1', - aggregators: [], - }, - { - address: tokenAddresses[1], - decimals: 18, - symbol: 'TST2', - aggregators: [], - }, - ], - }, - }, - chainId: selectedNetworkClientConfiguration.chainId, - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - method, - nativeCurrency: selectedNetworkClientConfiguration.ticker, - selectedNetworkClientId, - }); - - expect(controller.state).toMatchInlineSnapshot(` - Object { - "marketData": Object { - "0x3e7": Object { - "0x0000000000000000000000000000000000000001": undefined, - "0x0000000000000000000000000000000000000002": undefined, - }, - }, - } - `); - }, - ); - }); + ]); + }, + ); + }); + }); - it('correctly calls the Price API with unqiue native token addresses (e.g. MATIC)', async () => { - const tokenPricesService = buildMockTokenPricesService({ - fetchTokenPrices: jest.fn().mockResolvedValue({ - '0x0000000000000000000000000000000000001010': { - currency: 'MATIC', - tokenAddress: '0x0000000000000000000000000000000000001010', - value: 0.001, - }, - }), - }); + describe('disable', () => { + it('disables events', async () => { + jest.useFakeTimers(); - await withController( - { - options: { tokenPricesService }, - mockNetworkClientConfigurationsByNetworkClientId: { - 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - chainId: '0x89', - }), - }, - }, - async ({ - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - }) => { - await callUpdateExchangeRatesMethod({ - allTokens: { - '0x89': { - [defaultSelectedAddress]: [], - }, - }, - chainId: '0x89', - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - method, - nativeCurrency: 'MATIC', - selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', - }); - - expect( - controller.state.marketData['0x89'][ - '0x0000000000000000000000000000000000001010' - ], - ).toBeDefined(); + const chainId = '0x1'; + await withController( + { + options: { + disabled: false, }, - ); - }); + }, + async ({ controller, triggerTokensStateChange }) => { + jest.spyOn(controller, 'updateExchangeRates'); - it('only updates rates once when called twice', 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 tokenPricesService = buildMockTokenPricesService({ - fetchTokenPrices: fetchTokenPricesMock, - }); - await withController( - { options: { tokenPricesService } }, - async ({ - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - }) => { - const updateExchangeRates = async () => - await callUpdateExchangeRatesMethod({ - allTokens: { - [toHex(1)]: { - [defaultSelectedAddress]: [ - { - address: tokenAddresses[0], - decimals: 18, - symbol: 'TST1', - aggregators: [], - }, - { - address: tokenAddresses[1], - decimals: 18, - symbol: 'TST2', - aggregators: [], - }, - ], - }, - }, - chainId: ChainId.mainnet, - selectedNetworkClientId: InfuraNetworkType.mainnet, - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - method, - nativeCurrency: 'ETH', - }); - - await Promise.all([updateExchangeRates(), updateExchangeRates()]); - - 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, - }, - }, - }, - } - `); - }, - ); - }); + controller.disable(); - it('will update rates twice if detected tokens increased during second call', 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 tokenPricesService = buildMockTokenPricesService({ - fetchTokenPrices: fetchTokenPricesMock, - }); - await withController( - { options: { tokenPricesService } }, - async ({ - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - }) => { - const request1Payload = [ - { - address: tokenAddresses[0], - decimals: 18, - symbol: 'TST1', - aggregators: [], - }, - ]; - const request2Payload = [ - { - address: tokenAddresses[0], - decimals: 18, - symbol: 'TST1', - aggregators: [], - }, - { - address: tokenAddresses[1], - decimals: 18, - symbol: 'TST2', - aggregators: [], - }, - ]; - const updateExchangeRates = async ( - tokens: typeof request1Payload | typeof request2Payload, - ) => - await callUpdateExchangeRatesMethod({ - allTokens: { - [toHex(1)]: { - [defaultSelectedAddress]: tokens, + triggerTokensStateChange({ + allTokens: { + [chainId]: { + [defaultSelectedAddress]: [ + { + address: '0x0000000000000000000000000000000000000001', + decimals: 0, + symbol: 'TOK1', }, - }, - chainId: ChainId.mainnet, - selectedNetworkClientId: InfuraNetworkType.mainnet, - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - method, - nativeCurrency: 'ETH', - }); - - await Promise.all([ - updateExchangeRates(request1Payload), - updateExchangeRates(request2Payload), - ]); - - expect(fetchTokenPricesMock).toHaveBeenCalledTimes(2); - expect(fetchTokenPricesMock).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - tokenAddresses: [tokenAddresses[0]], - }), - ); - expect(fetchTokenPricesMock).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - tokenAddresses: [tokenAddresses[0], tokenAddresses[1]], - }), - ); - }, - ); - }); + ], + }, + }, + allDetectedTokens: {}, + allIgnoredTokens: {}, + }); + + jest.advanceTimersToNextTimer(); + await flushPromises(); + + expect(controller.updateExchangeRates).not.toHaveBeenCalled(); + }, + ); }); }); + // describe('TokensController::stateChange', () => { + // let clock: sinon.SinonFakeTimers; + + // beforeEach(() => { + // clock = useFakeTimers({ now: Date.now() }); + // }); + + // afterEach(() => { + // clock.restore(); + // }); + + // describe('when legacy polling is active', () => { + // it('should update exchange rates when any of the addresses in the "all tokens" collection change', async () => { + // const tokenAddresses = ['0xE1', '0xE2']; + // await withController( + // { + // mockTokensControllerState: { + // allTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: tokenAddresses[0], + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }, + // }, + // async ({ controller, triggerTokensStateChange }) => { + // const updateExchangeRatesSpy = jest + // .spyOn(controller, 'updateExchangeRates') + // .mockResolvedValue(); + // await controller.start(ChainId.mainnet, 'ETH'); + // triggerTokensStateChange({ + // ...getDefaultTokensState(), + // allTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: tokenAddresses[1], + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }); + + // // Once when starting, and another when tokens state changes + // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(2); + // }, + // ); + // }); + + // it('should update exchange rates when any of the addresses in the "all tokens" collection change with invalid addresses', async () => { + // const tokenAddresses = ['0xinvalidAddress']; + // await withController( + // { + // mockTokensControllerState: { + // allTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: tokenAddresses[0], + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }, + // }, + // async ({ controller, triggerTokensStateChange }) => { + // const updateExchangeRatesSpy = jest + // .spyOn(controller, 'updateExchangeRates') + // .mockResolvedValue(); + // await controller.start(ChainId.mainnet, 'ETH'); + // triggerTokensStateChange({ + // ...getDefaultTokensState(), + // allTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: tokenAddresses[1], + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }); + + // // Once when starting, and another when tokens state changes + // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(2); + // }, + // ); + // }); + + // it('should update exchange rates when any of the addresses in the "all detected tokens" collection change', async () => { + // const tokenAddresses = ['0xE1', '0xE2']; + // await withController( + // { + // mockTokensControllerState: { + // allDetectedTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: tokenAddresses[0], + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }, + // }, + // async ({ controller, triggerTokensStateChange }) => { + // const updateExchangeRatesSpy = jest + // .spyOn(controller, 'updateExchangeRates') + // .mockResolvedValue(); + // await controller.start(ChainId.mainnet, 'ETH'); + // triggerTokensStateChange({ + // ...getDefaultTokensState(), + // allDetectedTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: tokenAddresses[1], + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }); + // // Once when starting, and another when tokens state changes + // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(2); + // }, + // ); + // }); + + // it('should not update exchange rates if both the "all tokens" or "all detected tokens" are exactly the same', async () => { + // const tokensState = { + // allTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: mockTokenAddress, + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }; + // await withController( + // { + // mockTokensControllerState: { + // ...tokensState, + // }, + // }, + // async ({ controller, triggerTokensStateChange }) => { + // const updateExchangeRatesSpy = jest + // .spyOn(controller, 'updateExchangeRates') + // .mockResolvedValue(); + // await controller.start(ChainId.mainnet, 'ETH'); + // triggerTokensStateChange({ + // ...getDefaultTokensState(), + // ...tokensState, + // }); + + // // Once when starting, and that's it + // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); + // }, + // ); + // }); + + // it('should not update exchange rates if all of the tokens in "all tokens" just move to "all detected tokens"', async () => { + // const tokens = { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: mockTokenAddress, + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }; + // await withController( + // { + // mockTokensControllerState: { + // allTokens: tokens, + // }, + // }, + // async ({ controller, triggerTokensStateChange }) => { + // const updateExchangeRatesSpy = jest + // .spyOn(controller, 'updateExchangeRates') + // .mockResolvedValue(); + // await controller.start(ChainId.mainnet, 'ETH'); + // triggerTokensStateChange({ + // ...getDefaultTokensState(), + // allDetectedTokens: tokens, + // }); + + // // Once when starting, and that's it + // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); + // }, + // ); + // }); + + // it('should not update exchange rates if a new token is added to "all detected tokens" but is already present in "all tokens"', async () => { + // const tokens = { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: mockTokenAddress, + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }; + // await withController( + // { + // mockTokensControllerState: { + // allTokens: tokens, + // }, + // }, + // async ({ controller, triggerTokensStateChange }) => { + // const updateExchangeRatesSpy = jest + // .spyOn(controller, 'updateExchangeRates') + // .mockResolvedValue(); + // await controller.start(ChainId.mainnet, 'ETH'); + // triggerTokensStateChange({ + // ...getDefaultTokensState(), + // allTokens: tokens, + // allDetectedTokens: tokens, + // }); + + // // Once when starting, and that's it + // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); + // }, + // ); + // }); + + // it('should not update exchange rates if a new token is added to "all tokens" but is already present in "all detected tokens"', async () => { + // const tokens = { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: mockTokenAddress, + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }; + // await withController( + // { + // mockTokensControllerState: { + // allDetectedTokens: tokens, + // }, + // }, + // async ({ controller, triggerTokensStateChange }) => { + // const updateExchangeRatesSpy = jest + // .spyOn(controller, 'updateExchangeRates') + // .mockResolvedValue(); + // await controller.start(ChainId.mainnet, 'ETH'); + // triggerTokensStateChange({ + // ...getDefaultTokensState(), + // allTokens: tokens, + // allDetectedTokens: tokens, + // }); + + // // Once when starting, and that's it + // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); + // }, + // ); + // }); + + // it('should not update exchange rates if none of the addresses in "all tokens" or "all detected tokens" change, even if other parts of the token change', async () => { + // await withController( + // { + // mockTokensControllerState: { + // ...getDefaultTokensState(), + // allDetectedTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: mockTokenAddress, + // decimals: 3, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }, + // }, + // async ({ controller, triggerTokensStateChange }) => { + // const updateExchangeRatesSpy = jest + // .spyOn(controller, 'updateExchangeRates') + // .mockResolvedValue(); + // await controller.start(ChainId.mainnet, 'ETH'); + // triggerTokensStateChange({ + // ...getDefaultTokensState(), + // allDetectedTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: mockTokenAddress, + // decimals: 7, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }); + + // // Once when starting, and that's it + // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); + // }, + // ); + // }); + + // it('should not update exchange rates if none of the addresses in "all tokens" or "all detected tokens" change, when normalized to checksum addresses', async () => { + // await withController( + // { + // mockTokensControllerState: { + // allDetectedTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: '0x0EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE2', + // decimals: 3, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }, + // }, + // async ({ controller, triggerTokensStateChange }) => { + // const updateExchangeRatesSpy = jest + // .spyOn(controller, 'updateExchangeRates') + // .mockResolvedValue(); + // await controller.start(ChainId.mainnet, 'ETH'); + // triggerTokensStateChange({ + // ...getDefaultTokensState(), + // allDetectedTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: '0x0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2', + // decimals: 7, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }); + + // // Once when starting, and that's it + // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); + // }, + // ); + // }); + + // it('should not update exchange rates if any of the addresses in "all tokens" or "all detected tokens" merely change order', async () => { + // await withController( + // { + // mockTokensControllerState: { + // allDetectedTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: '0xE1', + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // { + // address: '0xE2', + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }, + // }, + // async ({ controller, triggerTokensStateChange }) => { + // const updateExchangeRatesSpy = jest + // .spyOn(controller, 'updateExchangeRates') + // .mockResolvedValue(); + // await controller.start(ChainId.mainnet, 'ETH'); + // triggerTokensStateChange({ + // ...getDefaultTokensState(), + // allDetectedTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: '0xE2', + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // { + // address: '0xE1', + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }); + + // // Once when starting, and that's it + // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); + // }, + // ); + // }); + // }); + + // describe('when legacy polling is inactive', () => { + // it('should not update exchange rates when any of the addresses in the "all tokens" collection change', async () => { + // const tokenAddresses = ['0xE1', '0xE2']; + // await withController( + // { + // mockTokensControllerState: { + // allTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: tokenAddresses[0], + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }, + // }, + // async ({ controller, triggerTokensStateChange }) => { + // const updateExchangeRatesSpy = jest + // .spyOn(controller, 'updateExchangeRates') + // .mockResolvedValue(); + // triggerTokensStateChange({ + // ...getDefaultTokensState(), + // allTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: tokenAddresses[1], + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }); + + // expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); + // }, + // ); + // }); + + // it('should not update exchange rates when any of the addresses in the "all detected tokens" collection change', async () => { + // const tokenAddresses = ['0xE1', '0xE2']; + // await withController( + // { + // mockTokensControllerState: { + // allDetectedTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: tokenAddresses[0], + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }, + // }, + // async ({ controller, triggerTokensStateChange }) => { + // const updateExchangeRatesSpy = jest + // .spyOn(controller, 'updateExchangeRates') + // .mockResolvedValue(); + // triggerTokensStateChange({ + // ...getDefaultTokensState(), + // allDetectedTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: tokenAddresses[1], + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }); + + // expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); + // }, + // ); + // }); + // }); + // }); + + // describe('NetworkController::stateChange', () => { + // let clock: sinon.SinonFakeTimers; + + // beforeEach(() => { + // clock = useFakeTimers({ now: Date.now() }); + // }); + + // afterEach(() => { + // clock.restore(); + // }); + + // describe('when polling is active', () => { + // it('should update exchange rates when ticker changes', async () => { + // await withController( + // { + // options: { + // interval: 100, + // }, + // mockNetworkClientConfigurationsByNetworkClientId: { + // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + // chainId: toHex(1337), + // ticker: 'NEW', + // }), + // }, + // }, + // async ({ controller, triggerNetworkStateChange }) => { + // await controller.start(ChainId.mainnet, 'ETH'); + // const updateExchangeRatesSpy = jest + // .spyOn(controller, 'updateExchangeRates') + // .mockResolvedValue(); + // triggerNetworkStateChange({ + // ...getDefaultNetworkControllerState(), + // selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', + // }); + + // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); + // }, + // ); + // }); + + // it('should not update exchange rates when chain ID changes', async () => { + // await withController( + // { + // options: { + // interval: 100, + // }, + // mockNetworkClientConfigurationsByNetworkClientId: { + // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + // chainId: toHex(1338), + // ticker: 'TEST', + // }), + // }, + // }, + // async ({ controller, triggerNetworkStateChange }) => { + // await controller.start(ChainId.mainnet, 'ETH'); + // const updateExchangeRatesSpy = jest + // .spyOn(controller, 'updateExchangeRates') + // .mockResolvedValue(); + // triggerNetworkStateChange({ + // ...getDefaultNetworkControllerState(), + // selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', + // }); + + // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); + // }, + // ); + // }); + + // it('should clear marketData in state when event is triggeredclear', async () => { + // await withController( + // { + // options: { + // interval: 100, + // state: { + // marketData: { + // [ChainId.mainnet]: { + // '0x02': { + // currency: 'ETH', + // priceChange1d: 0, + // pricePercentChange1d: 0, + // tokenAddress: '0x02', + // allTimeHigh: 4000, + // allTimeLow: 900, + // circulatingSupply: 2000, + // dilutedMarketCap: 100, + // high1d: 200, + // low1d: 100, + // marketCap: 1000, + // marketCapPercentChange1d: 100, + // price: 0.001, + // pricePercentChange14d: 100, + // pricePercentChange1h: 1, + // pricePercentChange1y: 200, + // pricePercentChange200d: 300, + // pricePercentChange30d: 200, + // pricePercentChange7d: 100, + // totalVolume: 100, + // }, + // }, + // }, + // }, + // }, + // mockNetworkClientConfigurationsByNetworkClientId: { + // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + // chainId: toHex(1337), + // ticker: 'NEW', + // }), + // }, + // }, + // async ({ controller, triggerNetworkStateChange }) => { + // await controller.start(ChainId.mainnet, 'ETH'); + // jest.spyOn(controller, 'updateExchangeRates').mockResolvedValue(); + // triggerNetworkStateChange({ + // ...getDefaultNetworkControllerState(), + // selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', + // }); + + // expect(controller.state.marketData).toStrictEqual({ + // '0x1': {}, + // }); + // }, + // ); + // }); + + // it('should update exchange rates when network state changes without adding a new network', async () => { + // await withController( + // { + // options: { + // interval: 100, + // }, + // mockNetworkClientConfigurationsByNetworkClientId: { + // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + // chainId: ChainId.mainnet, + // ticker: NetworksTicker.mainnet, + // }), + // }, + // }, + // async ({ controller, triggerNetworkStateChange }) => { + // await controller.start(ChainId.mainnet, 'ETH'); + // const updateExchangeRatesSpy = jest + // .spyOn(controller, 'updateExchangeRates') + // .mockResolvedValue(); + // triggerNetworkStateChange( + // { + // ...getDefaultNetworkControllerState(), + // selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', + // }, + // [ + // { + // op: 'add', + // path: ['networkConfigurationsByChainId', ChainId.mainnet], + // }, + // ], + // ); + // expect(updateExchangeRatesSpy).toHaveBeenCalled(); + // }, + // ); + // }); + // }); + + // describe('when polling is inactive', () => { + // it('should not update exchange rates when ticker changes', async () => { + // await withController( + // { + // options: { + // interval: 100, + // }, + // mockNetworkClientConfigurationsByNetworkClientId: { + // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + // chainId: toHex(1337), + // ticker: 'NEW', + // }), + // }, + // }, + // async ({ controller, triggerNetworkStateChange }) => { + // const updateExchangeRatesSpy = jest + // .spyOn(controller, 'updateExchangeRates') + // .mockResolvedValue(); + // triggerNetworkStateChange({ + // ...getDefaultNetworkControllerState(), + // selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', + // }); + + // expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); + // }, + // ); + // }); + + // it('should not update exchange rates when chain ID changes', async () => { + // await withController( + // { + // options: { + // interval: 100, + // }, + // mockNetworkClientConfigurationsByNetworkClientId: { + // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + // chainId: toHex(1338), + // ticker: 'TEST', + // }), + // }, + // }, + // async ({ controller, triggerNetworkStateChange }) => { + // const updateExchangeRatesSpy = jest + // .spyOn(controller, 'updateExchangeRates') + // .mockResolvedValue(); + // triggerNetworkStateChange({ + // ...getDefaultNetworkControllerState(), + // selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', + // }); + + // expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); + // }, + // ); + // }); + + // it('should not clear marketData state when ticker changes', async () => { + // await withController( + // { + // options: { + // interval: 100, + // state: { + // marketData: { + // [ChainId.mainnet]: { + // '0x02': { + // currency: 'ETH', + // priceChange1d: 0, + // pricePercentChange1d: 0, + // tokenAddress: '0x02', + // allTimeHigh: 4000, + // allTimeLow: 900, + // circulatingSupply: 2000, + // dilutedMarketCap: 100, + // high1d: 200, + // low1d: 100, + // marketCap: 1000, + // marketCapPercentChange1d: 100, + // price: 0.001, + // pricePercentChange14d: 100, + // pricePercentChange1h: 1, + // pricePercentChange1y: 200, + // pricePercentChange200d: 300, + // pricePercentChange30d: 200, + // pricePercentChange7d: 100, + // totalVolume: 100, + // }, + // }, + // }, + // }, + // }, + // mockNetworkClientConfigurationsByNetworkClientId: { + // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + // chainId: toHex(1337), + // ticker: 'NEW', + // }), + // }, + // }, + // async ({ controller, triggerNetworkStateChange }) => { + // jest.spyOn(controller, 'updateExchangeRates').mockResolvedValue(); + // triggerNetworkStateChange({ + // ...getDefaultNetworkControllerState(), + // selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', + // }); + + // expect(controller.state.marketData).toStrictEqual({ + // '0x1': { + // '0x02': { + // currency: 'ETH', + // priceChange1d: 0, + // pricePercentChange1d: 0, + // tokenAddress: '0x02', + // allTimeHigh: 4000, + // allTimeLow: 900, + // circulatingSupply: 2000, + // dilutedMarketCap: 100, + // high1d: 200, + // low1d: 100, + // marketCap: 1000, + // marketCapPercentChange1d: 100, + // price: 0.001, + // pricePercentChange14d: 100, + // pricePercentChange1h: 1, + // pricePercentChange1y: 200, + // pricePercentChange200d: 300, + // pricePercentChange30d: 200, + // pricePercentChange7d: 100, + // totalVolume: 100, + // }, + // }, + // }); + // }, + // ); + // }); + + // it('should not clear marketData state when chain ID changes', async () => { + // await withController( + // { + // options: { + // interval: 100, + // state: { + // marketData: { + // [ChainId.mainnet]: { + // '0x02': { + // currency: 'ETH', + // priceChange1d: 0, + // pricePercentChange1d: 0, + // tokenAddress: '0x02', + // allTimeHigh: 4000, + // allTimeLow: 900, + // circulatingSupply: 2000, + // dilutedMarketCap: 100, + // high1d: 200, + // low1d: 100, + // marketCap: 1000, + // marketCapPercentChange1d: 100, + // price: 0.001, + // pricePercentChange14d: 100, + // pricePercentChange1h: 1, + // pricePercentChange1y: 200, + // pricePercentChange200d: 300, + // pricePercentChange30d: 200, + // pricePercentChange7d: 100, + // totalVolume: 100, + // }, + // }, + // }, + // }, + // }, + // mockNetworkClientConfigurationsByNetworkClientId: { + // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + // chainId: toHex(1338), + // ticker: 'TEST', + // }), + // }, + // }, + // async ({ controller, triggerNetworkStateChange }) => { + // jest.spyOn(controller, 'updateExchangeRates').mockResolvedValue(); + // triggerNetworkStateChange({ + // ...getDefaultNetworkControllerState(), + // selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', + // }); + + // expect(controller.state.marketData).toStrictEqual({ + // '0x1': { + // '0x02': { + // currency: 'ETH', + // priceChange1d: 0, + // pricePercentChange1d: 0, + // tokenAddress: '0x02', + // allTimeHigh: 4000, + // allTimeLow: 900, + // circulatingSupply: 2000, + // dilutedMarketCap: 100, + // high1d: 200, + // low1d: 100, + // marketCap: 1000, + // marketCapPercentChange1d: 100, + // price: 0.001, + // pricePercentChange14d: 100, + // pricePercentChange1h: 1, + // pricePercentChange1y: 200, + // pricePercentChange200d: 300, + // pricePercentChange30d: 200, + // pricePercentChange7d: 100, + // totalVolume: 100, + // }, + // }, + // }); + // }, + // ); + // }); + // }); + + // it('removes state when networks are deleted', async () => { + // const marketData = { + // [ChainId.mainnet]: { + // '0x123456': { + // currency: 'ETH', + // priceChange1d: 0, + // pricePercentChange1d: 0, + // tokenAddress: '0x02', + // allTimeHigh: 4000, + // allTimeLow: 900, + // circulatingSupply: 2000, + // dilutedMarketCap: 100, + // high1d: 200, + // low1d: 100, + // marketCap: 1000, + // marketCapPercentChange1d: 100, + // price: 0.001, + // pricePercentChange14d: 100, + // pricePercentChange1h: 1, + // pricePercentChange1y: 200, + // pricePercentChange200d: 300, + // pricePercentChange30d: 200, + // pricePercentChange7d: 100, + // totalVolume: 100, + // }, + // }, + // [ChainId['linea-mainnet']]: { + // '0x789': { + // currency: 'ETH', + // priceChange1d: 0, + // pricePercentChange1d: 0, + // tokenAddress: '0x02', + // allTimeHigh: 4000, + // allTimeLow: 900, + // circulatingSupply: 2000, + // dilutedMarketCap: 100, + // high1d: 200, + // low1d: 100, + // marketCap: 1000, + // marketCapPercentChange1d: 100, + // price: 0.001, + // pricePercentChange14d: 100, + // pricePercentChange1h: 1, + // pricePercentChange1y: 200, + // pricePercentChange200d: 300, + // pricePercentChange30d: 200, + // pricePercentChange7d: 100, + // totalVolume: 100, + // }, + // }, + // } as const; + + // await withController( + // { + // options: { + // state: { + // marketData, + // }, + // }, + // }, + // async ({ controller, triggerNetworkStateChange }) => { + // // Verify initial state with both networks + // expect(controller.state.marketData).toStrictEqual(marketData); + + // triggerNetworkStateChange( + // { + // selectedNetworkClientId: 'mainnet', + // networkConfigurationsByChainId: {}, + // } as NetworkState, + // [ + // { + // op: 'remove', + // path: [ + // 'networkConfigurationsByChainId', + // ChainId['linea-mainnet'], + // ], + // }, + // ], + // ); + + // // Verify linea removed + // expect(controller.state.marketData).toStrictEqual({ + // [ChainId.mainnet]: marketData[ChainId.mainnet], + // }); + // }, + // ); + // }); + // }); + + // describe('PreferencesController::stateChange', () => { + // let clock: sinon.SinonFakeTimers; + + // beforeEach(() => { + // clock = useFakeTimers({ now: Date.now() }); + // }); + + // afterEach(() => { + // clock.restore(); + // }); + + // describe('when polling is active', () => { + // it('should not update exchange rates when selected address changes', async () => { + // const alternateSelectedAddress = + // '0x0000000000000000000000000000000000000002'; + // const alternateSelectedAccount = createMockInternalAccount({ + // address: alternateSelectedAddress, + // }); + // await withController( + // { + // options: { + // interval: 100, + // }, + // mockTokensControllerState: { + // allTokens: { + // '0x1': { + // [alternateSelectedAddress]: [ + // { + // address: '0x02', + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // { + // address: '0x03', + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }, + // }, + // async ({ controller, triggerSelectedAccountChange }) => { + // await controller.start(ChainId.mainnet, 'ETH'); + // const updateExchangeRatesSpy = jest + // .spyOn(controller, 'updateExchangeRates') + // .mockResolvedValue(); + // triggerSelectedAccountChange(alternateSelectedAccount); + + // expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); + // }, + // ); + // }); + // }); + + // describe('when polling is inactive', () => { + // it('does not update exchange rates when selected account changes', async () => { + // const alternateSelectedAddress = + // '0x0000000000000000000000000000000000000002'; + // const alternateSelectedAccount = createMockInternalAccount({ + // address: alternateSelectedAddress, + // }); + // await withController( + // { + // options: { + // interval: 100, + // }, + // mockTokensControllerState: { + // allTokens: { + // '0x1': { + // [alternateSelectedAddress]: [ + // { + // address: '0x02', + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // { + // address: '0x03', + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }, + // }, + // async ({ controller, triggerSelectedAccountChange }) => { + // const updateExchangeRatesSpy = jest + // .spyOn(controller, 'updateExchangeRates') + // .mockResolvedValue(); + // triggerSelectedAccountChange(alternateSelectedAccount); + + // expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); + // }, + // ); + // }); + // }); + // }); + + // describe('legacy polling', () => { + // let clock: sinon.SinonFakeTimers; + + // beforeEach(() => { + // clock = useFakeTimers({ now: Date.now() }); + // }); + + // afterEach(() => { + // clock.restore(); + // }); + + // describe('start', () => { + // it('should poll and update rate in the right interval', async () => { + // const interval = 100; + // const tokenPricesService = buildMockTokenPricesService(); + // jest.spyOn(tokenPricesService, 'fetchTokenPrices'); + // await withController( + // { + // options: { + // interval, + // tokenPricesService, + // }, + // mockTokensControllerState: { + // allTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: mockTokenAddress, + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }, + // }, + // async ({ controller }) => { + // await controller.start(ChainId.mainnet, 'ETH'); + + // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( + // 1, + // ); + + // await advanceTime({ clock, duration: interval }); + // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( + // 2, + // ); + + // await advanceTime({ clock, duration: interval }); + // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( + // 3, + // ); + // }, + // ); + // }); + // }); + + // describe('stop', () => { + // it('should stop polling', async () => { + // const interval = 100; + // const tokenPricesService = buildMockTokenPricesService(); + // jest.spyOn(tokenPricesService, 'fetchTokenPrices'); + // await withController( + // { + // options: { + // interval, + // tokenPricesService, + // }, + // mockTokensControllerState: { + // allTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: mockTokenAddress, + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }, + // }, + // async ({ controller }) => { + // await controller.start(ChainId.mainnet, 'ETH'); + + // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( + // 1, + // ); + + // controller.stop(); + + // await advanceTime({ clock, duration: interval }); + // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( + // 1, + // ); + // }, + // ); + // }); + // }); + // }); + + // describe('polling by networkClientId', () => { + // let clock: sinon.SinonFakeTimers; + + // beforeEach(() => { + // clock = useFakeTimers({ now: Date.now() }); + // }); + + // afterEach(() => { + // clock.restore(); + // }); + + // it('should poll on the right interval', async () => { + // const interval = 100; + // const tokenPricesService = buildMockTokenPricesService(); + // jest.spyOn(tokenPricesService, 'fetchTokenPrices'); + // await withController( + // { + // options: { + // interval, + // tokenPricesService, + // }, + // mockTokensControllerState: { + // allTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: mockTokenAddress, + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }, + // }, + // async ({ controller }) => { + // controller.startPolling({ + // chainIds: [ChainId.mainnet], + // }); + + // await advanceTime({ clock, duration: 0 }); + // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); + + // await advanceTime({ clock, duration: interval }); + // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(2); + + // await advanceTime({ clock, duration: interval }); + // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(3); + // }, + // ); + // }); + + // describe('updating state on poll', () => { + // describe('when the native currency is supported', () => { + // it('returns the exchange rates directly', async () => { + // const tokenPricesService = buildMockTokenPricesService({ + // fetchTokenPrices: fetchTokenPricesWithIncreasingPriceForEachToken, + // validateCurrencySupported(currency: unknown): currency is string { + // return currency === 'ETH'; + // }, + // }); + // const interval = 100; + // await withController( + // { + // options: { + // interval, + // tokenPricesService, + // }, + // mockTokensControllerState: { + // allTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: '0x02', + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // { + // address: '0x03', + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }, + // mockNetworkClientConfigurationsByNetworkClientId: { + // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + // chainId: ChainId.mainnet, + // ticker: 'ETH', + // }), + // }, + // }, + // async ({ controller }) => { + // controller.startPolling({ + // chainIds: [ChainId.mainnet], + // }); + // await advanceTime({ clock, duration: 0 }); + + // expect(controller.state).toStrictEqual({ + // marketData: { + // [ChainId.mainnet]: { + // [ZERO_ADDRESS]: { + // currency: 'ETH', + // priceChange1d: 0, + // pricePercentChange1d: 0, + // tokenAddress: ZERO_ADDRESS, + // chainId: '0x1', + // assetId: 'eip155:1/slip44:60', + // allTimeHigh: 4000, + // allTimeLow: 900, + // circulatingSupply: 2000, + // dilutedMarketCap: 100, + // high1d: 200, + // low1d: 100, + // marketCap: 1000, + // marketCapPercentChange1d: 100, + // price: 0.001, + // pricePercentChange14d: 100, + // pricePercentChange1h: 1, + // pricePercentChange1y: 200, + // pricePercentChange200d: 300, + // pricePercentChange30d: 200, + // pricePercentChange7d: 100, + // totalVolume: 100, + // }, + // '0x02': { + // currency: 'ETH', + // priceChange1d: 0, + // pricePercentChange1d: 0, + // tokenAddress: '0x02', + // chainId: '0x1', + // assetId: 'eip155:1/erc20:0x02', + // allTimeHigh: 4000, + // allTimeLow: 900, + // circulatingSupply: 2000, + // dilutedMarketCap: 100, + // high1d: 200, + // low1d: 100, + // marketCap: 1000, + // marketCapPercentChange1d: 100, + // price: 0.002, + // pricePercentChange14d: 100, + // pricePercentChange1h: 1, + // pricePercentChange1y: 200, + // pricePercentChange200d: 300, + // pricePercentChange30d: 200, + // pricePercentChange7d: 100, + // totalVolume: 100, + // }, + // '0x03': { + // currency: 'ETH', + // priceChange1d: 0, + // pricePercentChange1d: 0, + // tokenAddress: '0x03', + // chainId: '0x1', + // assetId: 'eip155:1/erc20:0x03', + // allTimeHigh: 4000, + // allTimeLow: 900, + // circulatingSupply: 2000, + // dilutedMarketCap: 100, + // high1d: 200, + // low1d: 100, + // marketCap: 1000, + // marketCapPercentChange1d: 100, + // price: 0.003, + // pricePercentChange14d: 100, + // pricePercentChange1h: 1, + // pricePercentChange1y: 200, + // pricePercentChange200d: 300, + // pricePercentChange30d: 200, + // pricePercentChange7d: 100, + // totalVolume: 100, + // }, + // }, + // }, + // }); + // }, + // ); + // }); + // }); + + // it('should stop polling', async () => { + // const interval = 100; + // const tokenPricesService = buildMockTokenPricesService(); + // jest.spyOn(tokenPricesService, 'fetchTokenPrices'); + // await withController( + // { + // options: { + // tokenPricesService, + // }, + // mockTokensControllerState: { + // allTokens: { + // '0x1': { + // [defaultSelectedAddress]: [ + // { + // address: mockTokenAddress, + // decimals: 0, + // symbol: '', + // aggregators: [], + // }, + // ], + // }, + // }, + // }, + // }, + // async ({ controller }) => { + // const pollingToken = controller.startPolling({ + // chainIds: [ChainId.mainnet], + // }); + // await advanceTime({ clock, duration: 0 }); + // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( + // 1, + // ); + + // controller.stopPollingByPollingToken(pollingToken); + + // await advanceTime({ clock, duration: interval }); + // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( + // 1, + // ); + // }, + // ); + // }); + // }); + + // it('does not update state when disabled', async () => { + // await withController( + // {}, + // async ({ + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // }) => { + // const tokenAddress = '0x0000000000000000000000000000000000000001'; + // controller.disable(); + // await callUpdateExchangeRatesMethod({ + // allTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: tokenAddress, + // decimals: 18, + // symbol: 'TST', + // aggregators: [], + // }, + // ], + // }, + // }, + // chainId: ChainId.mainnet, + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // nativeCurrency: 'ETH', + // selectedNetworkClientId: InfuraNetworkType.mainnet, + // }); + + // expect(controller.state.marketData).toStrictEqual({}); + // }, + // ); + // }); + + // it('does not update state if there are no tokens for the given chain', async () => { + // await withController( + // async ({ + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // }) => { + // const tokenAddress = '0x0000000000000000000000000000000000000001'; + // controller.enable(); + // await callUpdateExchangeRatesMethod({ + // allTokens: { + // // These tokens are on a different chain + // [toHex(2)]: { + // [defaultSelectedAddress]: [ + // { + // address: tokenAddress, + // decimals: 18, + // symbol: 'TST', + // aggregators: [], + // }, + // ], + // }, + // }, + // chainId: ChainId.mainnet, + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // nativeCurrency: 'ETH', + // selectedNetworkClientId: InfuraNetworkType.mainnet, + // }); + + // expect(controller.state).toStrictEqual({ + // marketData: {}, + // }); + // }, + // ); + // }); + + // it('does not update state if the price update fails', async () => { + // const tokenPricesService = buildMockTokenPricesService({ + // fetchTokenPrices: jest + // .fn() + // .mockRejectedValue(new Error('Failed to fetch')), + // }); + // await withController( + // { options: { tokenPricesService } }, + // async ({ + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // }) => { + // const tokenAddress = '0x0000000000000000000000000000000000000001'; + + // const updateExchangeRates = await callUpdateExchangeRatesMethod({ + // allTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: tokenAddress, + // decimals: 18, + // symbol: 'TST', + // aggregators: [], + // }, + // ], + // }, + // }, + // chainId: ChainId.mainnet, + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // nativeCurrency: 'ETH', + // selectedNetworkClientId: InfuraNetworkType.mainnet, + // }); + + // expect(updateExchangeRates).toBeUndefined(); + // expect(controller.state.marketData).toStrictEqual({}); + // }, + // ); + // }); + + // it('fetches rates for all tokens in batches', async () => { + // const chainId = ChainId.mainnet; + // const ticker = NetworksTicker.mainnet; + // const tokenAddresses = [...new Array(200).keys()] + // .map(buildAddress) + // .sort(); + // const tokenPricesService = buildMockTokenPricesService({ + // fetchTokenPrices: fetchTokenPricesWithIncreasingPriceForEachToken, + // }); + // const fetchTokenPricesSpy = jest.spyOn( + // tokenPricesService, + // 'fetchTokenPrices', + // ); + // const tokens = tokenAddresses.map((tokenAddress) => { + // return buildToken({ address: tokenAddress }); + // }); + // await withController( + // { + // options: { + // tokenPricesService, + // }, + // }, + // async ({ + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // }) => { + // await callUpdateExchangeRatesMethod({ + // allTokens: { + // [chainId]: { + // [defaultSelectedAddress]: tokens.slice(0, 100), + // // Include tokens from non selected addresses + // '0x0000000000000000000000000000000000000123': tokens.slice(100), + // }, + // }, + // chainId, + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // nativeCurrency: ticker, + // selectedNetworkClientId: InfuraNetworkType.mainnet, + // }); + + // const numBatches = Math.ceil( + // tokenAddresses.length / TOKEN_PRICES_BATCH_SIZE, + // ); + // expect(fetchTokenPricesSpy).toHaveBeenCalledTimes(numBatches); + + // for (let i = 1; i <= numBatches; i++) { + // expect(fetchTokenPricesSpy).toHaveBeenNthCalledWith(i, { + // assets: tokenAddresses + // .slice( + // (i - 1) * TOKEN_PRICES_BATCH_SIZE, + // i * TOKEN_PRICES_BATCH_SIZE, + // ) + // .map((tokenAddress) => ({ + // chainId, + // tokenAddress, + // })), + // currency: ticker, + // }); + // } + // }, + // ); + // }); + + // it('updates all rates', async () => { + // const tokenAddresses = [ + // '0x0000000000000000000000000000000000000001', + // '0x0000000000000000000000000000000000000002', + // '0x0000000000000000000000000000000000000003', + // ]; + // const tokenPricesService = buildMockTokenPricesService({ + // fetchTokenPrices: jest.fn().mockResolvedValue({ + // [tokenAddresses[0]]: { + // currency: 'ETH', + // tokenAddress: tokenAddresses[0], + // value: 0.001, + // }, + // [tokenAddresses[1]]: { + // currency: 'ETH', + // tokenAddress: tokenAddresses[1], + // value: 0.002, + // }, + // [tokenAddresses[2]]: { + // currency: 'ETH', + // tokenAddress: tokenAddresses[2], + // value: 0.003, + // }, + // }), + // }); + // await withController( + // { options: { tokenPricesService } }, + // async ({ + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // }) => { + // await callUpdateExchangeRatesMethod({ + // allTokens: { + // [ChainId.mainnet]: { + // [defaultSelectedAddress]: [ + // { + // address: tokenAddresses[0], + // decimals: 18, + // symbol: 'TST1', + // aggregators: [], + // }, + // { + // address: tokenAddresses[1], + // decimals: 18, + // symbol: 'TST2', + // aggregators: [], + // }, + // ], + // // Include tokens from non selected addresses + // '0x0000000000000000000000000000000000000123': [ + // { + // address: tokenAddresses[2], + // decimals: 18, + // symbol: 'TST1', + // aggregators: [], + // }, + // ], + // }, + // }, + // chainId: ChainId.mainnet, + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // nativeCurrency: 'ETH', + // 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, + // }, + // }, + // }, + // } + // `); + // }, + // ); + // }); + + // it('updates rates only for a non-selected chain', async () => { + // const tokenAddresses = [ + // '0x0000000000000000000000000000000000000001', + // '0x0000000000000000000000000000000000000002', + // ]; + // const tokenPricesService = buildMockTokenPricesService({ + // fetchTokenPrices: jest.fn().mockResolvedValue({ + // [tokenAddresses[0]]: { + // currency: 'ETH', + // tokenAddress: tokenAddresses[0], + // value: 0.001, + // }, + // [tokenAddresses[1]]: { + // currency: 'ETH', + // tokenAddress: tokenAddresses[1], + // value: 0.002, + // }, + // }), + // }); + // await withController( + // { options: { tokenPricesService } }, + // async ({ + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // }) => { + // await callUpdateExchangeRatesMethod({ + // allTokens: { + // [toHex(2)]: { + // [defaultSelectedAddress]: [ + // { + // address: tokenAddresses[0], + // decimals: 18, + // symbol: 'TST1', + // aggregators: [], + // }, + // { + // address: tokenAddresses[1], + // decimals: 18, + // symbol: 'TST2', + // aggregators: [], + // }, + // ], + // }, + // }, + // chainId: toHex(2), + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // nativeCurrency: 'ETH', + // 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, + // }, + // }, + // }, + // } + // `); + // }, + // ); + // }); + + // it('updates exchange rates when native currency is not supported by the Price API', async () => { + // const selectedNetworkClientId = 'AAAA-BBBB-CCCC-DDDD'; + // const selectedNetworkClientConfiguration = + // buildCustomNetworkClientConfiguration({ + // chainId: toHex(137), + // ticker: 'UNSUPPORTED', + // }); + // const tokenAddresses = [ + // '0x0000000000000000000000000000000000000001', + // '0x0000000000000000000000000000000000000002', + // ]; + // const tokenPricesService = buildMockTokenPricesService({ + // fetchTokenPrices: jest.fn().mockResolvedValue({ + // [tokenAddresses[0]]: { + // currency: 'ETH', + // tokenAddress: tokenAddresses[0], + // price: 0.001, + // }, + // [tokenAddresses[1]]: { + // currency: 'ETH', + // tokenAddress: tokenAddresses[1], + // price: 0.002, + // }, + // }), + // validateCurrencySupported(_currency: unknown): _currency is string { + // return false; + // }, + // }); + // nock('https://min-api.cryptocompare.com') + // .get('/data/price') + // .query({ + // fsym: 'ETH', + // tsyms: selectedNetworkClientConfiguration.ticker, + // }) + // .reply(200, { [selectedNetworkClientConfiguration.ticker]: 0.5 }); // .5 eth to 1 matic + + // await withController( + // { + // options: { tokenPricesService }, + // mockNetworkClientConfigurationsByNetworkClientId: { + // [selectedNetworkClientId]: selectedNetworkClientConfiguration, + // }, + // }, + // async ({ + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // }) => { + // await callUpdateExchangeRatesMethod({ + // allTokens: { + // [selectedNetworkClientConfiguration.chainId]: { + // [defaultSelectedAddress]: [ + // { + // address: tokenAddresses[0], + // decimals: 18, + // symbol: 'TST1', + // aggregators: [], + // }, + // { + // address: tokenAddresses[1], + // decimals: 18, + // symbol: 'TST2', + // aggregators: [], + // }, + // ], + // }, + // }, + // chainId: selectedNetworkClientConfiguration.chainId, + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // nativeCurrency: selectedNetworkClientConfiguration.ticker, + // selectedNetworkClientId, + // }); + + // // 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, + // }, + // }, + // }, + // } + // `); + // }, + // ); + // }); + + // it('fetches rates for all tokens in batches when native currency is not supported by the Price API', async () => { + // const selectedNetworkClientId = 'AAAA-BBBB-CCCC-DDDD'; + // const selectedNetworkClientConfiguration = + // buildCustomNetworkClientConfiguration({ + // chainId: toHex(999), + // ticker: 'UNSUPPORTED', + // }); + // const tokenAddresses = [...new Array(200).keys()] + // .map(buildAddress) + // .sort(); + // const tokenPricesService = buildMockTokenPricesService({ + // fetchTokenPrices: fetchTokenPricesWithIncreasingPriceForEachToken, + // validateCurrencySupported: (currency: unknown): currency is string => { + // return currency !== selectedNetworkClientConfiguration.ticker; + // }, + // }); + // const fetchTokenPricesSpy = jest.spyOn( + // tokenPricesService, + // 'fetchTokenPrices', + // ); + // const tokens = tokenAddresses.map((tokenAddress) => { + // return buildToken({ address: tokenAddress }); + // }); + // nock('https://min-api.cryptocompare.com') + // .get('/data/price') + // .query({ + // fsym: 'ETH', + // tsyms: selectedNetworkClientConfiguration.ticker, + // }) + // .reply(200, { [selectedNetworkClientConfiguration.ticker]: 0.5 }); + // await withController( + // { + // options: { + // tokenPricesService, + // }, + // mockNetworkClientConfigurationsByNetworkClientId: { + // [selectedNetworkClientId]: selectedNetworkClientConfiguration, + // }, + // mockNetworkState: { + // networkConfigurationsByChainId: { + // [selectedNetworkClientConfiguration.chainId]: { + // nativeCurrency: selectedNetworkClientConfiguration.ticker, + // chainId: selectedNetworkClientConfiguration.chainId, + // name: 'UNSUPPORTED', + // rpcEndpoints: [], + // blockExplorerUrls: [], + // defaultRpcEndpointIndex: 0, + // }, + // }, + // selectedNetworkClientId, + // }, + // }, + // async ({ + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // }) => { + // await callUpdateExchangeRatesMethod({ + // allTokens: { + // [selectedNetworkClientConfiguration.chainId]: { + // [defaultSelectedAddress]: tokens, + // }, + // }, + // chainId: selectedNetworkClientConfiguration.chainId, + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // nativeCurrency: selectedNetworkClientConfiguration.ticker, + // selectedNetworkClientId, + // }); + + // const numBatches = Math.ceil( + // tokenAddresses.length / TOKEN_PRICES_BATCH_SIZE, + // ); + // expect(fetchTokenPricesSpy).toHaveBeenCalledTimes(numBatches); + + // for (let i = 1; i <= numBatches; i++) { + // expect(fetchTokenPricesSpy).toHaveBeenNthCalledWith(i, { + // chainId: selectedNetworkClientConfiguration.chainId, + // tokenAddresses: tokenAddresses.slice( + // (i - 1) * TOKEN_PRICES_BATCH_SIZE, + // i * TOKEN_PRICES_BATCH_SIZE, + // ), + // currency: 'ETH', + // }); + // } + // }, + // ); + // }); + + // it('sets rates to undefined when chain is not supported by the Price API', async () => { + // const selectedNetworkClientId = 'AAAA-BBBB-CCCC-DDDD'; + // const selectedNetworkClientConfiguration = + // buildCustomNetworkClientConfiguration({ + // chainId: toHex(999), + // ticker: 'TST', + // }); + // const tokenAddresses = [ + // '0x0000000000000000000000000000000000000001', + // '0x0000000000000000000000000000000000000002', + // ]; + // const tokenPricesService = buildMockTokenPricesService({ + // fetchTokenPrices: jest.fn().mockResolvedValue({ + // [tokenAddresses[0]]: { + // currency: 'ETH', + // tokenAddress: tokenAddresses[0], + // value: 0.001, + // }, + // [tokenAddresses[1]]: { + // currency: 'ETH', + // tokenAddress: tokenAddresses[1], + // value: 0.002, + // }, + // }), + // validateChainIdSupported(_chainId: unknown): _chainId is Hex { + // return false; + // }, + // }); + // await withController( + // { + // options: { tokenPricesService }, + // mockNetworkClientConfigurationsByNetworkClientId: { + // [selectedNetworkClientId]: selectedNetworkClientConfiguration, + // }, + // }, + // async ({ + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // }) => { + // await callUpdateExchangeRatesMethod({ + // allTokens: { + // [toHex(999)]: { + // [defaultSelectedAddress]: [ + // { + // address: tokenAddresses[0], + // decimals: 18, + // symbol: 'TST1', + // aggregators: [], + // }, + // { + // address: tokenAddresses[1], + // decimals: 18, + // symbol: 'TST2', + // aggregators: [], + // }, + // ], + // }, + // }, + // chainId: selectedNetworkClientConfiguration.chainId, + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // nativeCurrency: selectedNetworkClientConfiguration.ticker, + // selectedNetworkClientId, + // }); + + // expect(controller.state).toMatchInlineSnapshot(` + // Object { + // "marketData": Object { + // "0x3e7": Object { + // "0x0000000000000000000000000000000000000001": undefined, + // "0x0000000000000000000000000000000000000002": undefined, + // }, + // }, + // } + // `); + // }, + // ); + // }); + + // it('correctly calls the Price API with unqiue native token addresses (e.g. MATIC)', async () => { + // const tokenPricesService = buildMockTokenPricesService({ + // fetchTokenPrices: jest.fn().mockResolvedValue({ + // '0x0000000000000000000000000000000000001010': { + // currency: 'MATIC', + // tokenAddress: '0x0000000000000000000000000000000000001010', + // value: 0.001, + // }, + // }), + // }); + + // await withController( + // { + // options: { tokenPricesService }, + // mockNetworkClientConfigurationsByNetworkClientId: { + // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ + // chainId: '0x89', + // }), + // }, + // }, + // async ({ + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // }) => { + // await callUpdateExchangeRatesMethod({ + // allTokens: { + // '0x89': { + // [defaultSelectedAddress]: [], + // }, + // }, + // chainId: '0x89', + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // nativeCurrency: 'MATIC', + // selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', + // }); + + // expect( + // controller.state.marketData['0x89'][ + // '0x0000000000000000000000000000000000001010' + // ], + // ).toBeDefined(); + // }, + // ); + // }); + + // it('only updates rates once when called twice', 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 tokenPricesService = buildMockTokenPricesService({ + // fetchTokenPrices: fetchTokenPricesMock, + // }); + // await withController( + // { options: { tokenPricesService } }, + // async ({ + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // }) => { + // const updateExchangeRates = async () => + // await callUpdateExchangeRatesMethod({ + // allTokens: { + // [toHex(1)]: { + // [defaultSelectedAddress]: [ + // { + // address: tokenAddresses[0], + // decimals: 18, + // symbol: 'TST1', + // aggregators: [], + // }, + // { + // address: tokenAddresses[1], + // decimals: 18, + // symbol: 'TST2', + // aggregators: [], + // }, + // ], + // }, + // }, + // chainId: ChainId.mainnet, + // selectedNetworkClientId: InfuraNetworkType.mainnet, + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // nativeCurrency: 'ETH', + // }); + + // await Promise.all([updateExchangeRates(), updateExchangeRates()]); + + // 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, + // }, + // }, + // }, + // } + // `); + // }, + // ); + // }); + + // it('will update rates twice if detected tokens increased during second call', async () => { + // const tokenAddresses = [ + // '0x0000000000000000000000000000000000000001', + // '0x0000000000000000000000000000000000000002', + // ]; + // const fetchTokenPricesMock = jest.fn().mockResolvedValue([ + // { + // currency: 'ETH', + // tokenAddress: tokenAddresses[0], + // value: 0.001, + // }, + // { + // currency: 'ETH', + // tokenAddress: tokenAddresses[1], + // value: 0.002, + // }, + // ]); + // const tokenPricesService = buildMockTokenPricesService({ + // fetchTokenPrices: fetchTokenPricesMock, + // }); + // await withController( + // { options: { tokenPricesService } }, + // async ({ + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // }) => { + // const request1Payload = [ + // { + // address: tokenAddresses[0], + // decimals: 18, + // symbol: 'TST1', + // aggregators: [], + // }, + // ]; + // const request2Payload = [ + // { + // address: tokenAddresses[0], + // decimals: 18, + // symbol: 'TST1', + // aggregators: [], + // }, + // { + // address: tokenAddresses[1], + // decimals: 18, + // symbol: 'TST2', + // aggregators: [], + // }, + // ]; + // const updateExchangeRates = async ( + // tokens: typeof request1Payload | typeof request2Payload, + // ) => + // await callUpdateExchangeRatesMethod({ + // allTokens: { + // [toHex(1)]: { + // [defaultSelectedAddress]: tokens, + // }, + // }, + // chainId: ChainId.mainnet, + // selectedNetworkClientId: InfuraNetworkType.mainnet, + // controller, + // triggerTokensStateChange, + // triggerNetworkStateChange, + // nativeCurrency: 'ETH', + // }); + + // await Promise.all([ + // updateExchangeRates(request1Payload), + // updateExchangeRates(request2Payload), + // ]); + + // expect(fetchTokenPricesMock).toHaveBeenCalledTimes(2); + // expect(fetchTokenPricesMock).toHaveBeenNthCalledWith( + // 1, + // expect.objectContaining({ + // assets: [{ tokenAddress: tokenAddresses[0], chainId: '0x1' }], + // }), + // ); + // expect(fetchTokenPricesMock).toHaveBeenNthCalledWith( + // 2, + // expect.objectContaining({ + // assets: [ + // { tokenAddress: tokenAddresses[0], chainId: '0x1' }, + // { tokenAddress: tokenAddresses[1], chainId: '0x1' }, + // ], + // }), + // ); + // }, + // ); + // }); + // }); + describe('resetState', () => { it('resets the state to default state', async () => { const initialState: TokenRatesControllerState = { @@ -2771,12 +2978,10 @@ describe('TokenRatesController', () => { */ type WithControllerCallback = ({ controller, - triggerSelectedAccountChange, triggerTokensStateChange, triggerNetworkStateChange, }: { controller: TokenRatesController; - triggerSelectedAccountChange: (state: InternalAccount) => void; triggerTokensStateChange: (state: TokensControllerState) => void; triggerNetworkStateChange: (state: NetworkState, patches?: Patch[]) => void; }) => Promise | ReturnValue; @@ -2864,13 +3069,6 @@ async function withController( try { return await fn({ controller, - triggerSelectedAccountChange: (account: InternalAccount) => { - messenger.publish( - 'AccountsController:selectedEvmAccountChange', - account, - ); - }, - triggerTokensStateChange: (state: TokensControllerState) => { messenger.publish('TokensController:stateChange', state, []); }, @@ -2882,7 +3080,6 @@ async function withController( }, }); } finally { - controller.stop(); controller.stopAllPolling(); } } @@ -2908,7 +3105,6 @@ async function withController( * update controller configuration. * @param args.triggerNetworkStateChange - Controller event handlers, used to * update controller configuration. - * @param args.method - The "update exchange rates" method to call. * @param args.nativeCurrency - The symbol for the native currency of the * network we're getting updated exchange rates for. * @param args.setChainAsCurrent - When calling `updateExchangeRatesByChainId`, @@ -2922,7 +3118,6 @@ async function callUpdateExchangeRatesMethod({ controller, triggerTokensStateChange, triggerNetworkStateChange, - method, nativeCurrency, selectedNetworkClientId, setChainAsCurrent = true, @@ -2932,17 +3127,10 @@ async function callUpdateExchangeRatesMethod({ controller: TokenRatesController; triggerTokensStateChange: (state: TokensControllerState) => void; triggerNetworkStateChange: (state: NetworkState) => void; - method: 'updateExchangeRates' | 'updateExchangeRatesByChainId'; nativeCurrency: string; selectedNetworkClientId?: NetworkClientId; setChainAsCurrent?: boolean; }) { - if (method === 'updateExchangeRates' && !setChainAsCurrent) { - throw new Error( - 'The "setChainAsCurrent" flag cannot be enabled when calling the "updateExchangeRates" method', - ); - } - triggerTokensStateChange({ ...getDefaultTokensState(), allDetectedTokens: {}, @@ -2965,21 +3153,12 @@ async function callUpdateExchangeRatesMethod({ }); } - if (method === 'updateExchangeRates') { - await controller.updateExchangeRates([ - { - chainId, - nativeCurrency, - }, - ]); - } else { - await controller.updateExchangeRatesByChainId([ - { - chainId, - nativeCurrency, - }, - ]); - } + await controller.updateExchangeRates([ + { + chainId, + nativeCurrency, + }, + ]); } /** @@ -2994,7 +3173,7 @@ function buildMockTokenPricesService( ): AbstractTokenPricesService { return { async fetchTokenPrices() { - return {}; + return []; }, async fetchExchangeRates() { return {}; @@ -3014,50 +3193,44 @@ function buildMockTokenPricesService( * price of each given token is incremented by one. * * @param args - The arguments to this function. - * @param args.tokenAddresses - The token addresses. + * @param args.assets - The token addresses and chainIds. * @param args.currency - The currency. * @returns The token prices. */ async function fetchTokenPricesWithIncreasingPriceForEachToken< - TokenAddress extends Hex, Currency extends string, >({ - tokenAddresses, + assets, currency, }: { - tokenAddresses: TokenAddress[]; + assets: { tokenAddress: Hex; chainId: Hex }[]; currency: Currency; -}) { - return tokenAddresses.reduce< - Partial> - >((obj, tokenAddress, i) => { - const tokenPrice: TokenPrice = { - tokenAddress, - currency, - pricePercentChange1d: 0, - priceChange1d: 0, - allTimeHigh: 4000, - allTimeLow: 900, - circulatingSupply: 2000, - dilutedMarketCap: 100, - high1d: 200, - low1d: 100, - marketCap: 1000, - marketCapPercentChange1d: 100, - price: (i + 1) / 1000, - pricePercentChange14d: 100, - pricePercentChange1h: 1, - pricePercentChange1y: 200, - pricePercentChange200d: 300, - pricePercentChange30d: 200, - pricePercentChange7d: 100, - totalVolume: 100, - }; - return { - ...obj, - [tokenAddress]: tokenPrice, - }; - }, {}) as TokenPricesByTokenAddress; +}): Promise[]> { + return assets.map(({ tokenAddress, chainId }, i) => ({ + tokenAddress, + chainId, + assetId: + `${KnownCaipNamespace.Eip155}:1/${tokenAddress === ZERO_ADDRESS ? 'slip44:60' : `erc20:${tokenAddress.toLowerCase()}`}` as CaipAssetType, + currency, + pricePercentChange1d: 0, + priceChange1d: 0, + allTimeHigh: 4000, + allTimeLow: 900, + circulatingSupply: 2000, + dilutedMarketCap: 100, + high1d: 200, + low1d: 100, + marketCap: 1000, + marketCapPercentChange1d: 100, + price: (i + 1) / 1000, + pricePercentChange14d: 100, + pricePercentChange1h: 1, + pricePercentChange1y: 200, + pricePercentChange200d: 300, + pricePercentChange30d: 200, + pricePercentChange7d: 100, + totalVolume: 100, + })); } /** diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 07cf5c23809..b2dbfcade0e 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -1,17 +1,13 @@ import type { AccountsControllerGetAccountAction, AccountsControllerGetSelectedAccountAction, - AccountsControllerSelectedEvmAccountChangeEvent, } from '@metamask/accounts-controller'; import type { ControllerGetStateAction, ControllerStateChangeEvent, StateMetadata, } from '@metamask/base-controller'; -import { - safelyExecute, - toChecksumHexAddress, -} from '@metamask/controller-utils'; +import { toChecksumHexAddress } from '@metamask/controller-utils'; import type { Messenger } from '@metamask/messenger'; import type { NetworkControllerGetNetworkClientByIdAction, @@ -95,11 +91,6 @@ type ChainIdAndNativeCurrency = { nativeCurrency: string; }; -enum PollState { - Active = 'Active', - Inactive = 'Inactive', -} - /** * The external actions available to the {@link TokenRatesController}. */ @@ -115,8 +106,7 @@ export type AllowedActions = */ export type AllowedEvents = | TokensControllerStateChangeEvent - | NetworkControllerStateChangeEvent - | AccountsControllerSelectedEvmAccountChangeEvent; + | NetworkControllerStateChangeEvent; /** * The name of the {@link TokenRatesController}. @@ -204,16 +194,10 @@ export class TokenRatesController extends StaticIntervalPollingController { - #handle?: ReturnType; - - #pollState = PollState.Inactive; - readonly #tokenPricesService: AbstractTokenPricesService; #disabled: boolean; - readonly #interval: number; - #allTokens: TokensControllerState['allTokens']; #allDetectedTokens: TokensControllerState['allDetectedTokens']; @@ -251,7 +235,6 @@ export class TokenRatesController extends StaticIntervalPollingController { return { allTokens, allDetectedTokens }; @@ -338,7 +321,7 @@ export class TokenRatesController extends StaticIntervalPollingController - this.updateExchangeRates([{ chainId, nativeCurrency }]), - ); - - // Poll using recursive `setTimeout` instead of `setInterval` so that - // requests don't stack if they take longer than the polling interval - this.#handle = setTimeout(() => { - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.#poll(chainId, nativeCurrency); - }, this.#interval); - } - /** * Updates exchange rates for all tokens. * @@ -466,17 +400,6 @@ export class TokenRatesController extends StaticIntervalPollingController { const marketData: Record> = {}; const assetsByNativeCurrency: Record< @@ -499,6 +422,11 @@ export class TokenRatesController extends StaticIntervalPollingController { @@ -515,6 +443,12 @@ export class TokenRatesController extends StaticIntervalPollingController chain.chainId), + ); + + for (const chainId of chainIds) { + if (!marketData[chainId]) { + marketData[chainId] = {}; + } + } + // console.log('GGGGG', marketData); + if (Object.keys(marketData).length > 0) { this.update((state) => { state.marketData = { @@ -567,7 +512,7 @@ export class TokenRatesController extends StaticIntervalPollingController Date: Tue, 18 Nov 2025 10:12:17 +0000 Subject: [PATCH 22/42] tests fixed --- .../src/TokenRatesController.test.ts | 2614 ++--------------- .../src/TokenRatesController.ts | 25 +- 2 files changed, 195 insertions(+), 2444 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index ff8edbbb307..22417101ec9 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -1,12 +1,5 @@ import { deriveStateFromMetadata } from '@metamask/base-controller'; -import { - ChainId, - InfuraNetworkType, - NetworksTicker, - toChecksumHexAddress, - toHex, -} from '@metamask/controller-utils'; -import type { InternalAccount } from '@metamask/keyring-internal-api'; +import { ChainId, toChecksumHexAddress } from '@metamask/controller-utils'; import { Messenger, MOCK_ANY_NAMESPACE, @@ -17,15 +10,13 @@ import { import type { NetworkClientConfiguration, NetworkClientId, + NetworkConfiguration, NetworkState, } from '@metamask/network-controller'; import { getDefaultNetworkControllerState } from '@metamask/network-controller'; import type { CaipAssetType, Hex } from '@metamask/utils'; import { add0x, KnownCaipNamespace } from '@metamask/utils'; -import assert from 'assert'; import type { Patch } from 'immer'; -import nock from 'nock'; -import { useFakeTimers } from 'sinon'; import { TOKEN_PRICES_BATCH_SIZE } from './assetsUtil'; import type { @@ -35,24 +26,16 @@ import type { import { ZERO_ADDRESS } from './token-prices-service/codefi-v2'; import { controllerName, TokenRatesController } from './TokenRatesController'; import type { + MarketDataDetails, Token, TokenRatesControllerMessenger, TokenRatesControllerState, } from './TokenRatesController'; import { getDefaultTokensState } from './TokensController'; import type { TokensControllerState } from './TokensController'; -import { advanceTime, flushPromises } from '../../../tests/helpers'; -import { createMockInternalAccount } from '../../accounts-controller/src/tests/mocks'; -import { - buildCustomNetworkClientConfiguration, - buildMockGetNetworkClientById, -} from '../../network-controller/tests/helpers'; +import { flushPromises } from '../../../tests/helpers'; const defaultSelectedAddress = '0x1111111111111111111111111111111111111111'; -const defaultSelectedAccount = createMockInternalAccount({ - address: defaultSelectedAddress, -}); -const mockTokenAddress = '0x0000000000000000000000000000000000000010'; type AllTokenRatesControllerActions = MessengerActions; @@ -86,13 +69,7 @@ function buildTokenRatesControllerMessenger( }); messenger.delegate({ messenger: tokenRatesControllerMessenger, - actions: [ - 'TokensController:getState', - 'NetworkController:getNetworkClientById', - 'NetworkController:getState', - 'AccountsController:getAccount', - 'AccountsController:getSelectedAccount', - ], + actions: ['TokensController:getState', 'NetworkController:getState'], events: ['TokensController:stateChange', 'NetworkController:stateChange'], }); return tokenRatesControllerMessenger; @@ -100,16 +77,6 @@ function buildTokenRatesControllerMessenger( describe('TokenRatesController', () => { describe('constructor', () => { - // let clock: sinon.SinonFakeTimers; - - // beforeEach(() => { - // clock = useFakeTimers({ now: Date.now() }); - // }); - - // afterEach(() => { - // clock.restore(); - // }); - it('should set default state', async () => { await withController(async ({ controller }) => { expect(controller.state).toStrictEqual({ @@ -450,6 +417,55 @@ describe('TokenRatesController', () => { ); }); + it('does not include chains when tokens are not updated', async () => { + jest.useFakeTimers(); + const chainId = '0x1'; + + await withController( + { + mockTokensControllerState: { + allTokens: { + [chainId]: { + [defaultSelectedAddress]: [ + { + address: '0x0000000000000000000000000000000000000001', + decimals: 0, + symbol: 'TOK1', + }, + ], + }, + }, + allDetectedTokens: {}, + allIgnoredTokens: {}, + }, + }, + async ({ controller, triggerTokensStateChange }) => { + jest.spyOn(controller, 'updateExchangeRates'); + + triggerTokensStateChange({ + allTokens: { + [chainId]: { + [defaultSelectedAddress]: [ + { + address: '0x0000000000000000000000000000000000000001', + decimals: 0, + symbol: 'TOK1', + }, + ], + }, + }, + allDetectedTokens: {}, + allIgnoredTokens: {}, + }); + + jest.advanceTimersToNextTimer(); + await flushPromises(); + + expect(controller.updateExchangeRates).toHaveBeenCalledWith([]); + }, + ); + }); + it('does not include chains with no network configuration', async () => { jest.useFakeTimers(); const chainId = '0x1'; @@ -488,6 +504,139 @@ describe('TokenRatesController', () => { }); }); + describe('NetworkController:stateChange', () => { + it('fetches rates for all updated chains', async () => { + const chainId = '0x1'; + const nativeCurrency = 'ETH'; + + await withController( + {}, + async ({ controller, triggerNetworkStateChange }) => { + jest.spyOn(controller, 'updateExchangeRates'); + + triggerNetworkStateChange( + { + ...getDefaultNetworkControllerState(), + networkConfigurationsByChainId: { + [chainId]: { + chainId, + nativeCurrency, + } as unknown as NetworkConfiguration, + }, + }, + [], + ); + + jest.advanceTimersToNextTimer(); + await flushPromises(); + + expect(controller.updateExchangeRates).toHaveBeenCalledWith([ + { + chainId, + nativeCurrency, + }, + ]); + }, + ); + }); + + it('does not fetch when disabled', async () => { + const chainId = '0x1'; + const nativeCurrency = 'ETH'; + + await withController( + { + options: { + disabled: true, + }, + }, + async ({ controller, triggerNetworkStateChange }) => { + jest.spyOn(controller, 'updateExchangeRates'); + + triggerNetworkStateChange( + { + ...getDefaultNetworkControllerState(), + networkConfigurationsByChainId: { + [chainId]: { + chainId, + nativeCurrency, + } as unknown as NetworkConfiguration, + }, + }, + [], + ); + + jest.advanceTimersToNextTimer(); + await flushPromises(); + + expect(controller.updateExchangeRates).not.toHaveBeenCalled(); + }, + ); + }); + + it('remove state from deleted networks', async () => { + const chainId = '0x1'; + const nativeCurrency = 'ETH'; + + await withController( + { + options: { + disabled: true, + state: { + marketData: { + [chainId]: { + '0x0000000000000000000000000000000000000000': { + currency: nativeCurrency, + price: 0.001, + } as unknown as MarketDataDetails, + }, + '0x2': { + '0x0000000000000000000000000000000000000000': { + currency: nativeCurrency, + price: 0.001, + } as unknown as MarketDataDetails, + }, + }, + }, + }, + }, + async ({ controller, triggerNetworkStateChange }) => { + jest.spyOn(controller, 'updateExchangeRates'); + + triggerNetworkStateChange( + { + ...getDefaultNetworkControllerState(), + networkConfigurationsByChainId: { + [chainId]: { + chainId, + nativeCurrency, + } as unknown as NetworkConfiguration, + }, + }, + [ + { + op: 'remove', + path: ['networkConfigurationsByChainId', chainId], + }, + ], + ); + + jest.advanceTimersToNextTimer(); + await flushPromises(); + + expect(controller.state.marketData).toStrictEqual({ + '0x2': { + '0x0000000000000000000000000000000000000000': { + currency: nativeCurrency, + price: 0.001, + } as unknown as MarketDataDetails, + }, + }); + }, + ); + }); + }); + describe('enable', () => { it('enables events', async () => { jest.useFakeTimers(); @@ -575,2291 +724,6 @@ describe('TokenRatesController', () => { }); }); - // describe('TokensController::stateChange', () => { - // let clock: sinon.SinonFakeTimers; - - // beforeEach(() => { - // clock = useFakeTimers({ now: Date.now() }); - // }); - - // afterEach(() => { - // clock.restore(); - // }); - - // describe('when legacy polling is active', () => { - // it('should update exchange rates when any of the addresses in the "all tokens" collection change', async () => { - // const tokenAddresses = ['0xE1', '0xE2']; - // await withController( - // { - // mockTokensControllerState: { - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddresses[0], - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }, - // }, - // async ({ controller, triggerTokensStateChange }) => { - // const updateExchangeRatesSpy = jest - // .spyOn(controller, 'updateExchangeRates') - // .mockResolvedValue(); - // await controller.start(ChainId.mainnet, 'ETH'); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddresses[1], - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); - - // // Once when starting, and another when tokens state changes - // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(2); - // }, - // ); - // }); - - // it('should update exchange rates when any of the addresses in the "all tokens" collection change with invalid addresses', async () => { - // const tokenAddresses = ['0xinvalidAddress']; - // await withController( - // { - // mockTokensControllerState: { - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddresses[0], - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }, - // }, - // async ({ controller, triggerTokensStateChange }) => { - // const updateExchangeRatesSpy = jest - // .spyOn(controller, 'updateExchangeRates') - // .mockResolvedValue(); - // await controller.start(ChainId.mainnet, 'ETH'); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddresses[1], - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); - - // // Once when starting, and another when tokens state changes - // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(2); - // }, - // ); - // }); - - // it('should update exchange rates when any of the addresses in the "all detected tokens" collection change', async () => { - // const tokenAddresses = ['0xE1', '0xE2']; - // await withController( - // { - // mockTokensControllerState: { - // allDetectedTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddresses[0], - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }, - // }, - // async ({ controller, triggerTokensStateChange }) => { - // const updateExchangeRatesSpy = jest - // .spyOn(controller, 'updateExchangeRates') - // .mockResolvedValue(); - // await controller.start(ChainId.mainnet, 'ETH'); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allDetectedTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddresses[1], - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); - // // Once when starting, and another when tokens state changes - // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(2); - // }, - // ); - // }); - - // it('should not update exchange rates if both the "all tokens" or "all detected tokens" are exactly the same', async () => { - // const tokensState = { - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }; - // await withController( - // { - // mockTokensControllerState: { - // ...tokensState, - // }, - // }, - // async ({ controller, triggerTokensStateChange }) => { - // const updateExchangeRatesSpy = jest - // .spyOn(controller, 'updateExchangeRates') - // .mockResolvedValue(); - // await controller.start(ChainId.mainnet, 'ETH'); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // ...tokensState, - // }); - - // // Once when starting, and that's it - // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); - // }, - // ); - // }); - - // it('should not update exchange rates if all of the tokens in "all tokens" just move to "all detected tokens"', async () => { - // const tokens = { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }; - // await withController( - // { - // mockTokensControllerState: { - // allTokens: tokens, - // }, - // }, - // async ({ controller, triggerTokensStateChange }) => { - // const updateExchangeRatesSpy = jest - // .spyOn(controller, 'updateExchangeRates') - // .mockResolvedValue(); - // await controller.start(ChainId.mainnet, 'ETH'); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allDetectedTokens: tokens, - // }); - - // // Once when starting, and that's it - // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); - // }, - // ); - // }); - - // it('should not update exchange rates if a new token is added to "all detected tokens" but is already present in "all tokens"', async () => { - // const tokens = { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }; - // await withController( - // { - // mockTokensControllerState: { - // allTokens: tokens, - // }, - // }, - // async ({ controller, triggerTokensStateChange }) => { - // const updateExchangeRatesSpy = jest - // .spyOn(controller, 'updateExchangeRates') - // .mockResolvedValue(); - // await controller.start(ChainId.mainnet, 'ETH'); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: tokens, - // allDetectedTokens: tokens, - // }); - - // // Once when starting, and that's it - // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); - // }, - // ); - // }); - - // it('should not update exchange rates if a new token is added to "all tokens" but is already present in "all detected tokens"', async () => { - // const tokens = { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }; - // await withController( - // { - // mockTokensControllerState: { - // allDetectedTokens: tokens, - // }, - // }, - // async ({ controller, triggerTokensStateChange }) => { - // const updateExchangeRatesSpy = jest - // .spyOn(controller, 'updateExchangeRates') - // .mockResolvedValue(); - // await controller.start(ChainId.mainnet, 'ETH'); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: tokens, - // allDetectedTokens: tokens, - // }); - - // // Once when starting, and that's it - // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); - // }, - // ); - // }); - - // it('should not update exchange rates if none of the addresses in "all tokens" or "all detected tokens" change, even if other parts of the token change', async () => { - // await withController( - // { - // mockTokensControllerState: { - // ...getDefaultTokensState(), - // allDetectedTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 3, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }, - // }, - // async ({ controller, triggerTokensStateChange }) => { - // const updateExchangeRatesSpy = jest - // .spyOn(controller, 'updateExchangeRates') - // .mockResolvedValue(); - // await controller.start(ChainId.mainnet, 'ETH'); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allDetectedTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 7, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); - - // // Once when starting, and that's it - // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); - // }, - // ); - // }); - - // it('should not update exchange rates if none of the addresses in "all tokens" or "all detected tokens" change, when normalized to checksum addresses', async () => { - // await withController( - // { - // mockTokensControllerState: { - // allDetectedTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: '0x0EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE2', - // decimals: 3, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }, - // }, - // async ({ controller, triggerTokensStateChange }) => { - // const updateExchangeRatesSpy = jest - // .spyOn(controller, 'updateExchangeRates') - // .mockResolvedValue(); - // await controller.start(ChainId.mainnet, 'ETH'); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allDetectedTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: '0x0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2', - // decimals: 7, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); - - // // Once when starting, and that's it - // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); - // }, - // ); - // }); - - // it('should not update exchange rates if any of the addresses in "all tokens" or "all detected tokens" merely change order', async () => { - // await withController( - // { - // mockTokensControllerState: { - // allDetectedTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: '0xE1', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // { - // address: '0xE2', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }, - // }, - // async ({ controller, triggerTokensStateChange }) => { - // const updateExchangeRatesSpy = jest - // .spyOn(controller, 'updateExchangeRates') - // .mockResolvedValue(); - // await controller.start(ChainId.mainnet, 'ETH'); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allDetectedTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: '0xE2', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // { - // address: '0xE1', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); - - // // Once when starting, and that's it - // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); - // }, - // ); - // }); - // }); - - // describe('when legacy polling is inactive', () => { - // it('should not update exchange rates when any of the addresses in the "all tokens" collection change', async () => { - // const tokenAddresses = ['0xE1', '0xE2']; - // await withController( - // { - // mockTokensControllerState: { - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddresses[0], - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }, - // }, - // async ({ controller, triggerTokensStateChange }) => { - // const updateExchangeRatesSpy = jest - // .spyOn(controller, 'updateExchangeRates') - // .mockResolvedValue(); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddresses[1], - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); - - // expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); - // }, - // ); - // }); - - // it('should not update exchange rates when any of the addresses in the "all detected tokens" collection change', async () => { - // const tokenAddresses = ['0xE1', '0xE2']; - // await withController( - // { - // mockTokensControllerState: { - // allDetectedTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddresses[0], - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }, - // }, - // async ({ controller, triggerTokensStateChange }) => { - // const updateExchangeRatesSpy = jest - // .spyOn(controller, 'updateExchangeRates') - // .mockResolvedValue(); - // triggerTokensStateChange({ - // ...getDefaultTokensState(), - // allDetectedTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddresses[1], - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }); - - // expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); - // }, - // ); - // }); - // }); - // }); - - // describe('NetworkController::stateChange', () => { - // let clock: sinon.SinonFakeTimers; - - // beforeEach(() => { - // clock = useFakeTimers({ now: Date.now() }); - // }); - - // afterEach(() => { - // clock.restore(); - // }); - - // describe('when polling is active', () => { - // it('should update exchange rates when ticker changes', async () => { - // await withController( - // { - // options: { - // interval: 100, - // }, - // mockNetworkClientConfigurationsByNetworkClientId: { - // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - // chainId: toHex(1337), - // ticker: 'NEW', - // }), - // }, - // }, - // async ({ controller, triggerNetworkStateChange }) => { - // await controller.start(ChainId.mainnet, 'ETH'); - // const updateExchangeRatesSpy = jest - // .spyOn(controller, 'updateExchangeRates') - // .mockResolvedValue(); - // triggerNetworkStateChange({ - // ...getDefaultNetworkControllerState(), - // selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', - // }); - - // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); - // }, - // ); - // }); - - // it('should not update exchange rates when chain ID changes', async () => { - // await withController( - // { - // options: { - // interval: 100, - // }, - // mockNetworkClientConfigurationsByNetworkClientId: { - // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - // chainId: toHex(1338), - // ticker: 'TEST', - // }), - // }, - // }, - // async ({ controller, triggerNetworkStateChange }) => { - // await controller.start(ChainId.mainnet, 'ETH'); - // const updateExchangeRatesSpy = jest - // .spyOn(controller, 'updateExchangeRates') - // .mockResolvedValue(); - // triggerNetworkStateChange({ - // ...getDefaultNetworkControllerState(), - // selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', - // }); - - // expect(updateExchangeRatesSpy).toHaveBeenCalledTimes(1); - // }, - // ); - // }); - - // it('should clear marketData in state when event is triggeredclear', async () => { - // await withController( - // { - // options: { - // interval: 100, - // state: { - // marketData: { - // [ChainId.mainnet]: { - // '0x02': { - // currency: 'ETH', - // priceChange1d: 0, - // pricePercentChange1d: 0, - // tokenAddress: '0x02', - // allTimeHigh: 4000, - // allTimeLow: 900, - // circulatingSupply: 2000, - // dilutedMarketCap: 100, - // high1d: 200, - // low1d: 100, - // marketCap: 1000, - // marketCapPercentChange1d: 100, - // price: 0.001, - // pricePercentChange14d: 100, - // pricePercentChange1h: 1, - // pricePercentChange1y: 200, - // pricePercentChange200d: 300, - // pricePercentChange30d: 200, - // pricePercentChange7d: 100, - // totalVolume: 100, - // }, - // }, - // }, - // }, - // }, - // mockNetworkClientConfigurationsByNetworkClientId: { - // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - // chainId: toHex(1337), - // ticker: 'NEW', - // }), - // }, - // }, - // async ({ controller, triggerNetworkStateChange }) => { - // await controller.start(ChainId.mainnet, 'ETH'); - // jest.spyOn(controller, 'updateExchangeRates').mockResolvedValue(); - // triggerNetworkStateChange({ - // ...getDefaultNetworkControllerState(), - // selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', - // }); - - // expect(controller.state.marketData).toStrictEqual({ - // '0x1': {}, - // }); - // }, - // ); - // }); - - // it('should update exchange rates when network state changes without adding a new network', async () => { - // await withController( - // { - // options: { - // interval: 100, - // }, - // mockNetworkClientConfigurationsByNetworkClientId: { - // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - // chainId: ChainId.mainnet, - // ticker: NetworksTicker.mainnet, - // }), - // }, - // }, - // async ({ controller, triggerNetworkStateChange }) => { - // await controller.start(ChainId.mainnet, 'ETH'); - // const updateExchangeRatesSpy = jest - // .spyOn(controller, 'updateExchangeRates') - // .mockResolvedValue(); - // triggerNetworkStateChange( - // { - // ...getDefaultNetworkControllerState(), - // selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', - // }, - // [ - // { - // op: 'add', - // path: ['networkConfigurationsByChainId', ChainId.mainnet], - // }, - // ], - // ); - // expect(updateExchangeRatesSpy).toHaveBeenCalled(); - // }, - // ); - // }); - // }); - - // describe('when polling is inactive', () => { - // it('should not update exchange rates when ticker changes', async () => { - // await withController( - // { - // options: { - // interval: 100, - // }, - // mockNetworkClientConfigurationsByNetworkClientId: { - // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - // chainId: toHex(1337), - // ticker: 'NEW', - // }), - // }, - // }, - // async ({ controller, triggerNetworkStateChange }) => { - // const updateExchangeRatesSpy = jest - // .spyOn(controller, 'updateExchangeRates') - // .mockResolvedValue(); - // triggerNetworkStateChange({ - // ...getDefaultNetworkControllerState(), - // selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', - // }); - - // expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); - // }, - // ); - // }); - - // it('should not update exchange rates when chain ID changes', async () => { - // await withController( - // { - // options: { - // interval: 100, - // }, - // mockNetworkClientConfigurationsByNetworkClientId: { - // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - // chainId: toHex(1338), - // ticker: 'TEST', - // }), - // }, - // }, - // async ({ controller, triggerNetworkStateChange }) => { - // const updateExchangeRatesSpy = jest - // .spyOn(controller, 'updateExchangeRates') - // .mockResolvedValue(); - // triggerNetworkStateChange({ - // ...getDefaultNetworkControllerState(), - // selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', - // }); - - // expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); - // }, - // ); - // }); - - // it('should not clear marketData state when ticker changes', async () => { - // await withController( - // { - // options: { - // interval: 100, - // state: { - // marketData: { - // [ChainId.mainnet]: { - // '0x02': { - // currency: 'ETH', - // priceChange1d: 0, - // pricePercentChange1d: 0, - // tokenAddress: '0x02', - // allTimeHigh: 4000, - // allTimeLow: 900, - // circulatingSupply: 2000, - // dilutedMarketCap: 100, - // high1d: 200, - // low1d: 100, - // marketCap: 1000, - // marketCapPercentChange1d: 100, - // price: 0.001, - // pricePercentChange14d: 100, - // pricePercentChange1h: 1, - // pricePercentChange1y: 200, - // pricePercentChange200d: 300, - // pricePercentChange30d: 200, - // pricePercentChange7d: 100, - // totalVolume: 100, - // }, - // }, - // }, - // }, - // }, - // mockNetworkClientConfigurationsByNetworkClientId: { - // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - // chainId: toHex(1337), - // ticker: 'NEW', - // }), - // }, - // }, - // async ({ controller, triggerNetworkStateChange }) => { - // jest.spyOn(controller, 'updateExchangeRates').mockResolvedValue(); - // triggerNetworkStateChange({ - // ...getDefaultNetworkControllerState(), - // selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', - // }); - - // expect(controller.state.marketData).toStrictEqual({ - // '0x1': { - // '0x02': { - // currency: 'ETH', - // priceChange1d: 0, - // pricePercentChange1d: 0, - // tokenAddress: '0x02', - // allTimeHigh: 4000, - // allTimeLow: 900, - // circulatingSupply: 2000, - // dilutedMarketCap: 100, - // high1d: 200, - // low1d: 100, - // marketCap: 1000, - // marketCapPercentChange1d: 100, - // price: 0.001, - // pricePercentChange14d: 100, - // pricePercentChange1h: 1, - // pricePercentChange1y: 200, - // pricePercentChange200d: 300, - // pricePercentChange30d: 200, - // pricePercentChange7d: 100, - // totalVolume: 100, - // }, - // }, - // }); - // }, - // ); - // }); - - // it('should not clear marketData state when chain ID changes', async () => { - // await withController( - // { - // options: { - // interval: 100, - // state: { - // marketData: { - // [ChainId.mainnet]: { - // '0x02': { - // currency: 'ETH', - // priceChange1d: 0, - // pricePercentChange1d: 0, - // tokenAddress: '0x02', - // allTimeHigh: 4000, - // allTimeLow: 900, - // circulatingSupply: 2000, - // dilutedMarketCap: 100, - // high1d: 200, - // low1d: 100, - // marketCap: 1000, - // marketCapPercentChange1d: 100, - // price: 0.001, - // pricePercentChange14d: 100, - // pricePercentChange1h: 1, - // pricePercentChange1y: 200, - // pricePercentChange200d: 300, - // pricePercentChange30d: 200, - // pricePercentChange7d: 100, - // totalVolume: 100, - // }, - // }, - // }, - // }, - // }, - // mockNetworkClientConfigurationsByNetworkClientId: { - // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - // chainId: toHex(1338), - // ticker: 'TEST', - // }), - // }, - // }, - // async ({ controller, triggerNetworkStateChange }) => { - // jest.spyOn(controller, 'updateExchangeRates').mockResolvedValue(); - // triggerNetworkStateChange({ - // ...getDefaultNetworkControllerState(), - // selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', - // }); - - // expect(controller.state.marketData).toStrictEqual({ - // '0x1': { - // '0x02': { - // currency: 'ETH', - // priceChange1d: 0, - // pricePercentChange1d: 0, - // tokenAddress: '0x02', - // allTimeHigh: 4000, - // allTimeLow: 900, - // circulatingSupply: 2000, - // dilutedMarketCap: 100, - // high1d: 200, - // low1d: 100, - // marketCap: 1000, - // marketCapPercentChange1d: 100, - // price: 0.001, - // pricePercentChange14d: 100, - // pricePercentChange1h: 1, - // pricePercentChange1y: 200, - // pricePercentChange200d: 300, - // pricePercentChange30d: 200, - // pricePercentChange7d: 100, - // totalVolume: 100, - // }, - // }, - // }); - // }, - // ); - // }); - // }); - - // it('removes state when networks are deleted', async () => { - // const marketData = { - // [ChainId.mainnet]: { - // '0x123456': { - // currency: 'ETH', - // priceChange1d: 0, - // pricePercentChange1d: 0, - // tokenAddress: '0x02', - // allTimeHigh: 4000, - // allTimeLow: 900, - // circulatingSupply: 2000, - // dilutedMarketCap: 100, - // high1d: 200, - // low1d: 100, - // marketCap: 1000, - // marketCapPercentChange1d: 100, - // price: 0.001, - // pricePercentChange14d: 100, - // pricePercentChange1h: 1, - // pricePercentChange1y: 200, - // pricePercentChange200d: 300, - // pricePercentChange30d: 200, - // pricePercentChange7d: 100, - // totalVolume: 100, - // }, - // }, - // [ChainId['linea-mainnet']]: { - // '0x789': { - // currency: 'ETH', - // priceChange1d: 0, - // pricePercentChange1d: 0, - // tokenAddress: '0x02', - // allTimeHigh: 4000, - // allTimeLow: 900, - // circulatingSupply: 2000, - // dilutedMarketCap: 100, - // high1d: 200, - // low1d: 100, - // marketCap: 1000, - // marketCapPercentChange1d: 100, - // price: 0.001, - // pricePercentChange14d: 100, - // pricePercentChange1h: 1, - // pricePercentChange1y: 200, - // pricePercentChange200d: 300, - // pricePercentChange30d: 200, - // pricePercentChange7d: 100, - // totalVolume: 100, - // }, - // }, - // } as const; - - // await withController( - // { - // options: { - // state: { - // marketData, - // }, - // }, - // }, - // async ({ controller, triggerNetworkStateChange }) => { - // // Verify initial state with both networks - // expect(controller.state.marketData).toStrictEqual(marketData); - - // triggerNetworkStateChange( - // { - // selectedNetworkClientId: 'mainnet', - // networkConfigurationsByChainId: {}, - // } as NetworkState, - // [ - // { - // op: 'remove', - // path: [ - // 'networkConfigurationsByChainId', - // ChainId['linea-mainnet'], - // ], - // }, - // ], - // ); - - // // Verify linea removed - // expect(controller.state.marketData).toStrictEqual({ - // [ChainId.mainnet]: marketData[ChainId.mainnet], - // }); - // }, - // ); - // }); - // }); - - // describe('PreferencesController::stateChange', () => { - // let clock: sinon.SinonFakeTimers; - - // beforeEach(() => { - // clock = useFakeTimers({ now: Date.now() }); - // }); - - // afterEach(() => { - // clock.restore(); - // }); - - // describe('when polling is active', () => { - // it('should not update exchange rates when selected address changes', async () => { - // const alternateSelectedAddress = - // '0x0000000000000000000000000000000000000002'; - // const alternateSelectedAccount = createMockInternalAccount({ - // address: alternateSelectedAddress, - // }); - // await withController( - // { - // options: { - // interval: 100, - // }, - // mockTokensControllerState: { - // allTokens: { - // '0x1': { - // [alternateSelectedAddress]: [ - // { - // address: '0x02', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // { - // address: '0x03', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }, - // }, - // async ({ controller, triggerSelectedAccountChange }) => { - // await controller.start(ChainId.mainnet, 'ETH'); - // const updateExchangeRatesSpy = jest - // .spyOn(controller, 'updateExchangeRates') - // .mockResolvedValue(); - // triggerSelectedAccountChange(alternateSelectedAccount); - - // expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); - // }, - // ); - // }); - // }); - - // describe('when polling is inactive', () => { - // it('does not update exchange rates when selected account changes', async () => { - // const alternateSelectedAddress = - // '0x0000000000000000000000000000000000000002'; - // const alternateSelectedAccount = createMockInternalAccount({ - // address: alternateSelectedAddress, - // }); - // await withController( - // { - // options: { - // interval: 100, - // }, - // mockTokensControllerState: { - // allTokens: { - // '0x1': { - // [alternateSelectedAddress]: [ - // { - // address: '0x02', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // { - // address: '0x03', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }, - // }, - // async ({ controller, triggerSelectedAccountChange }) => { - // const updateExchangeRatesSpy = jest - // .spyOn(controller, 'updateExchangeRates') - // .mockResolvedValue(); - // triggerSelectedAccountChange(alternateSelectedAccount); - - // expect(updateExchangeRatesSpy).not.toHaveBeenCalled(); - // }, - // ); - // }); - // }); - // }); - - // describe('legacy polling', () => { - // let clock: sinon.SinonFakeTimers; - - // beforeEach(() => { - // clock = useFakeTimers({ now: Date.now() }); - // }); - - // afterEach(() => { - // clock.restore(); - // }); - - // describe('start', () => { - // it('should poll and update rate in the right interval', async () => { - // const interval = 100; - // const tokenPricesService = buildMockTokenPricesService(); - // jest.spyOn(tokenPricesService, 'fetchTokenPrices'); - // await withController( - // { - // options: { - // interval, - // tokenPricesService, - // }, - // mockTokensControllerState: { - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }, - // }, - // async ({ controller }) => { - // await controller.start(ChainId.mainnet, 'ETH'); - - // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( - // 1, - // ); - - // await advanceTime({ clock, duration: interval }); - // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( - // 2, - // ); - - // await advanceTime({ clock, duration: interval }); - // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( - // 3, - // ); - // }, - // ); - // }); - // }); - - // describe('stop', () => { - // it('should stop polling', async () => { - // const interval = 100; - // const tokenPricesService = buildMockTokenPricesService(); - // jest.spyOn(tokenPricesService, 'fetchTokenPrices'); - // await withController( - // { - // options: { - // interval, - // tokenPricesService, - // }, - // mockTokensControllerState: { - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }, - // }, - // async ({ controller }) => { - // await controller.start(ChainId.mainnet, 'ETH'); - - // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( - // 1, - // ); - - // controller.stop(); - - // await advanceTime({ clock, duration: interval }); - // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( - // 1, - // ); - // }, - // ); - // }); - // }); - // }); - - // describe('polling by networkClientId', () => { - // let clock: sinon.SinonFakeTimers; - - // beforeEach(() => { - // clock = useFakeTimers({ now: Date.now() }); - // }); - - // afterEach(() => { - // clock.restore(); - // }); - - // it('should poll on the right interval', async () => { - // const interval = 100; - // const tokenPricesService = buildMockTokenPricesService(); - // jest.spyOn(tokenPricesService, 'fetchTokenPrices'); - // await withController( - // { - // options: { - // interval, - // tokenPricesService, - // }, - // mockTokensControllerState: { - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }, - // }, - // async ({ controller }) => { - // controller.startPolling({ - // chainIds: [ChainId.mainnet], - // }); - - // await advanceTime({ clock, duration: 0 }); - // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); - - // await advanceTime({ clock, duration: interval }); - // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(2); - - // await advanceTime({ clock, duration: interval }); - // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(3); - // }, - // ); - // }); - - // describe('updating state on poll', () => { - // describe('when the native currency is supported', () => { - // it('returns the exchange rates directly', async () => { - // const tokenPricesService = buildMockTokenPricesService({ - // fetchTokenPrices: fetchTokenPricesWithIncreasingPriceForEachToken, - // validateCurrencySupported(currency: unknown): currency is string { - // return currency === 'ETH'; - // }, - // }); - // const interval = 100; - // await withController( - // { - // options: { - // interval, - // tokenPricesService, - // }, - // mockTokensControllerState: { - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: '0x02', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // { - // address: '0x03', - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }, - // mockNetworkClientConfigurationsByNetworkClientId: { - // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - // chainId: ChainId.mainnet, - // ticker: 'ETH', - // }), - // }, - // }, - // async ({ controller }) => { - // controller.startPolling({ - // chainIds: [ChainId.mainnet], - // }); - // await advanceTime({ clock, duration: 0 }); - - // expect(controller.state).toStrictEqual({ - // marketData: { - // [ChainId.mainnet]: { - // [ZERO_ADDRESS]: { - // currency: 'ETH', - // priceChange1d: 0, - // pricePercentChange1d: 0, - // tokenAddress: ZERO_ADDRESS, - // chainId: '0x1', - // assetId: 'eip155:1/slip44:60', - // allTimeHigh: 4000, - // allTimeLow: 900, - // circulatingSupply: 2000, - // dilutedMarketCap: 100, - // high1d: 200, - // low1d: 100, - // marketCap: 1000, - // marketCapPercentChange1d: 100, - // price: 0.001, - // pricePercentChange14d: 100, - // pricePercentChange1h: 1, - // pricePercentChange1y: 200, - // pricePercentChange200d: 300, - // pricePercentChange30d: 200, - // pricePercentChange7d: 100, - // totalVolume: 100, - // }, - // '0x02': { - // currency: 'ETH', - // priceChange1d: 0, - // pricePercentChange1d: 0, - // tokenAddress: '0x02', - // chainId: '0x1', - // assetId: 'eip155:1/erc20:0x02', - // allTimeHigh: 4000, - // allTimeLow: 900, - // circulatingSupply: 2000, - // dilutedMarketCap: 100, - // high1d: 200, - // low1d: 100, - // marketCap: 1000, - // marketCapPercentChange1d: 100, - // price: 0.002, - // pricePercentChange14d: 100, - // pricePercentChange1h: 1, - // pricePercentChange1y: 200, - // pricePercentChange200d: 300, - // pricePercentChange30d: 200, - // pricePercentChange7d: 100, - // totalVolume: 100, - // }, - // '0x03': { - // currency: 'ETH', - // priceChange1d: 0, - // pricePercentChange1d: 0, - // tokenAddress: '0x03', - // chainId: '0x1', - // assetId: 'eip155:1/erc20:0x03', - // allTimeHigh: 4000, - // allTimeLow: 900, - // circulatingSupply: 2000, - // dilutedMarketCap: 100, - // high1d: 200, - // low1d: 100, - // marketCap: 1000, - // marketCapPercentChange1d: 100, - // price: 0.003, - // pricePercentChange14d: 100, - // pricePercentChange1h: 1, - // pricePercentChange1y: 200, - // pricePercentChange200d: 300, - // pricePercentChange30d: 200, - // pricePercentChange7d: 100, - // totalVolume: 100, - // }, - // }, - // }, - // }); - // }, - // ); - // }); - // }); - - // it('should stop polling', async () => { - // const interval = 100; - // const tokenPricesService = buildMockTokenPricesService(); - // jest.spyOn(tokenPricesService, 'fetchTokenPrices'); - // await withController( - // { - // options: { - // tokenPricesService, - // }, - // mockTokensControllerState: { - // allTokens: { - // '0x1': { - // [defaultSelectedAddress]: [ - // { - // address: mockTokenAddress, - // decimals: 0, - // symbol: '', - // aggregators: [], - // }, - // ], - // }, - // }, - // }, - // }, - // async ({ controller }) => { - // const pollingToken = controller.startPolling({ - // chainIds: [ChainId.mainnet], - // }); - // await advanceTime({ clock, duration: 0 }); - // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( - // 1, - // ); - - // controller.stopPollingByPollingToken(pollingToken); - - // await advanceTime({ clock, duration: interval }); - // expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( - // 1, - // ); - // }, - // ); - // }); - // }); - - // it('does not update state when disabled', async () => { - // await withController( - // {}, - // async ({ - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // }) => { - // const tokenAddress = '0x0000000000000000000000000000000000000001'; - // controller.disable(); - // await callUpdateExchangeRatesMethod({ - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddress, - // decimals: 18, - // symbol: 'TST', - // aggregators: [], - // }, - // ], - // }, - // }, - // chainId: ChainId.mainnet, - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // nativeCurrency: 'ETH', - // selectedNetworkClientId: InfuraNetworkType.mainnet, - // }); - - // expect(controller.state.marketData).toStrictEqual({}); - // }, - // ); - // }); - - // it('does not update state if there are no tokens for the given chain', async () => { - // await withController( - // async ({ - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // }) => { - // const tokenAddress = '0x0000000000000000000000000000000000000001'; - // controller.enable(); - // await callUpdateExchangeRatesMethod({ - // allTokens: { - // // These tokens are on a different chain - // [toHex(2)]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddress, - // decimals: 18, - // symbol: 'TST', - // aggregators: [], - // }, - // ], - // }, - // }, - // chainId: ChainId.mainnet, - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // nativeCurrency: 'ETH', - // selectedNetworkClientId: InfuraNetworkType.mainnet, - // }); - - // expect(controller.state).toStrictEqual({ - // marketData: {}, - // }); - // }, - // ); - // }); - - // it('does not update state if the price update fails', async () => { - // const tokenPricesService = buildMockTokenPricesService({ - // fetchTokenPrices: jest - // .fn() - // .mockRejectedValue(new Error('Failed to fetch')), - // }); - // await withController( - // { options: { tokenPricesService } }, - // async ({ - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // }) => { - // const tokenAddress = '0x0000000000000000000000000000000000000001'; - - // const updateExchangeRates = await callUpdateExchangeRatesMethod({ - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddress, - // decimals: 18, - // symbol: 'TST', - // aggregators: [], - // }, - // ], - // }, - // }, - // chainId: ChainId.mainnet, - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // nativeCurrency: 'ETH', - // selectedNetworkClientId: InfuraNetworkType.mainnet, - // }); - - // expect(updateExchangeRates).toBeUndefined(); - // expect(controller.state.marketData).toStrictEqual({}); - // }, - // ); - // }); - - // it('fetches rates for all tokens in batches', async () => { - // const chainId = ChainId.mainnet; - // const ticker = NetworksTicker.mainnet; - // const tokenAddresses = [...new Array(200).keys()] - // .map(buildAddress) - // .sort(); - // const tokenPricesService = buildMockTokenPricesService({ - // fetchTokenPrices: fetchTokenPricesWithIncreasingPriceForEachToken, - // }); - // const fetchTokenPricesSpy = jest.spyOn( - // tokenPricesService, - // 'fetchTokenPrices', - // ); - // const tokens = tokenAddresses.map((tokenAddress) => { - // return buildToken({ address: tokenAddress }); - // }); - // await withController( - // { - // options: { - // tokenPricesService, - // }, - // }, - // async ({ - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // }) => { - // await callUpdateExchangeRatesMethod({ - // allTokens: { - // [chainId]: { - // [defaultSelectedAddress]: tokens.slice(0, 100), - // // Include tokens from non selected addresses - // '0x0000000000000000000000000000000000000123': tokens.slice(100), - // }, - // }, - // chainId, - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // nativeCurrency: ticker, - // selectedNetworkClientId: InfuraNetworkType.mainnet, - // }); - - // const numBatches = Math.ceil( - // tokenAddresses.length / TOKEN_PRICES_BATCH_SIZE, - // ); - // expect(fetchTokenPricesSpy).toHaveBeenCalledTimes(numBatches); - - // for (let i = 1; i <= numBatches; i++) { - // expect(fetchTokenPricesSpy).toHaveBeenNthCalledWith(i, { - // assets: tokenAddresses - // .slice( - // (i - 1) * TOKEN_PRICES_BATCH_SIZE, - // i * TOKEN_PRICES_BATCH_SIZE, - // ) - // .map((tokenAddress) => ({ - // chainId, - // tokenAddress, - // })), - // currency: ticker, - // }); - // } - // }, - // ); - // }); - - // it('updates all rates', async () => { - // const tokenAddresses = [ - // '0x0000000000000000000000000000000000000001', - // '0x0000000000000000000000000000000000000002', - // '0x0000000000000000000000000000000000000003', - // ]; - // const tokenPricesService = buildMockTokenPricesService({ - // fetchTokenPrices: jest.fn().mockResolvedValue({ - // [tokenAddresses[0]]: { - // currency: 'ETH', - // tokenAddress: tokenAddresses[0], - // value: 0.001, - // }, - // [tokenAddresses[1]]: { - // currency: 'ETH', - // tokenAddress: tokenAddresses[1], - // value: 0.002, - // }, - // [tokenAddresses[2]]: { - // currency: 'ETH', - // tokenAddress: tokenAddresses[2], - // value: 0.003, - // }, - // }), - // }); - // await withController( - // { options: { tokenPricesService } }, - // async ({ - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // }) => { - // await callUpdateExchangeRatesMethod({ - // allTokens: { - // [ChainId.mainnet]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddresses[0], - // decimals: 18, - // symbol: 'TST1', - // aggregators: [], - // }, - // { - // address: tokenAddresses[1], - // decimals: 18, - // symbol: 'TST2', - // aggregators: [], - // }, - // ], - // // Include tokens from non selected addresses - // '0x0000000000000000000000000000000000000123': [ - // { - // address: tokenAddresses[2], - // decimals: 18, - // symbol: 'TST1', - // aggregators: [], - // }, - // ], - // }, - // }, - // chainId: ChainId.mainnet, - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // nativeCurrency: 'ETH', - // 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, - // }, - // }, - // }, - // } - // `); - // }, - // ); - // }); - - // it('updates rates only for a non-selected chain', async () => { - // const tokenAddresses = [ - // '0x0000000000000000000000000000000000000001', - // '0x0000000000000000000000000000000000000002', - // ]; - // const tokenPricesService = buildMockTokenPricesService({ - // fetchTokenPrices: jest.fn().mockResolvedValue({ - // [tokenAddresses[0]]: { - // currency: 'ETH', - // tokenAddress: tokenAddresses[0], - // value: 0.001, - // }, - // [tokenAddresses[1]]: { - // currency: 'ETH', - // tokenAddress: tokenAddresses[1], - // value: 0.002, - // }, - // }), - // }); - // await withController( - // { options: { tokenPricesService } }, - // async ({ - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // }) => { - // await callUpdateExchangeRatesMethod({ - // allTokens: { - // [toHex(2)]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddresses[0], - // decimals: 18, - // symbol: 'TST1', - // aggregators: [], - // }, - // { - // address: tokenAddresses[1], - // decimals: 18, - // symbol: 'TST2', - // aggregators: [], - // }, - // ], - // }, - // }, - // chainId: toHex(2), - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // nativeCurrency: 'ETH', - // 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, - // }, - // }, - // }, - // } - // `); - // }, - // ); - // }); - - // it('updates exchange rates when native currency is not supported by the Price API', async () => { - // const selectedNetworkClientId = 'AAAA-BBBB-CCCC-DDDD'; - // const selectedNetworkClientConfiguration = - // buildCustomNetworkClientConfiguration({ - // chainId: toHex(137), - // ticker: 'UNSUPPORTED', - // }); - // const tokenAddresses = [ - // '0x0000000000000000000000000000000000000001', - // '0x0000000000000000000000000000000000000002', - // ]; - // const tokenPricesService = buildMockTokenPricesService({ - // fetchTokenPrices: jest.fn().mockResolvedValue({ - // [tokenAddresses[0]]: { - // currency: 'ETH', - // tokenAddress: tokenAddresses[0], - // price: 0.001, - // }, - // [tokenAddresses[1]]: { - // currency: 'ETH', - // tokenAddress: tokenAddresses[1], - // price: 0.002, - // }, - // }), - // validateCurrencySupported(_currency: unknown): _currency is string { - // return false; - // }, - // }); - // nock('https://min-api.cryptocompare.com') - // .get('/data/price') - // .query({ - // fsym: 'ETH', - // tsyms: selectedNetworkClientConfiguration.ticker, - // }) - // .reply(200, { [selectedNetworkClientConfiguration.ticker]: 0.5 }); // .5 eth to 1 matic - - // await withController( - // { - // options: { tokenPricesService }, - // mockNetworkClientConfigurationsByNetworkClientId: { - // [selectedNetworkClientId]: selectedNetworkClientConfiguration, - // }, - // }, - // async ({ - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // }) => { - // await callUpdateExchangeRatesMethod({ - // allTokens: { - // [selectedNetworkClientConfiguration.chainId]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddresses[0], - // decimals: 18, - // symbol: 'TST1', - // aggregators: [], - // }, - // { - // address: tokenAddresses[1], - // decimals: 18, - // symbol: 'TST2', - // aggregators: [], - // }, - // ], - // }, - // }, - // chainId: selectedNetworkClientConfiguration.chainId, - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // nativeCurrency: selectedNetworkClientConfiguration.ticker, - // selectedNetworkClientId, - // }); - - // // 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, - // }, - // }, - // }, - // } - // `); - // }, - // ); - // }); - - // it('fetches rates for all tokens in batches when native currency is not supported by the Price API', async () => { - // const selectedNetworkClientId = 'AAAA-BBBB-CCCC-DDDD'; - // const selectedNetworkClientConfiguration = - // buildCustomNetworkClientConfiguration({ - // chainId: toHex(999), - // ticker: 'UNSUPPORTED', - // }); - // const tokenAddresses = [...new Array(200).keys()] - // .map(buildAddress) - // .sort(); - // const tokenPricesService = buildMockTokenPricesService({ - // fetchTokenPrices: fetchTokenPricesWithIncreasingPriceForEachToken, - // validateCurrencySupported: (currency: unknown): currency is string => { - // return currency !== selectedNetworkClientConfiguration.ticker; - // }, - // }); - // const fetchTokenPricesSpy = jest.spyOn( - // tokenPricesService, - // 'fetchTokenPrices', - // ); - // const tokens = tokenAddresses.map((tokenAddress) => { - // return buildToken({ address: tokenAddress }); - // }); - // nock('https://min-api.cryptocompare.com') - // .get('/data/price') - // .query({ - // fsym: 'ETH', - // tsyms: selectedNetworkClientConfiguration.ticker, - // }) - // .reply(200, { [selectedNetworkClientConfiguration.ticker]: 0.5 }); - // await withController( - // { - // options: { - // tokenPricesService, - // }, - // mockNetworkClientConfigurationsByNetworkClientId: { - // [selectedNetworkClientId]: selectedNetworkClientConfiguration, - // }, - // mockNetworkState: { - // networkConfigurationsByChainId: { - // [selectedNetworkClientConfiguration.chainId]: { - // nativeCurrency: selectedNetworkClientConfiguration.ticker, - // chainId: selectedNetworkClientConfiguration.chainId, - // name: 'UNSUPPORTED', - // rpcEndpoints: [], - // blockExplorerUrls: [], - // defaultRpcEndpointIndex: 0, - // }, - // }, - // selectedNetworkClientId, - // }, - // }, - // async ({ - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // }) => { - // await callUpdateExchangeRatesMethod({ - // allTokens: { - // [selectedNetworkClientConfiguration.chainId]: { - // [defaultSelectedAddress]: tokens, - // }, - // }, - // chainId: selectedNetworkClientConfiguration.chainId, - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // nativeCurrency: selectedNetworkClientConfiguration.ticker, - // selectedNetworkClientId, - // }); - - // const numBatches = Math.ceil( - // tokenAddresses.length / TOKEN_PRICES_BATCH_SIZE, - // ); - // expect(fetchTokenPricesSpy).toHaveBeenCalledTimes(numBatches); - - // for (let i = 1; i <= numBatches; i++) { - // expect(fetchTokenPricesSpy).toHaveBeenNthCalledWith(i, { - // chainId: selectedNetworkClientConfiguration.chainId, - // tokenAddresses: tokenAddresses.slice( - // (i - 1) * TOKEN_PRICES_BATCH_SIZE, - // i * TOKEN_PRICES_BATCH_SIZE, - // ), - // currency: 'ETH', - // }); - // } - // }, - // ); - // }); - - // it('sets rates to undefined when chain is not supported by the Price API', async () => { - // const selectedNetworkClientId = 'AAAA-BBBB-CCCC-DDDD'; - // const selectedNetworkClientConfiguration = - // buildCustomNetworkClientConfiguration({ - // chainId: toHex(999), - // ticker: 'TST', - // }); - // const tokenAddresses = [ - // '0x0000000000000000000000000000000000000001', - // '0x0000000000000000000000000000000000000002', - // ]; - // const tokenPricesService = buildMockTokenPricesService({ - // fetchTokenPrices: jest.fn().mockResolvedValue({ - // [tokenAddresses[0]]: { - // currency: 'ETH', - // tokenAddress: tokenAddresses[0], - // value: 0.001, - // }, - // [tokenAddresses[1]]: { - // currency: 'ETH', - // tokenAddress: tokenAddresses[1], - // value: 0.002, - // }, - // }), - // validateChainIdSupported(_chainId: unknown): _chainId is Hex { - // return false; - // }, - // }); - // await withController( - // { - // options: { tokenPricesService }, - // mockNetworkClientConfigurationsByNetworkClientId: { - // [selectedNetworkClientId]: selectedNetworkClientConfiguration, - // }, - // }, - // async ({ - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // }) => { - // await callUpdateExchangeRatesMethod({ - // allTokens: { - // [toHex(999)]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddresses[0], - // decimals: 18, - // symbol: 'TST1', - // aggregators: [], - // }, - // { - // address: tokenAddresses[1], - // decimals: 18, - // symbol: 'TST2', - // aggregators: [], - // }, - // ], - // }, - // }, - // chainId: selectedNetworkClientConfiguration.chainId, - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // nativeCurrency: selectedNetworkClientConfiguration.ticker, - // selectedNetworkClientId, - // }); - - // expect(controller.state).toMatchInlineSnapshot(` - // Object { - // "marketData": Object { - // "0x3e7": Object { - // "0x0000000000000000000000000000000000000001": undefined, - // "0x0000000000000000000000000000000000000002": undefined, - // }, - // }, - // } - // `); - // }, - // ); - // }); - - // it('correctly calls the Price API with unqiue native token addresses (e.g. MATIC)', async () => { - // const tokenPricesService = buildMockTokenPricesService({ - // fetchTokenPrices: jest.fn().mockResolvedValue({ - // '0x0000000000000000000000000000000000001010': { - // currency: 'MATIC', - // tokenAddress: '0x0000000000000000000000000000000000001010', - // value: 0.001, - // }, - // }), - // }); - - // await withController( - // { - // options: { tokenPricesService }, - // mockNetworkClientConfigurationsByNetworkClientId: { - // 'AAAA-BBBB-CCCC-DDDD': buildCustomNetworkClientConfiguration({ - // chainId: '0x89', - // }), - // }, - // }, - // async ({ - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // }) => { - // await callUpdateExchangeRatesMethod({ - // allTokens: { - // '0x89': { - // [defaultSelectedAddress]: [], - // }, - // }, - // chainId: '0x89', - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // nativeCurrency: 'MATIC', - // selectedNetworkClientId: 'AAAA-BBBB-CCCC-DDDD', - // }); - - // expect( - // controller.state.marketData['0x89'][ - // '0x0000000000000000000000000000000000001010' - // ], - // ).toBeDefined(); - // }, - // ); - // }); - - // it('only updates rates once when called twice', 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 tokenPricesService = buildMockTokenPricesService({ - // fetchTokenPrices: fetchTokenPricesMock, - // }); - // await withController( - // { options: { tokenPricesService } }, - // async ({ - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // }) => { - // const updateExchangeRates = async () => - // await callUpdateExchangeRatesMethod({ - // allTokens: { - // [toHex(1)]: { - // [defaultSelectedAddress]: [ - // { - // address: tokenAddresses[0], - // decimals: 18, - // symbol: 'TST1', - // aggregators: [], - // }, - // { - // address: tokenAddresses[1], - // decimals: 18, - // symbol: 'TST2', - // aggregators: [], - // }, - // ], - // }, - // }, - // chainId: ChainId.mainnet, - // selectedNetworkClientId: InfuraNetworkType.mainnet, - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // nativeCurrency: 'ETH', - // }); - - // await Promise.all([updateExchangeRates(), updateExchangeRates()]); - - // 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, - // }, - // }, - // }, - // } - // `); - // }, - // ); - // }); - - // it('will update rates twice if detected tokens increased during second call', async () => { - // const tokenAddresses = [ - // '0x0000000000000000000000000000000000000001', - // '0x0000000000000000000000000000000000000002', - // ]; - // const fetchTokenPricesMock = jest.fn().mockResolvedValue([ - // { - // currency: 'ETH', - // tokenAddress: tokenAddresses[0], - // value: 0.001, - // }, - // { - // currency: 'ETH', - // tokenAddress: tokenAddresses[1], - // value: 0.002, - // }, - // ]); - // const tokenPricesService = buildMockTokenPricesService({ - // fetchTokenPrices: fetchTokenPricesMock, - // }); - // await withController( - // { options: { tokenPricesService } }, - // async ({ - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // }) => { - // const request1Payload = [ - // { - // address: tokenAddresses[0], - // decimals: 18, - // symbol: 'TST1', - // aggregators: [], - // }, - // ]; - // const request2Payload = [ - // { - // address: tokenAddresses[0], - // decimals: 18, - // symbol: 'TST1', - // aggregators: [], - // }, - // { - // address: tokenAddresses[1], - // decimals: 18, - // symbol: 'TST2', - // aggregators: [], - // }, - // ]; - // const updateExchangeRates = async ( - // tokens: typeof request1Payload | typeof request2Payload, - // ) => - // await callUpdateExchangeRatesMethod({ - // allTokens: { - // [toHex(1)]: { - // [defaultSelectedAddress]: tokens, - // }, - // }, - // chainId: ChainId.mainnet, - // selectedNetworkClientId: InfuraNetworkType.mainnet, - // controller, - // triggerTokensStateChange, - // triggerNetworkStateChange, - // nativeCurrency: 'ETH', - // }); - - // await Promise.all([ - // updateExchangeRates(request1Payload), - // updateExchangeRates(request2Payload), - // ]); - - // expect(fetchTokenPricesMock).toHaveBeenCalledTimes(2); - // expect(fetchTokenPricesMock).toHaveBeenNthCalledWith( - // 1, - // expect.objectContaining({ - // assets: [{ tokenAddress: tokenAddresses[0], chainId: '0x1' }], - // }), - // ); - // expect(fetchTokenPricesMock).toHaveBeenNthCalledWith( - // 2, - // expect.objectContaining({ - // assets: [ - // { tokenAddress: tokenAddresses[0], chainId: '0x1' }, - // { tokenAddress: tokenAddresses[1], chainId: '0x1' }, - // ], - // }), - // ); - // }, - // ); - // }); - // }); - describe('resetState', () => { it('resets the state to default state', async () => { const initialState: TokenRatesControllerState = { @@ -3013,12 +877,7 @@ async function withController( ...args: WithControllerArgs ): Promise { const [{ ...rest }, fn] = args.length === 2 ? args : [{}, args[0]]; - const { - options, - mockNetworkClientConfigurationsByNetworkClientId, - mockTokensControllerState, - mockNetworkState, - } = rest; + const { options, mockTokensControllerState, mockNetworkState } = rest; const messenger: RootMessenger = new Messenger({ namespace: MOCK_ANY_NAMESPACE, }); @@ -3032,14 +891,6 @@ async function withController( }), ); - const getNetworkClientById = buildMockGetNetworkClientById( - mockNetworkClientConfigurationsByNetworkClientId, - ); - messenger.registerActionHandler( - 'NetworkController:getNetworkClientById', - getNetworkClientById, - ); - const networkStateMock = jest.fn(); messenger.registerActionHandler( 'NetworkController:getState', @@ -3049,18 +900,6 @@ async function withController( }), ); - const mockGetSelectedAccount = jest.fn(); - messenger.registerActionHandler( - 'AccountsController:getSelectedAccount', - mockGetSelectedAccount.mockReturnValue(defaultSelectedAccount), - ); - - const mockGetAccount = jest.fn(); - messenger.registerActionHandler( - 'AccountsController:getAccount', - mockGetAccount.mockReturnValue(defaultSelectedAccount), - ); - const controller = new TokenRatesController({ tokenPricesService: buildMockTokenPricesService(), messenger: buildTokenRatesControllerMessenger(messenger), @@ -3084,83 +923,6 @@ async function withController( } } -/** - * Call an "update exchange rates" method with the given parameters. - * - * The TokenRatesController has two methods for updating exchange rates: - * `updateExchangeRates` and `updateExchangeRatesByChainId`. They are the same - * except in how the inputs are specified. `updateExchangeRates` gets the - * inputs from controller configuration, whereas `updateExchangeRatesByChainId` - * accepts the inputs as parameters. - * - * This helper function normalizes between these two functions, so that we can - * test them the same way. - * - * @param args - The arguments. - * @param args.allTokens - The `allTokens` state (from the TokensController) - * @param args.chainId - The chain ID of the chain we want to update the - * exchange rates for. - * @param args.controller - The controller to call the method with. - * @param args.triggerTokensStateChange - Controller event handlers, used to - * update controller configuration. - * @param args.triggerNetworkStateChange - Controller event handlers, used to - * update controller configuration. - * @param args.nativeCurrency - The symbol for the native currency of the - * network we're getting updated exchange rates for. - * @param args.setChainAsCurrent - When calling `updateExchangeRatesByChainId`, - * this determines whether to set the chain as the globally selected chain. - * @param args.selectedNetworkClientId - The network client ID to use if - * `setChainAsCurrent` is true. - */ -async function callUpdateExchangeRatesMethod({ - allTokens, - chainId, - controller, - triggerTokensStateChange, - triggerNetworkStateChange, - nativeCurrency, - selectedNetworkClientId, - setChainAsCurrent = true, -}: { - allTokens: TokensControllerState['allTokens']; - chainId: Hex; - controller: TokenRatesController; - triggerTokensStateChange: (state: TokensControllerState) => void; - triggerNetworkStateChange: (state: NetworkState) => void; - nativeCurrency: string; - selectedNetworkClientId?: NetworkClientId; - setChainAsCurrent?: boolean; -}) { - triggerTokensStateChange({ - ...getDefaultTokensState(), - allDetectedTokens: {}, - allTokens, - }); - - if (setChainAsCurrent) { - assert( - selectedNetworkClientId, - 'The "selectedNetworkClientId" option must be given if the "setChainAsCurrent" flag is also given', - ); - - // We're using controller events here instead of calling `configure` - // because `configure` does not update internal controller state correctly. - // As with many BaseControllerV1-based controllers, runtime config - // modification is allowed by the API but not supported in practice. - triggerNetworkStateChange({ - ...getDefaultNetworkControllerState(), - selectedNetworkClientId, - }); - } - - await controller.updateExchangeRates([ - { - chainId, - nativeCurrency, - }, - ]); -} - /** * Builds a mock token prices service. * diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index b2dbfcade0e..5a8aac8df54 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -1,7 +1,3 @@ -import type { - AccountsControllerGetAccountAction, - AccountsControllerGetSelectedAccountAction, -} from '@metamask/accounts-controller'; import type { ControllerGetStateAction, ControllerStateChangeEvent, @@ -10,7 +6,6 @@ import type { import { toChecksumHexAddress } from '@metamask/controller-utils'; import type { Messenger } from '@metamask/messenger'; import type { - NetworkControllerGetNetworkClientByIdAction, NetworkControllerGetStateAction, NetworkControllerStateChangeEvent, } from '@metamask/network-controller'; @@ -96,10 +91,7 @@ type ChainIdAndNativeCurrency = { */ export type AllowedActions = | TokensControllerGetStateAction - | NetworkControllerGetNetworkClientByIdAction - | NetworkControllerGetStateAction - | AccountsControllerGetAccountAction - | AccountsControllerGetSelectedAccountAction; + | NetworkControllerGetStateAction; /** * The external events available to the {@link TokenRatesController}. @@ -309,19 +301,16 @@ export class TokenRatesController extends StaticIntervalPollingController { - const chainIdAndNativeCurrency: { - chainId: Hex; - nativeCurrency: string; - }[] = Object.values(networkConfigurationsByChainId).map( - ({ chainId, nativeCurrency }) => { + if (!this.#disabled) { + const chainIdAndNativeCurrency = Object.values( + networkConfigurationsByChainId, + ).map(({ chainId, nativeCurrency }) => { return { - chainId: chainId as Hex, + chainId, nativeCurrency, }; - }, - ); + }); - if (!this.#disabled) { await this.updateExchangeRates(chainIdAndNativeCurrency); } From 689ba0544000914eb1bec37fb741c53c2be9e0b8 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 18 Nov 2025 12:05:34 +0000 Subject: [PATCH 23/42] fallback --- .../src/TokenRatesController.test.ts | 495 ++++++++++++++++++ .../src/TokenRatesController.ts | 195 +++++-- 2 files changed, 642 insertions(+), 48 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index 22417101ec9..d918399805f 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -250,6 +250,501 @@ describe('TokenRatesController', () => { }, ); }); + + it('fetches rates for unsupported native currencies', async () => { + const chainId = '0x1'; + const nativeCurrency = 'ETH'; + + const tokenPricesService = buildMockTokenPricesService({ + fetchTokenPrices: async ({ currency }) => { + return [ + { + tokenAddress: ZERO_ADDRESS, + chainId, + assetId: `${KnownCaipNamespace.Eip155}:1/slip44:60`, + currency, + price: 50, + pricePercentChange1d: 0, + priceChange1d: 0, + allTimeHigh: 60, + allTimeLow: 40, + circulatingSupply: 2000, + dilutedMarketCap: 1000, + high1d: 55, + low1d: 45, + marketCap: 2000, + marketCapPercentChange1d: 100, + pricePercentChange14d: 100, + pricePercentChange1h: 1, + pricePercentChange1y: 200, + pricePercentChange200d: 300, + pricePercentChange30d: 200, + pricePercentChange7d: 100, + totalVolume: 100, + }, + { + tokenAddress: '0x0000000000000000000000000000000000000001', + chainId, + assetId: `${KnownCaipNamespace.Eip155}:1/erc20:0x0000000000000000000000000000000000000001`, + currency, + price: 100, + pricePercentChange1d: 0, + priceChange1d: 0, + allTimeHigh: 200, + allTimeLow: 80, + circulatingSupply: 2000, + dilutedMarketCap: 500, + high1d: 110, + low1d: 95, + marketCap: 1000, + marketCapPercentChange1d: 100, + pricePercentChange14d: 100, + pricePercentChange1h: 1, + pricePercentChange1y: 200, + pricePercentChange200d: 300, + pricePercentChange30d: 200, + pricePercentChange7d: 100, + totalVolume: 100, + }, + ]; + }, + validateCurrencySupported: (_currency: unknown): _currency is string => + false, + }); + jest.spyOn(tokenPricesService, 'fetchTokenPrices'); + + await withController( + { + options: { + tokenPricesService, + }, + mockTokensControllerState: { + allTokens: { + [chainId]: { + [defaultSelectedAddress]: [ + { + address: '0x0000000000000000000000000000000000000001', + decimals: 0, + symbol: 'TOK1', + }, + ], + }, + }, + }, + }, + async ({ controller }) => { + await controller.updateExchangeRates([ + { + chainId, + nativeCurrency, + }, + ]); + + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledWith({ + assets: [ + { + chainId, + tokenAddress: '0x0000000000000000000000000000000000000000', + }, + { + chainId, + tokenAddress: '0x0000000000000000000000000000000000000001', + }, + ], + currency: 'usd', + }); + + expect(controller.state.marketData).toStrictEqual({ + '0x1': { + '0x0000000000000000000000000000000000000000': { + tokenAddress: '0x0000000000000000000000000000000000000000', + chainId: '0x1', + assetId: 'eip155:1/slip44:60', + currency: 'ETH', + price: 1, + pricePercentChange1d: 0, + priceChange1d: 0, + allTimeHigh: 1.2, + allTimeLow: 0.8, + circulatingSupply: 2000, + dilutedMarketCap: 20, + high1d: 1.1, + low1d: 0.9, + marketCap: 40, + marketCapPercentChange1d: 100, + pricePercentChange14d: 100, + pricePercentChange1h: 1, + pricePercentChange1y: 200, + pricePercentChange200d: 300, + pricePercentChange30d: 200, + pricePercentChange7d: 100, + totalVolume: 2, + }, + '0x0000000000000000000000000000000000000001': { + tokenAddress: '0x0000000000000000000000000000000000000001', + chainId: '0x1', + assetId: + 'eip155:1/erc20:0x0000000000000000000000000000000000000001', + currency: 'ETH', + price: 2, + pricePercentChange1d: 0, + priceChange1d: 0, + allTimeHigh: 4, + allTimeLow: 1.6, + circulatingSupply: 2000, + dilutedMarketCap: 10, + high1d: 2.2, + low1d: 1.9, + marketCap: 20, + marketCapPercentChange1d: 100, + pricePercentChange14d: 100, + pricePercentChange1h: 1, + pricePercentChange1y: 200, + pricePercentChange200d: 300, + pricePercentChange30d: 200, + pricePercentChange7d: 100, + totalVolume: 2, + }, + }, + }); + }, + ); + }); + + it('does not convert prices when the native currency fallback price is 0', async () => { + const chainId = '0x1'; + const nativeCurrency = 'ETH'; + + const tokenPricesService = buildMockTokenPricesService({ + fetchTokenPrices: async ({ currency }) => { + return [ + { + tokenAddress: ZERO_ADDRESS, + chainId, + assetId: `${KnownCaipNamespace.Eip155}:1/slip44:60`, + currency, + price: 0, + pricePercentChange1d: 0, + priceChange1d: 0, + allTimeHigh: 60, + allTimeLow: 40, + circulatingSupply: 2000, + dilutedMarketCap: 1000, + high1d: 55, + low1d: 45, + marketCap: 2000, + marketCapPercentChange1d: 100, + pricePercentChange14d: 100, + pricePercentChange1h: 1, + pricePercentChange1y: 200, + pricePercentChange200d: 300, + pricePercentChange30d: 200, + pricePercentChange7d: 100, + totalVolume: 100, + }, + { + tokenAddress: '0x0000000000000000000000000000000000000001', + chainId, + assetId: `${KnownCaipNamespace.Eip155}:1/erc20:0x0000000000000000000000000000000000000001`, + currency, + price: 100, + pricePercentChange1d: 0, + priceChange1d: 0, + allTimeHigh: 200, + allTimeLow: 80, + circulatingSupply: 2000, + dilutedMarketCap: 500, + high1d: 110, + low1d: 95, + marketCap: 1000, + marketCapPercentChange1d: 100, + pricePercentChange14d: 100, + pricePercentChange1h: 1, + pricePercentChange1y: 200, + pricePercentChange200d: 300, + pricePercentChange30d: 200, + pricePercentChange7d: 100, + totalVolume: 100, + }, + ]; + }, + validateCurrencySupported: (_currency: unknown): _currency is string => + false, + }); + jest.spyOn(tokenPricesService, 'fetchTokenPrices'); + + await withController( + { + options: { + tokenPricesService, + }, + mockTokensControllerState: { + allTokens: { + [chainId]: { + [defaultSelectedAddress]: [ + { + address: '0x0000000000000000000000000000000000000001', + decimals: 0, + symbol: 'TOK1', + }, + ], + }, + }, + }, + }, + async ({ controller }) => { + await controller.updateExchangeRates([ + { + chainId, + nativeCurrency, + }, + ]); + + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledWith({ + assets: [ + { + chainId, + tokenAddress: '0x0000000000000000000000000000000000000000', + }, + { + chainId, + tokenAddress: '0x0000000000000000000000000000000000000001', + }, + ], + currency: 'usd', + }); + + expect(controller.state.marketData).toStrictEqual({ + '0x1': {}, + }); + }, + ); + }); + + it('does not convert prices when the native currency fallback price is missing', async () => { + const chainId = '0x1'; + const nativeCurrency = 'ETH'; + + const tokenPricesService = buildMockTokenPricesService({ + fetchTokenPrices: async ({ currency }) => { + return [ + { + tokenAddress: '0x0000000000000000000000000000000000000001', + chainId, + assetId: `${KnownCaipNamespace.Eip155}:1/erc20:0x0000000000000000000000000000000000000001`, + currency, + price: 100, + pricePercentChange1d: 0, + priceChange1d: 0, + allTimeHigh: 200, + allTimeLow: 80, + circulatingSupply: 2000, + dilutedMarketCap: 500, + high1d: 110, + low1d: 95, + marketCap: 1000, + marketCapPercentChange1d: 100, + pricePercentChange14d: 100, + pricePercentChange1h: 1, + pricePercentChange1y: 200, + pricePercentChange200d: 300, + pricePercentChange30d: 200, + pricePercentChange7d: 100, + totalVolume: 100, + }, + ]; + }, + validateCurrencySupported: (_currency: unknown): _currency is string => + false, + }); + jest.spyOn(tokenPricesService, 'fetchTokenPrices'); + + await withController( + { + options: { + tokenPricesService, + }, + mockTokensControllerState: { + allTokens: { + [chainId]: { + [defaultSelectedAddress]: [ + { + address: '0x0000000000000000000000000000000000000001', + decimals: 0, + symbol: 'TOK1', + }, + ], + }, + }, + }, + }, + async ({ controller }) => { + await controller.updateExchangeRates([ + { + chainId, + nativeCurrency, + }, + ]); + + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledWith({ + assets: [ + { + chainId, + tokenAddress: '0x0000000000000000000000000000000000000000', + }, + { + chainId, + tokenAddress: '0x0000000000000000000000000000000000000001', + }, + ], + currency: 'usd', + }); + + expect(controller.state.marketData).toStrictEqual({ + '0x1': {}, + }); + }, + ); + }); + + it('does not convert prices when the token currency fallback price is missing', async () => { + const chainId = '0x1'; + const nativeCurrency = 'ETH'; + + const tokenPricesService = buildMockTokenPricesService({ + fetchTokenPrices: async ({ currency }) => { + return [ + { + tokenAddress: ZERO_ADDRESS, + chainId, + assetId: `${KnownCaipNamespace.Eip155}:1/slip44:60`, + currency, + price: 50, + pricePercentChange1d: 0, + priceChange1d: 0, + allTimeHigh: 60, + allTimeLow: 40, + circulatingSupply: 2000, + dilutedMarketCap: 1000, + high1d: 55, + low1d: 45, + marketCap: 2000, + marketCapPercentChange1d: 100, + pricePercentChange14d: 100, + pricePercentChange1h: 1, + pricePercentChange1y: 200, + pricePercentChange200d: 300, + pricePercentChange30d: 200, + pricePercentChange7d: 100, + totalVolume: 100, + }, + { + tokenAddress: '0x0000000000000000000000000000000000000001', + chainId, + assetId: `${KnownCaipNamespace.Eip155}:1/erc20:0x0000000000000000000000000000000000000001`, + currency, + price: undefined, + pricePercentChange1d: 0, + priceChange1d: 0, + allTimeHigh: 200, + allTimeLow: 80, + circulatingSupply: 2000, + dilutedMarketCap: 500, + high1d: 110, + low1d: 95, + marketCap: 1000, + marketCapPercentChange1d: 100, + pricePercentChange14d: 100, + pricePercentChange1h: 1, + pricePercentChange1y: 200, + pricePercentChange200d: 300, + pricePercentChange30d: 200, + pricePercentChange7d: 100, + totalVolume: 100, + }, + ] as unknown as EvmAssetWithMarketData[]; + }, + validateCurrencySupported: (_currency: unknown): _currency is string => + false, + }); + jest.spyOn(tokenPricesService, 'fetchTokenPrices'); + + await withController( + { + options: { + tokenPricesService, + }, + mockTokensControllerState: { + allTokens: { + [chainId]: { + [defaultSelectedAddress]: [ + { + address: '0x0000000000000000000000000000000000000001', + decimals: 0, + symbol: 'TOK1', + }, + ], + }, + }, + }, + }, + async ({ controller }) => { + await controller.updateExchangeRates([ + { + chainId, + nativeCurrency, + }, + ]); + + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); + expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledWith({ + assets: [ + { + chainId, + tokenAddress: '0x0000000000000000000000000000000000000000', + }, + { + chainId, + tokenAddress: '0x0000000000000000000000000000000000000001', + }, + ], + currency: 'usd', + }); + + expect(controller.state.marketData).toStrictEqual({ + '0x1': { + '0x0000000000000000000000000000000000000000': { + tokenAddress: '0x0000000000000000000000000000000000000000', + chainId: '0x1', + assetId: 'eip155:1/slip44:60', + currency: 'ETH', + price: 1, + pricePercentChange1d: 0, + priceChange1d: 0, + allTimeHigh: 1.2, + allTimeLow: 0.8, + circulatingSupply: 2000, + dilutedMarketCap: 20, + high1d: 1.1, + low1d: 0.9, + marketCap: 40, + marketCapPercentChange1d: 100, + pricePercentChange14d: 100, + pricePercentChange1h: 1, + pricePercentChange1y: 200, + pricePercentChange200d: 300, + pricePercentChange30d: 200, + pricePercentChange7d: 100, + totalVolume: 2, + }, + }, + }); + }, + ); + }); }); describe('_executePoll', () => { diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 5a8aac8df54..63c5fa517f0 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -398,59 +398,53 @@ export class TokenRatesController extends StaticIntervalPollingController = {}; + const unsupportedAssetsByNativeCurrency: Record< + string, + { + chainId: Hex; + tokenAddress: Hex; + }[] + > = {}; for (const { chainId, nativeCurrency } of chainIdAndNativeCurrency) { if (this.#tokenPricesService.validateChainIdSupported(chainId)) { - this.#getTokenAddresses(chainId).forEach((tokenAddress) => { - (assetsByNativeCurrency[nativeCurrency] ??= []).push({ - chainId, - tokenAddress, - }); - }); - } else { - marketData[chainId] = {}; + for (const tokenAddress of this.#getTokenAddresses(chainId)) { + if ( + this.#tokenPricesService.validateCurrencySupported(nativeCurrency) + ) { + (assetsByNativeCurrency[nativeCurrency] ??= []).push({ + chainId, + tokenAddress, + }); + } else { + (unsupportedAssetsByNativeCurrency[nativeCurrency] ??= []).push({ + chainId, + tokenAddress, + }); + } + } } } - // console.log('EEEEEE', { - // assetsByNativeCurrency, - // chainIdAndNativeCurrency, - // }); - - await Promise.allSettled( - Object.entries(assetsByNativeCurrency).map( - async ([nativeCurrency, assets]) => { - return await reduceInBatchesSerially< - { chainId: Hex; tokenAddress: Hex }, - Record> - >({ - values: assets, - batchSize: TOKEN_PRICES_BATCH_SIZE, - eachBatch: async (partialMarketData, assetsBatch) => { - const batchMarketData = - await this.#tokenPricesService.fetchTokenPrices({ - assets: assetsBatch, - currency: nativeCurrency, - }); - - // console.log('FFFFF', { - // batchMarketData, - // assetsBatch, - // nativeCurrency, - // }); - - for (const tokenPrice of batchMarketData) { - (partialMarketData[tokenPrice.chainId] ??= {})[ - tokenPrice.tokenAddress - ] = tokenPrice; - } - - return partialMarketData; - }, - initialResult: marketData, - }); - }, + const promises = [ + ...Object.entries(assetsByNativeCurrency).map( + ([nativeCurrency, assets]) => + this.#fetchAndMapExchangeRatesForSupportedNativeCurrency( + assets, + nativeCurrency, + marketData, + ), ), - ); + ...Object.entries(unsupportedAssetsByNativeCurrency).map( + ([nativeCurrency, assets]) => + this.#fetchAndMapExchangeRatesForUnsupportedNativeCurrency( + assets, + nativeCurrency, + marketData, + ), + ), + ]; + + await Promise.allSettled(promises); const chainIds = new Set( Object.values(chainIdAndNativeCurrency).map((chain) => chain.chainId), @@ -461,7 +455,6 @@ export class TokenRatesController extends StaticIntervalPollingController 0) { this.update((state) => { @@ -473,6 +466,112 @@ export class TokenRatesController extends StaticIntervalPollingController> = {}, + ) { + return await reduceInBatchesSerially< + { chainId: Hex; tokenAddress: Hex }, + Record> + >({ + values: assets, + batchSize: TOKEN_PRICES_BATCH_SIZE, + eachBatch: async (partialMarketData, assetsBatch) => { + const batchMarketData = await this.#tokenPricesService.fetchTokenPrices( + { + assets: assetsBatch, + currency, + }, + ); + + for (const tokenPrice of batchMarketData) { + (partialMarketData[tokenPrice.chainId] ??= {})[ + tokenPrice.tokenAddress + ] = tokenPrice; + } + + return partialMarketData; + }, + initialResult: marketData, + }); + } + + async #fetchAndMapExchangeRatesForUnsupportedNativeCurrency( + assets: { + chainId: Hex; + tokenAddress: Hex; + }[], + currency: string, + marketData: Record>, + ) { + // Step -1: Then fetch all tracked tokens priced in USD + const marketDataInUSD = + await this.#fetchAndMapExchangeRatesForSupportedNativeCurrency( + assets, + 'usd', // Fallback currency when the native currency is not supported + ); + + // Formula: price_in_native = token_usd / native_usd + const convertUSDToNative = ( + valueInUSD: number, + nativeTokenPriceInUSD: number, + ) => valueInUSD / nativeTokenPriceInUSD; + + // Step -2: Convert USD prices to native currency + for (const [chainId, marketDataByTokenAddress] of Object.entries( + marketDataInUSD, + ) as [Hex, Record][]) { + const nativeTokenPriceInUSD = + marketDataByTokenAddress[getNativeTokenAddress(chainId)]?.price; + + // Return here if it's null, undefined or 0 + if (!nativeTokenPriceInUSD) { + continue; + } + + for (const [tokenAddress, tokenData] of Object.entries( + marketDataByTokenAddress, + ) as [Hex, MarketDataDetails][]) { + // eslint-disable-next-line no-eq-null -- Checking for null or undefined only + if (tokenData.price == null) { + continue; + } + + (marketData[chainId] ??= {})[tokenAddress] = { + ...tokenData, + currency, + price: convertUSDToNative(tokenData.price, nativeTokenPriceInUSD), + marketCap: convertUSDToNative( + tokenData.marketCap, + nativeTokenPriceInUSD, + ), + allTimeHigh: convertUSDToNative( + tokenData.allTimeHigh, + nativeTokenPriceInUSD, + ), + allTimeLow: convertUSDToNative( + tokenData.allTimeLow, + nativeTokenPriceInUSD, + ), + totalVolume: convertUSDToNative( + tokenData.totalVolume, + nativeTokenPriceInUSD, + ), + high1d: convertUSDToNative(tokenData.high1d, nativeTokenPriceInUSD), + low1d: convertUSDToNative(tokenData.low1d, nativeTokenPriceInUSD), + dilutedMarketCap: convertUSDToNative( + tokenData.dilutedMarketCap, + nativeTokenPriceInUSD, + ), + }; + } + } + } + /** * Updates token rates for the given networkClientId * From 51c0efdee5cafcf7ef9944245230cfef9fedec1d Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 18 Nov 2025 12:09:46 +0000 Subject: [PATCH 24/42] removed overcautious check --- .../src/TokenRatesController.test.ts | 136 ------------------ .../src/TokenRatesController.ts | 5 - 2 files changed, 141 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index d918399805f..12c9fb93a75 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -609,142 +609,6 @@ describe('TokenRatesController', () => { }, ); }); - - it('does not convert prices when the token currency fallback price is missing', async () => { - const chainId = '0x1'; - const nativeCurrency = 'ETH'; - - const tokenPricesService = buildMockTokenPricesService({ - fetchTokenPrices: async ({ currency }) => { - return [ - { - tokenAddress: ZERO_ADDRESS, - chainId, - assetId: `${KnownCaipNamespace.Eip155}:1/slip44:60`, - currency, - price: 50, - pricePercentChange1d: 0, - priceChange1d: 0, - allTimeHigh: 60, - allTimeLow: 40, - circulatingSupply: 2000, - dilutedMarketCap: 1000, - high1d: 55, - low1d: 45, - marketCap: 2000, - marketCapPercentChange1d: 100, - pricePercentChange14d: 100, - pricePercentChange1h: 1, - pricePercentChange1y: 200, - pricePercentChange200d: 300, - pricePercentChange30d: 200, - pricePercentChange7d: 100, - totalVolume: 100, - }, - { - tokenAddress: '0x0000000000000000000000000000000000000001', - chainId, - assetId: `${KnownCaipNamespace.Eip155}:1/erc20:0x0000000000000000000000000000000000000001`, - currency, - price: undefined, - pricePercentChange1d: 0, - priceChange1d: 0, - allTimeHigh: 200, - allTimeLow: 80, - circulatingSupply: 2000, - dilutedMarketCap: 500, - high1d: 110, - low1d: 95, - marketCap: 1000, - marketCapPercentChange1d: 100, - pricePercentChange14d: 100, - pricePercentChange1h: 1, - pricePercentChange1y: 200, - pricePercentChange200d: 300, - pricePercentChange30d: 200, - pricePercentChange7d: 100, - totalVolume: 100, - }, - ] as unknown as EvmAssetWithMarketData[]; - }, - validateCurrencySupported: (_currency: unknown): _currency is string => - false, - }); - jest.spyOn(tokenPricesService, 'fetchTokenPrices'); - - await withController( - { - options: { - tokenPricesService, - }, - mockTokensControllerState: { - allTokens: { - [chainId]: { - [defaultSelectedAddress]: [ - { - address: '0x0000000000000000000000000000000000000001', - decimals: 0, - symbol: 'TOK1', - }, - ], - }, - }, - }, - }, - async ({ controller }) => { - await controller.updateExchangeRates([ - { - chainId, - nativeCurrency, - }, - ]); - - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); - expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledWith({ - assets: [ - { - chainId, - tokenAddress: '0x0000000000000000000000000000000000000000', - }, - { - chainId, - tokenAddress: '0x0000000000000000000000000000000000000001', - }, - ], - currency: 'usd', - }); - - expect(controller.state.marketData).toStrictEqual({ - '0x1': { - '0x0000000000000000000000000000000000000000': { - tokenAddress: '0x0000000000000000000000000000000000000000', - chainId: '0x1', - assetId: 'eip155:1/slip44:60', - currency: 'ETH', - price: 1, - pricePercentChange1d: 0, - priceChange1d: 0, - allTimeHigh: 1.2, - allTimeLow: 0.8, - circulatingSupply: 2000, - dilutedMarketCap: 20, - high1d: 1.1, - low1d: 0.9, - marketCap: 40, - marketCapPercentChange1d: 100, - pricePercentChange14d: 100, - pricePercentChange1h: 1, - pricePercentChange1y: 200, - pricePercentChange200d: 300, - pricePercentChange30d: 200, - pricePercentChange7d: 100, - totalVolume: 2, - }, - }, - }); - }, - ); - }); }); describe('_executePoll', () => { diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 63c5fa517f0..f4f38ec0530 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -536,11 +536,6 @@ export class TokenRatesController extends StaticIntervalPollingController Date: Tue, 18 Nov 2025 12:31:33 +0000 Subject: [PATCH 25/42] fixes for tests --- .../src/CurrencyRateController.test.ts | 109 ++++++++++++------ .../src/CurrencyRateController.ts | 9 +- .../src/TokenRatesController.ts | 4 +- 3 files changed, 81 insertions(+), 41 deletions(-) diff --git a/packages/assets-controllers/src/CurrencyRateController.test.ts b/packages/assets-controllers/src/CurrencyRateController.test.ts index bed88442aa8..133830487a5 100644 --- a/packages/assets-controllers/src/CurrencyRateController.test.ts +++ b/packages/assets-controllers/src/CurrencyRateController.test.ts @@ -1138,12 +1138,15 @@ describe('CurrencyRateController', () => { // Mock fetchTokenPrices to return token prices jest .spyOn(tokenPricesService, 'fetchTokenPrices') - .mockImplementation(async ({ chainId }) => { - if (chainId === '0x1') { - return { - '0x0000000000000000000000000000000000000000': { + .mockImplementation(async ({ assets }) => { + // eslint-disable-next-line jest/no-conditional-in-test + if (assets.some((asset) => asset.chainId === '0x1')) { + return [ + { currency: 'usd', tokenAddress: '0x0000000000000000000000000000000000000000', + chainId: assets[0].chainId, + assetId: 'xx:yy/aa:bb', price: 2500.5, pricePercentChange1d: 0, priceChange1d: 0, @@ -1163,13 +1166,16 @@ describe('CurrencyRateController', () => { pricePercentChange7d: 100, totalVolume: 100, }, - }; + ]; } - if (chainId === '0x89') { - return { - '0x0000000000000000000000000000000000001010': { + // eslint-disable-next-line jest/no-conditional-in-test + if (assets.some((asset) => asset.chainId === '0x89')) { + return [ + { currency: 'usd', tokenAddress: '0x0000000000000000000000000000000000001010', + chainId: assets[0].chainId, + assetId: 'xx:yy/aa:bb', price: 0.85, pricePercentChange1d: 0, priceChange1d: 0, @@ -1189,9 +1195,9 @@ describe('CurrencyRateController', () => { pricePercentChange7d: 100, totalVolume: 100, }, - }; + ]; } - return {}; + return []; }); // Make crypto compare also fail by not mocking it (no nock setup) @@ -1255,12 +1261,21 @@ describe('CurrencyRateController', () => { const fetchTokenPricesSpy = jest .spyOn(tokenPricesService, 'fetchTokenPrices') - .mockImplementation(async ({ chainId }) => { - if (chainId === '0x1' || chainId === '0xaa36a7') { - return { - '0x0000000000000000000000000000000000000000': { + .mockImplementation(async ({ assets }) => { + // eslint-disable-next-line jest/no-conditional-in-test + if ( + assets.some( + (asset) => + // eslint-disable-next-line jest/no-conditional-in-test + asset.chainId === '0x1' || asset.chainId === '0xaa36a7', + ) + ) { + return [ + { currency: 'usd', tokenAddress: '0x0000000000000000000000000000000000000000', + chainId: assets[0].chainId, + assetId: 'xx:yy/aa:bb', price: 2500.5, pricePercentChange1d: 0, priceChange1d: 0, @@ -1280,9 +1295,9 @@ describe('CurrencyRateController', () => { pricePercentChange7d: 100, totalVolume: 100, }, - }; + ]; } - return {}; + return []; }); const controller = new CurrencyRateController({ @@ -1296,8 +1311,12 @@ describe('CurrencyRateController', () => { // Should only call fetchTokenPrices once, using first matching chainId (line 255) expect(fetchTokenPricesSpy).toHaveBeenCalledTimes(1); expect(fetchTokenPricesSpy).toHaveBeenCalledWith({ - chainId: '0x1', // First chainId with ETH as native currency - tokenAddresses: ['0x0000000000000000000000000000000000000000'], + assets: [ + { + chainId: '0x1', + tokenAddress: '0x0000000000000000000000000000000000000000', + }, + ], currency: 'usd', }); @@ -1338,13 +1357,16 @@ describe('CurrencyRateController', () => { jest .spyOn(tokenPricesService, 'fetchTokenPrices') - .mockImplementation(async ({ chainId }) => { - if (chainId === '0x1') { + .mockImplementation(async ({ assets }) => { + // eslint-disable-next-line jest/no-conditional-in-test + if (assets.some((asset) => asset.chainId === '0x1')) { // ETH succeeds - return { - '0x0000000000000000000000000000000000000000': { + return [ + { currency: 'usd', tokenAddress: '0x0000000000000000000000000000000000000000', + chainId: assets[0].chainId, + assetId: 'xx:yy/aa:bb', price: 2500.5, pricePercentChange1d: 0, priceChange1d: 0, @@ -1364,7 +1386,7 @@ describe('CurrencyRateController', () => { pricePercentChange7d: 100, totalVolume: 100, }, - }; + ]; } // POL fails throw new Error('Failed to fetch POL price'); @@ -1427,7 +1449,7 @@ describe('CurrencyRateController', () => { .mockRejectedValue(new Error('Price API failed')); // Return empty object (no token price) - jest.spyOn(tokenPricesService, 'fetchTokenPrices').mockResolvedValue({}); + jest.spyOn(tokenPricesService, 'fetchTokenPrices').mockResolvedValue([]); const controller = new CurrencyRateController({ messenger, @@ -1475,12 +1497,15 @@ describe('CurrencyRateController', () => { const fetchTokenPricesSpy = jest .spyOn(tokenPricesService, 'fetchTokenPrices') - .mockImplementation(async ({ chainId }) => { - if (chainId === '0x1') { - return { - '0x0000000000000000000000000000000000000000': { + .mockImplementation(async ({ assets }) => { + // eslint-disable-next-line jest/no-conditional-in-test + if (assets.some((asset) => asset.chainId === '0x1')) { + return [ + { currency: 'usd', tokenAddress: '0x0000000000000000000000000000000000000000', + chainId: assets[0].chainId, + assetId: 'xx:yy/aa:bb', price: 2500.5, pricePercentChange1d: 0, priceChange1d: 0, @@ -1500,9 +1525,9 @@ describe('CurrencyRateController', () => { pricePercentChange7d: 100, totalVolume: 100, }, - }; + ]; } - return {}; + return []; }); const controller = new CurrencyRateController({ @@ -1517,8 +1542,12 @@ describe('CurrencyRateController', () => { // Should only call fetchTokenPrices for ETH, not BNB (line 252: if chainIds.length > 0) expect(fetchTokenPricesSpy).toHaveBeenCalledTimes(1); expect(fetchTokenPricesSpy).toHaveBeenCalledWith({ - chainId: '0x1', - tokenAddresses: ['0x0000000000000000000000000000000000000000'], + assets: [ + { + chainId: '0x1', + tokenAddress: '0x0000000000000000000000000000000000000000', + }, + ], currency: 'usd', }); @@ -1562,10 +1591,12 @@ describe('CurrencyRateController', () => { const fetchTokenPricesSpy = jest .spyOn(tokenPricesService, 'fetchTokenPrices') - .mockResolvedValue({ - '0x0000000000000000000000000000000000001010': { + .mockResolvedValue([ + { currency: 'usd', tokenAddress: '0x0000000000000000000000000000000000001010', + chainId: '0x89', + assetId: 'xx:yy/aa:bb', price: 0.85, pricePercentChange1d: 0, priceChange1d: 0, @@ -1585,7 +1616,7 @@ describe('CurrencyRateController', () => { pricePercentChange7d: 100, totalVolume: 100, }, - }); + ]); const controller = new CurrencyRateController({ messenger, @@ -1597,8 +1628,12 @@ describe('CurrencyRateController', () => { // Should use Polygon's native token address (line 269) expect(fetchTokenPricesSpy).toHaveBeenCalledWith({ - chainId: '0x89', - tokenAddresses: ['0x0000000000000000000000000000000000001010'], + assets: [ + { + chainId: '0x89', + tokenAddress: '0x0000000000000000000000000000000000001010', + }, + ], currency: 'usd', }); diff --git a/packages/assets-controllers/src/CurrencyRateController.ts b/packages/assets-controllers/src/CurrencyRateController.ts index 01155aa117b..5683418a333 100644 --- a/packages/assets-controllers/src/CurrencyRateController.ts +++ b/packages/assets-controllers/src/CurrencyRateController.ts @@ -258,12 +258,15 @@ export class CurrencyRateController extends StaticIntervalPollingController { const nativeTokenAddress = getNativeTokenAddress(chainId); const tokenPrices = await this.#tokenPricesService.fetchTokenPrices({ - chainId, - tokenAddresses: [nativeTokenAddress], + assets: [{ chainId, tokenAddress: nativeTokenAddress }], currency: currentCurrency, }); - const tokenPrice = tokenPrices[nativeTokenAddress]; + const tokenPrice = tokenPrices.find( + (item) => + item.tokenAddress.toLowerCase() === + nativeTokenAddress.toLowerCase(), + ); return { nativeCurrency, diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index f4f38ec0530..de33902672d 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -526,7 +526,9 @@ export class TokenRatesController extends StaticIntervalPollingController][]) { const nativeTokenPriceInUSD = - marketDataByTokenAddress[getNativeTokenAddress(chainId)]?.price; + marketDataByTokenAddress[ + getNativeTokenAddress(chainId).toLowerCase() as Hex + ]?.price; // Return here if it's null, undefined or 0 if (!nativeTokenPriceInUSD) { From ec44139efb0470ec31a619606b73bf801c7e76a1 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 18 Nov 2025 12:41:47 +0000 Subject: [PATCH 26/42] changelog --- packages/assets-controllers/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 9da3bbcefb2..d0251fb57e6 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **BREAKING:** Update `spot-prices` endpoint to use Price API v3 ([7119](https://github.com/MetaMask/core/pull/7119)) + - Update `AbstractTokenPricesService.fetchTokenPrices` arguments and return type + - Update `CodefiTokenPricesServiceV2` list of supported currencies + - Update `TokenRatesController` to fetch prices by native currency instead of by chain + - Remove legacy polling code and unused events from `TokenRatesController` - **BREAKING:** Remove fallback to CryptoCompare on `CurrencyRatesController` and `TokenRatesController` ([#7167](https://github.com/MetaMask/core/pull/7167)) - Bump `@metamask/core-backend` from `^4.0.0` to `^4.1.0` From 1310de46a8d9ba5218a9d9a9cddb00afe01a2708 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 18 Nov 2025 12:45:06 +0000 Subject: [PATCH 27/42] missing check --- .../src/TokenRatesController.test.ts | 24 +++++++++++++++++++ .../src/TokenRatesController.ts | 4 ++++ 2 files changed, 28 insertions(+) diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index 12c9fb93a75..d23d06e13ae 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -87,6 +87,30 @@ describe('TokenRatesController', () => { }); describe('updateExchangeRates', () => { + it('does not fetch when disabled', async () => { + const tokenPricesService = buildMockTokenPricesService(); + jest.spyOn(tokenPricesService, 'fetchTokenPrices'); + + await withController( + { + options: { + tokenPricesService, + disabled: true, + }, + }, + async ({ controller }) => { + await controller.updateExchangeRates([ + { + chainId: '0x1', + nativeCurrency: 'ETH', + }, + ]); + + expect(tokenPricesService.fetchTokenPrices).not.toHaveBeenCalled(); + }, + ); + }); + it('fetches rates for tokens in one batch', async () => { const chainId = '0x1'; const nativeCurrency = 'ETH'; diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index de33902672d..68c273c9d2b 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -390,6 +390,10 @@ export class TokenRatesController extends StaticIntervalPollingController { + if (this.#disabled) { + return; + } + const marketData: Record> = {}; const assetsByNativeCurrency: Record< string, From 16cf753d8e7b93f3c4093be8d7829f52d4dc45f4 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 18 Nov 2025 12:53:00 +0000 Subject: [PATCH 28/42] remove lowercase conversion --- packages/assets-controllers/src/TokenRatesController.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 68c273c9d2b..ea0b6b5eb48 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -530,9 +530,7 @@ export class TokenRatesController extends StaticIntervalPollingController][]) { const nativeTokenPriceInUSD = - marketDataByTokenAddress[ - getNativeTokenAddress(chainId).toLowerCase() as Hex - ]?.price; + marketDataByTokenAddress[getNativeTokenAddress(chainId)]?.price; // Return here if it's null, undefined or 0 if (!nativeTokenPriceInUSD) { From dcf49504b622627be7a18e0a16156c1b7b9c90e2 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 18 Nov 2025 12:55:07 +0000 Subject: [PATCH 29/42] fix changelog --- packages/assets-controllers/CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index d0251fb57e6..b68019e8333 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -10,10 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - **BREAKING:** Update `spot-prices` endpoint to use Price API v3 ([7119](https://github.com/MetaMask/core/pull/7119)) - - Update `AbstractTokenPricesService.fetchTokenPrices` arguments and return type - - Update `CodefiTokenPricesServiceV2` list of supported currencies - - Update `TokenRatesController` to fetch prices by native currency instead of by chain - - Remove legacy polling code and unused events from `TokenRatesController` + - Update `AbstractTokenPricesService.fetchTokenPrices` arguments and return type + - Update `CodefiTokenPricesServiceV2` list of supported currencies + - Update `TokenRatesController` to fetch prices by native currency instead of by chain + - Remove legacy polling code and unused events from `TokenRatesController` - **BREAKING:** Remove fallback to CryptoCompare on `CurrencyRatesController` and `TokenRatesController` ([#7167](https://github.com/MetaMask/core/pull/7167)) - Bump `@metamask/core-backend` from `^4.0.0` to `^4.1.0` From 2ce7690241c5c6959e255dc544819a188a6cd104 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 18 Nov 2025 12:59:40 +0000 Subject: [PATCH 30/42] remove trailing space --- packages/assets-controllers/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index b68019e8333..bc57bc749c1 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING:** Update `spot-prices` endpoint to use Price API v3 ([7119](https://github.com/MetaMask/core/pull/7119)) - Update `AbstractTokenPricesService.fetchTokenPrices` arguments and return type - - Update `CodefiTokenPricesServiceV2` list of supported currencies + - Update `CodefiTokenPricesServiceV2` list of supported currencies - Update `TokenRatesController` to fetch prices by native currency instead of by chain - Remove legacy polling code and unused events from `TokenRatesController` - **BREAKING:** Remove fallback to CryptoCompare on `CurrencyRatesController` and `TokenRatesController` ([#7167](https://github.com/MetaMask/core/pull/7167)) From cf12c8c93a808c9a74ebe3f7bddc10fe1a8f2e83 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 18 Nov 2025 14:11:05 +0000 Subject: [PATCH 31/42] support object --- .../src/token-prices-service/codefi-v2.ts | 165 +++++------------- 1 file changed, 47 insertions(+), 118 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 acf31eaecd7..46fea47831d 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -229,48 +229,50 @@ export const getNativeTokenAddress = (chainId: Hex): Hex => chainIdToNativeTokenAddress[chainId] ?? ZERO_ADDRESS; // We can only support PricesAPI V3 for EVM chains that have a CAIP-19 native asset mapping. -export const HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP: Record< - Hex, - CaipAssetType -> = { - '0x1': 'eip155:1/slip44:60', // Ethereum Mainnet - '0xa': 'eip155:10/slip44:60', // OP Mainnet - '0x19': 'eip155:25/slip44:394', // Cronos Mainnet - '0x38': 'eip155:56/slip44:714', // BNB Smart Chain Mainnet - '0x39': 'eip155:57/slip44:57', // Syscoin Mainnet - // '0x42': 'eip155:1/slip44:60', // OKXChain Mainnet - // '0x46': 'eip155:1/slip44:60', // Hoo Smart Chain - // '0x52': 'eip155:1/slip44:60', // Meter Mainnet - // '0x58': 'eip155:1/slip44:60', // TomoChain - // '0x64': 'eip155:1/slip44:60', // Gnosis - // '0x6a': 'eip155:1/slip44:60', // Velas EVM Mainnet - // '0x7a': 'eip155:1/slip44:60', // Fuse Mainnet - // '0x80': 'eip155:1/slip44:60', // Huobi ECO Chain Mainnet - '0x89': 'eip155:137/slip44:966', // Polygon Mainnet - '0x8f': 'eip155:143/slip44:268435779', // Monad Mainnet - // '0x92': 'eip155:1/slip44:60', // Sonic Mainnet - // '0xfa': 'eip155:1/slip44:60', // Fantom Opera - // '0x120': 'eip155:1/slip44:60', // Boba Network - // '0x141': 'eip155:1/slip44:60', // KCC Mainnet - // '0x144': 'eip155:1/slip44:60', // zkSync Era Mainnet - // '0x150': 'eip155:1/slip44:60', // Shiden - // '0x169': 'eip155:1/slip44:60', // Theta Mainnet - // '0x440': 'eip155:1/slip44:60', // Metis Andromeda Mainnet - // '0x504': 'eip155:1/slip44:60', // Moonbeam - // '0x505': 'eip155:1/slip44:60', // Moonriver - '0x531': 'eip155:1329/slip44:19000118', // Sei Mainnet - // '0x1388': 'eip155:1/slip44:60', // Mantle - '0x2105': 'eip155:8453/slip44:60', // Base - // '0x2710': 'eip155:1/slip44:60', // Smart Bitcoin Cash - '0xa4b1': 'eip155:42161/slip44:60', // Arbitrum One - // '0xa4ec': 'eip155:1/slip44:60', // Celo Mainnet - // '0xa516': 'eip155:1/slip44:60', // Oasis Emerald - '0xa86a': 'eip155:43114/slip44:9000', // Avalanche C-Chain - '0xe708': 'eip155:59144/slip44:60', // Linea Mainnet - // '0x518af': 'eip155:1/slip44:60', // Polis Mainnet - // '0x4e454152': 'eip155:1/slip44:60', // Aurora Mainnet - // '0x63564c40': 'eip155:1/slip44:60', // Harmony Mainnet Shard 0 -}; +export const HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP = { + '0x1': 'eip155:1/slip44:60', // Ethereum Mainnet - Native symbol: ETH + '0xa': 'eip155:10/slip44:60', // OP Mainnet - Native symbol: ETH + '0x19': 'eip155:25/slip44:394', // Cronos Mainnet - Native symbol: CRO + '0x38': 'eip155:56/slip44:714', // BNB Smart Chain Mainnet - Native symbol: BNB + '0x39': 'eip155:57/slip44:57', // Syscoin Mainnet - Native symbol: SYS + '0x42': 'eip155:66/slip44:996', // OKXChain Mainnet - Native symbol: OKT + '0x46': 'eip155:70/slip44:1170', // Hoo Smart Chain - Native symbol: HOO + '0x52': 'eip155:82/slip44:18000', // Meter Mainnet - Native symbol: MTR + '0x58': 'eip155:88/slip44:889', // TomoChain - Native symbol: TOMO + '0x64': 'eip155:100/slip44:700', // Gnosis (formerly xDAI Chain) - Native symbol: xDAI + '0x6a': 'eip155:106/slip44:5655640', // Velas EVM Mainnet - Native symbol: VLX + '0x7a': 'eip155:122/slip44:XXX', // Fuse Mainnet - Native symbol: FUSE + '0x80': 'eip155:128/slip44:1010', // Huobi ECO Chain Mainnet - Native symbol: HT + '0x89': 'eip155:137/slip44:966', // Polygon Mainnet - Native symbol: POL + '0x8f': 'eip155:143/slip44:268435779', // Monad Mainnet - Native symbol: MONAD + '0x92': 'eip155:146/slip44:10007', // Sonic Mainnet - Native symbol: S + '0xfa': 'eip155:250/slip44:1007', // Fantom Opera - Native symbol: FTM + '0x120': 'eip155:288/slip44:60', // Boba Network (Ethereum L2) - Native symbol: ETH + '0x141': 'eip155:321/slip44:641', // KCC Mainnet - Native symbol: KCS + '0x144': 'eip155:324/slip44:60', // zkSync Era Mainnet (Ethereum L2) - Native symbol: ETH + '0x150': 'eip155:336/slip44:809', // Shiden - Native symbol: SDN + '0x169': 'eip155:361/slip44:589', // Theta Mainnet - Native symbol: TFUEL + '0x440': 'eip155:1088/slip44:60', // Metis Andromeda Mainnet (Ethereum L2) - Native symbol: ETH + '0x44d': 'eip155:1101/slip44:60', // Polygon zkEVM mainnet - Native symbol: ETH + '0x504': 'eip155:1284/slip44:1284', // Moonbeam - Native symbol: GLMR + '0x505': 'eip155:1285/slip44:1285', // Moonriver - Native symbol: MOVR + '0x531': 'eip155:1329/slip44:19000118', // Sei Mainnet - Native symbol: SEI + '0x1388': 'eip155:5000/slip44:XXX', // Mantle - Native symbol: MNT + '0x2105': 'eip155:8453/slip44:60', // Base - Native symbol: ETH + '0x2710': 'eip155:10000/slip44:145', // Smart Bitcoin Cash - Native symbol: BCH + '0xa4b1': 'eip155:42161/slip44:60', // Arbitrum One - Native symbol: ETH + '0xa4ec': 'eip155:42220/slip44:52752', // Celo Mainnet - Native symbol: CELO + '0xa516': 'eip155:42262/slip44:474', // Oasis Emerald - Native symbol: ROSE + '0xa86a': 'eip155:43114/slip44:9000', // Avalanche C-Chain - Native symbol: AVAX + '0xe708': 'eip155:59144/slip44:60', // Linea Mainnet - Native symbol: ETH + '0x13c31': 'eip155:81457/slip44:60', // Blast Mainnet - Native symbol: ETH + '0x17dcd': 'eip155:97741/slip44:XXX', // Pepe Unchained Mainnet - Native symbol: PEPU + '0x28c58': 'eip155:167000/slip44:60', // Taiko Mainnet - Native symbol: ETH + '0x518af': 'eip155:333999/slip44:1997', // Polis Mainnet - Native symbol: POLIS + '0x82750': 'eip155:534352/slip44:60', // Scroll Mainnet - Native symbol: ETH + '0x4e454152': 'eip155:1313161554/slip44:60', // Aurora Mainnet (Ethereum L2 on NEAR) - Native symbol: ETH + '0x63564c40': 'eip155:1666600000/slip44:1023', // Harmony Mainnet Shard 0 - Native symbol: ONE +} as const; /** * A currency that can be supplied as the `vsCurrency` parameter to @@ -287,82 +289,9 @@ type SupportedCurrency = * * @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; +export const SUPPORTED_CHAIN_IDS = Object.keys( + HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP, +) as (keyof typeof HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP)[]; /** * A chain ID that can be supplied in the URL for the `/spot-prices` endpoint, From 158332d75d82cc32b5113ad496f19d335de65bee Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 18 Nov 2025 16:35:11 +0000 Subject: [PATCH 32/42] fixes --- .../src/TokenRatesController.test.ts | 69 ------------------- .../src/TokenRatesController.ts | 17 +---- .../src/token-prices-service/codefi-v2.ts | 12 ++-- 3 files changed, 7 insertions(+), 91 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index d23d06e13ae..ab4b3f42e1d 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -888,75 +888,6 @@ describe('TokenRatesController', () => { }); describe('NetworkController:stateChange', () => { - it('fetches rates for all updated chains', async () => { - const chainId = '0x1'; - const nativeCurrency = 'ETH'; - - await withController( - {}, - async ({ controller, triggerNetworkStateChange }) => { - jest.spyOn(controller, 'updateExchangeRates'); - - triggerNetworkStateChange( - { - ...getDefaultNetworkControllerState(), - networkConfigurationsByChainId: { - [chainId]: { - chainId, - nativeCurrency, - } as unknown as NetworkConfiguration, - }, - }, - [], - ); - - jest.advanceTimersToNextTimer(); - await flushPromises(); - - expect(controller.updateExchangeRates).toHaveBeenCalledWith([ - { - chainId, - nativeCurrency, - }, - ]); - }, - ); - }); - - it('does not fetch when disabled', async () => { - const chainId = '0x1'; - const nativeCurrency = 'ETH'; - - await withController( - { - options: { - disabled: true, - }, - }, - async ({ controller, triggerNetworkStateChange }) => { - jest.spyOn(controller, 'updateExchangeRates'); - - triggerNetworkStateChange( - { - ...getDefaultNetworkControllerState(), - networkConfigurationsByChainId: { - [chainId]: { - chainId, - nativeCurrency, - } as unknown as NetworkConfiguration, - }, - }, - [], - ); - - jest.advanceTimersToNextTimer(); - await flushPromises(); - - expect(controller.updateExchangeRates).not.toHaveBeenCalled(); - }, - ); - }); - it('remove state from deleted networks', async () => { const chainId = '0x1'; const nativeCurrency = 'ETH'; diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index ea0b6b5eb48..b16cde9aab7 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -298,22 +298,7 @@ export class TokenRatesController extends StaticIntervalPollingController { - if (!this.#disabled) { - const chainIdAndNativeCurrency = Object.values( - networkConfigurationsByChainId, - ).map(({ chainId, nativeCurrency }) => { - return { - chainId, - nativeCurrency, - }; - }); - - await this.updateExchangeRates(chainIdAndNativeCurrency); - } - + (_state, patches) => { // Remove state for deleted networks for (const patch of patches) { if ( 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 46fea47831d..b243a108afd 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -263,7 +263,7 @@ export const HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP = { '0xa4b1': 'eip155:42161/slip44:60', // Arbitrum One - Native symbol: ETH '0xa4ec': 'eip155:42220/slip44:52752', // Celo Mainnet - Native symbol: CELO '0xa516': 'eip155:42262/slip44:474', // Oasis Emerald - Native symbol: ROSE - '0xa86a': 'eip155:43114/slip44:9000', // Avalanche C-Chain - Native symbol: AVAX + '0xa86a': 'eip155:43114/slip44:9005', // Avalanche C-Chain - Native symbol: AVAX '0xe708': 'eip155:59144/slip44:60', // Linea Mainnet - Native symbol: ETH '0x13c31': 'eip155:81457/slip44:60', // Blast Mainnet - Native symbol: ETH '0x17dcd': 'eip155:97741/slip44:XXX', // Pepe Unchained Mainnet - Native symbol: PEPU @@ -432,8 +432,8 @@ export class CodefiTokenPricesServiceV2 assets: EvmAssetAddressWithChain[]; currency: SupportedCurrency; }): Promise[]> { - const assetsWithIds: EvmAssetWithId[] = assets.map( - (asset) => { + const assetsWithIds: EvmAssetWithId[] = assets + .map((asset) => { const caipChainId = toCaipChainId( KnownCaipNamespace.Eip155, hexToNumber(asset.chainId).toString(), @@ -447,9 +447,9 @@ export class CodefiTokenPricesServiceV2 nativeAddress.toLowerCase() === asset.tokenAddress.toLowerCase() ? HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP[asset.chainId] : `${caipChainId}/erc20:${asset.tokenAddress.toLowerCase()}`, - }; - }, - ); + } as EvmAssetWithId; + }) + .filter((asset) => asset.assetId); const url = new URL(`${BASE_URL_V3}/spot-prices`); url.searchParams.append( From d356e04761bffa2d8785c31b35e12288be6ce394 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 18 Nov 2025 16:38:21 +0000 Subject: [PATCH 33/42] fix changelog --- packages/assets-controllers/CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 385b574f4a2..702a26d6122 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -7,8 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [89.0.0] - ### Changed - **BREAKING:** Update `spot-prices` endpoint to use Price API v3 ([7119](https://github.com/MetaMask/core/pull/7119)) @@ -16,6 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update `CodefiTokenPricesServiceV2` list of supported currencies - Update `TokenRatesController` to fetch prices by native currency instead of by chain - Remove legacy polling code and unused events from `TokenRatesController` + +## [89.0.0] + +### Changed + - **BREAKING:** Remove fallback to CryptoCompare on `CurrencyRatesController` and `TokenRatesController` ([#7167](https://github.com/MetaMask/core/pull/7167)) - Bump `@metamask/core-backend` from `^4.0.0` to `^4.1.0` From 92aa023e4999ffd398bc4dcd87374bec5aabb1d4 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 18 Nov 2025 16:44:04 +0000 Subject: [PATCH 34/42] changelog fix --- packages/assets-controllers/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 702a26d6122..c0284a8cf55 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **BREAKING:** Update `spot-prices` endpoint to use Price API v3 ([7119](https://github.com/MetaMask/core/pull/7119)) +- **BREAKING:** Update `spot-prices` endpoint to use Price API v3 ([#7119](https://github.com/MetaMask/core/pull/7119)) - Update `AbstractTokenPricesService.fetchTokenPrices` arguments and return type - Update `CodefiTokenPricesServiceV2` list of supported currencies - Update `TokenRatesController` to fetch prices by native currency instead of by chain From 7b66c7a9fd7edf4877c4fb94c7210b34b1b1b9c8 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Wed, 19 Nov 2025 09:30:56 +0000 Subject: [PATCH 35/42] update chains --- .../src/token-prices-service/codefi-v2.ts | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 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 b243a108afd..04617ad1169 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -228,51 +228,55 @@ const chainIdToNativeTokenAddress: Record = { export const getNativeTokenAddress = (chainId: Hex): Hex => chainIdToNativeTokenAddress[chainId] ?? ZERO_ADDRESS; +// Source: https://github.com/consensys-vertical-apps/va-mmcx-price-api/blob/main/src/constants/slip44.ts // We can only support PricesAPI V3 for EVM chains that have a CAIP-19 native asset mapping. export const HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP = { '0x1': 'eip155:1/slip44:60', // Ethereum Mainnet - Native symbol: ETH '0xa': 'eip155:10/slip44:60', // OP Mainnet - Native symbol: ETH '0x19': 'eip155:25/slip44:394', // Cronos Mainnet - Native symbol: CRO '0x38': 'eip155:56/slip44:714', // BNB Smart Chain Mainnet - Native symbol: BNB - '0x39': 'eip155:57/slip44:57', // Syscoin Mainnet - Native symbol: SYS - '0x42': 'eip155:66/slip44:996', // OKXChain Mainnet - Native symbol: OKT - '0x46': 'eip155:70/slip44:1170', // Hoo Smart Chain - Native symbol: HOO - '0x52': 'eip155:82/slip44:18000', // Meter Mainnet - Native symbol: MTR - '0x58': 'eip155:88/slip44:889', // TomoChain - Native symbol: TOMO '0x64': 'eip155:100/slip44:700', // Gnosis (formerly xDAI Chain) - Native symbol: xDAI - '0x6a': 'eip155:106/slip44:5655640', // Velas EVM Mainnet - Native symbol: VLX - '0x7a': 'eip155:122/slip44:XXX', // Fuse Mainnet - Native symbol: FUSE - '0x80': 'eip155:128/slip44:1010', // Huobi ECO Chain Mainnet - Native symbol: HT '0x89': 'eip155:137/slip44:966', // Polygon Mainnet - Native symbol: POL - '0x8f': 'eip155:143/slip44:268435779', // Monad Mainnet - Native symbol: MONAD '0x92': 'eip155:146/slip44:10007', // Sonic Mainnet - Native symbol: S '0xfa': 'eip155:250/slip44:1007', // Fantom Opera - Native symbol: FTM - '0x120': 'eip155:288/slip44:60', // Boba Network (Ethereum L2) - Native symbol: ETH - '0x141': 'eip155:321/slip44:641', // KCC Mainnet - Native symbol: KCS '0x144': 'eip155:324/slip44:60', // zkSync Era Mainnet (Ethereum L2) - Native symbol: ETH - '0x150': 'eip155:336/slip44:809', // Shiden - Native symbol: SDN - '0x169': 'eip155:361/slip44:589', // Theta Mainnet - Native symbol: TFUEL - '0x440': 'eip155:1088/slip44:60', // Metis Andromeda Mainnet (Ethereum L2) - Native symbol: ETH '0x44d': 'eip155:1101/slip44:60', // Polygon zkEVM mainnet - Native symbol: ETH '0x504': 'eip155:1284/slip44:1284', // Moonbeam - Native symbol: GLMR '0x505': 'eip155:1285/slip44:1285', // Moonriver - Native symbol: MOVR '0x531': 'eip155:1329/slip44:19000118', // Sei Mainnet - Native symbol: SEI - '0x1388': 'eip155:5000/slip44:XXX', // Mantle - Native symbol: MNT '0x2105': 'eip155:8453/slip44:60', // Base - Native symbol: ETH - '0x2710': 'eip155:10000/slip44:145', // Smart Bitcoin Cash - Native symbol: BCH '0xa4b1': 'eip155:42161/slip44:60', // Arbitrum One - Native symbol: ETH '0xa4ec': 'eip155:42220/slip44:52752', // Celo Mainnet - Native symbol: CELO - '0xa516': 'eip155:42262/slip44:474', // Oasis Emerald - Native symbol: ROSE '0xa86a': 'eip155:43114/slip44:9005', // Avalanche C-Chain - Native symbol: AVAX '0xe708': 'eip155:59144/slip44:60', // Linea Mainnet - Native symbol: ETH - '0x13c31': 'eip155:81457/slip44:60', // Blast Mainnet - Native symbol: ETH - '0x17dcd': 'eip155:97741/slip44:XXX', // Pepe Unchained Mainnet - Native symbol: PEPU - '0x28c58': 'eip155:167000/slip44:60', // Taiko Mainnet - Native symbol: ETH - '0x518af': 'eip155:333999/slip44:1997', // Polis Mainnet - Native symbol: POLIS '0x82750': 'eip155:534352/slip44:60', // Scroll Mainnet - Native symbol: ETH - '0x4e454152': 'eip155:1313161554/slip44:60', // Aurora Mainnet (Ethereum L2 on NEAR) - Native symbol: ETH + '0x4e454152': 'eip155:60/slip44:60', // Aurora Mainnet (Ethereum L2 on NEAR) - Native symbol: ETH '0x63564c40': 'eip155:1666600000/slip44:1023', // Harmony Mainnet Shard 0 - Native symbol: ONE -} as const; + '0x279f': 'eip155:143/slip44:268435779', // Monad Testnet - Native symbol: MON + '0x3e7': 'eip155:999/slip44:2457', // HyperEVM - Native symbol: ETH +}; + +// MISSING CHAINS +// '0x39': 'eip155:57/slip44:57', // Syscoin Mainnet - Native symbol: SYS +// '0x42': 'eip155:66/slip44:996', // OKXChain Mainnet - Native symbol: OKT +// '0x46': 'eip155:70/slip44:1170', // Hoo Smart Chain - Native symbol: HOO +// '0x52': 'eip155:82/slip44:18000', // Meter Mainnet - Native symbol: MTR +// '0x58': 'eip155:88/slip44:889', // TomoChain - Native symbol: TOMO +// '0x6a': 'eip155:106/slip44:5655640', // Velas EVM Mainnet - Native symbol: VLX +// '0x7a': 'eip155:122/slip44:XXX', // Fuse Mainnet - Native symbol: FUSE +// '0x80': 'eip155:128/slip44:1010', // Huobi ECO Chain Mainnet - Native symbol: HT +// '0x120': 'eip155:288/slip44:60', // Boba Network (Ethereum L2) - Native symbol: ETH +// '0x141': 'eip155:321/slip44:641', // KCC Mainnet - Native symbol: KCS +// '0x150': 'eip155:336/slip44:809', // Shiden - Native symbol: SDN +// '0x169': 'eip155:361/slip44:589', // Theta Mainnet - Native symbol: TFUEL +// '0x440': 'eip155:1088/slip44:60', // Metis Andromeda Mainnet (Ethereum L2) - Native symbol: ETH +// '0x1388': 'eip155:5000/slip44:XXX', // Mantle - Native symbol: MNT +// '0x2710': 'eip155:10000/slip44:145', // Smart Bitcoin Cash - Native symbol: BCH +// '0xa516': 'eip155:42262/slip44:474', // Oasis Emerald - Native symbol: ROSE +// '0x13c31': 'eip155:81457/slip44:60', // Blast Mainnet - Native symbol: ETH +// '0x17dcd': 'eip155:97741/slip44:XXX', // Pepe Unchained Mainnet - Native symbol: PEPU +// '0x28c58': 'eip155:167000/slip44:60', // Taiko Mainnet - Native symbol: ETH +// '0x518af': 'eip155:333999/slip44:1997', // Polis Mainnet - Native symbol: POLIS /** * A currency that can be supplied as the `vsCurrency` parameter to From 655d5735da0f5317d500b3771804be391553f901 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Wed, 19 Nov 2025 09:47:13 +0000 Subject: [PATCH 36/42] commented chains --- .../src/token-prices-service/codefi-v2.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 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 04617ad1169..4e7dc8f9380 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -256,28 +256,30 @@ export const HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP = { '0x3e7': 'eip155:999/slip44:2457', // HyperEVM - Native symbol: ETH }; -// MISSING CHAINS +// MISSING CHAINS WITH NATIVE ASSET PRICES IN V2 // '0x39': 'eip155:57/slip44:57', // Syscoin Mainnet - Native symbol: SYS -// '0x42': 'eip155:66/slip44:996', // OKXChain Mainnet - Native symbol: OKT -// '0x46': 'eip155:70/slip44:1170', // Hoo Smart Chain - Native symbol: HOO // '0x52': 'eip155:82/slip44:18000', // Meter Mainnet - Native symbol: MTR // '0x58': 'eip155:88/slip44:889', // TomoChain - Native symbol: TOMO // '0x6a': 'eip155:106/slip44:5655640', // Velas EVM Mainnet - Native symbol: VLX -// '0x7a': 'eip155:122/slip44:XXX', // Fuse Mainnet - Native symbol: FUSE // '0x80': 'eip155:128/slip44:1010', // Huobi ECO Chain Mainnet - Native symbol: HT -// '0x120': 'eip155:288/slip44:60', // Boba Network (Ethereum L2) - Native symbol: ETH // '0x141': 'eip155:321/slip44:641', // KCC Mainnet - Native symbol: KCS -// '0x150': 'eip155:336/slip44:809', // Shiden - Native symbol: SDN // '0x169': 'eip155:361/slip44:589', // Theta Mainnet - Native symbol: TFUEL -// '0x440': 'eip155:1088/slip44:60', // Metis Andromeda Mainnet (Ethereum L2) - Native symbol: ETH -// '0x1388': 'eip155:5000/slip44:XXX', // Mantle - Native symbol: MNT // '0x2710': 'eip155:10000/slip44:145', // Smart Bitcoin Cash - Native symbol: BCH // '0xa516': 'eip155:42262/slip44:474', // Oasis Emerald - Native symbol: ROSE // '0x13c31': 'eip155:81457/slip44:60', // Blast Mainnet - Native symbol: ETH // '0x17dcd': 'eip155:97741/slip44:XXX', // Pepe Unchained Mainnet - Native symbol: PEPU -// '0x28c58': 'eip155:167000/slip44:60', // Taiko Mainnet - Native symbol: ETH // '0x518af': 'eip155:333999/slip44:1997', // Polis Mainnet - Native symbol: POLIS +// MISSING CHAINS WITH NO NATIVE ASSET PRICES IN V2 +// '0x42': 'eip155:66/slip44:996', // OKXChain Mainnet - Native symbol: OKT +// '0x46': 'eip155:70/slip44:1170', // Hoo Smart Chain - Native symbol: HOO +// '0x7a': 'eip155:122/slip44:XXX', // Fuse Mainnet - Native symbol: FUSE +// '0x120': 'eip155:288/slip44:60', // Boba Network (Ethereum L2) - Native symbol: ETH +// '0x150': 'eip155:336/slip44:809', // Shiden - Native symbol: SDN +// '0x440': 'eip155:1088/slip44:60', // Metis Andromeda Mainnet (Ethereum L2) - Native symbol: ETH +// '0x1388': 'eip155:5000/slip44:XXX', // Mantle - Native symbol: MNT +// '0x28c58': 'eip155:167000/slip44:60', // Taiko Mainnet - Native symbol: ETH + /** * A currency that can be supplied as the `vsCurrency` parameter to * the `/spot-prices` endpoint. Covers both uppercase and lowercase versions. From ad447d978717daf1b35d0877f5e7452c15c86a48 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Wed, 19 Nov 2025 12:43:27 +0000 Subject: [PATCH 37/42] add fallback for v2 --- .../src/CurrencyRateController.test.ts | 1 - ...TokenSearchDiscoveryDataController.test.ts | 6 +- .../assets-controllers/src/assetsUtil.test.ts | 3 +- .../abstract-token-prices-service.ts | 3 +- .../src/token-prices-service/codefi-v2.ts | 143 +++++++++++++++--- 5 files changed, 123 insertions(+), 33 deletions(-) diff --git a/packages/assets-controllers/src/CurrencyRateController.test.ts b/packages/assets-controllers/src/CurrencyRateController.test.ts index 133830487a5..4b5e75c67ed 100644 --- a/packages/assets-controllers/src/CurrencyRateController.test.ts +++ b/packages/assets-controllers/src/CurrencyRateController.test.ts @@ -1596,7 +1596,6 @@ describe('CurrencyRateController', () => { currency: 'usd', tokenAddress: '0x0000000000000000000000000000000000001010', chainId: '0x89', - assetId: 'xx:yy/aa:bb', price: 0.85, pricePercentChange1d: 0, priceChange1d: 0, diff --git a/packages/assets-controllers/src/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.test.ts b/packages/assets-controllers/src/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.test.ts index 1ab0ad94796..d1a2882d1f1 100644 --- a/packages/assets-controllers/src/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.test.ts +++ b/packages/assets-controllers/src/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.test.ts @@ -7,7 +7,7 @@ import { type MessengerEvents, type MockAnyNamespace, } from '@metamask/messenger'; -import { KnownCaipNamespace, type Hex } from '@metamask/utils'; +import { type Hex } from '@metamask/utils'; import assert from 'assert'; import { useFakeTimers } from 'sinon'; @@ -83,7 +83,6 @@ function buildFoundTokenDisplayData( currency: 'USD', tokenAddress: tokenAddress as Hex, chainId: '0x1', - assetId: `${KnownCaipNamespace.Eip155}:1/erc20:${tokenAddress.toLowerCase()}`, allTimeHigh: 20, allTimeLow: 5, circulatingSupply: 1000000, @@ -498,7 +497,6 @@ describe('TokenSearchDiscoveryDataController', () => { currency: 'USD', tokenAddress: tokenAddress as Hex, chainId: '0x1', - assetId: `${KnownCaipNamespace.Eip155}:1/erc20:${tokenAddress.toLowerCase()}`, allTimeHigh: 20, allTimeLow: 5, circulatingSupply: 1000000, @@ -651,7 +649,6 @@ describe('TokenSearchDiscoveryDataController', () => { > = { tokenAddress: tokenAddress as Hex, chainId: '0x1', - assetId: `${KnownCaipNamespace.Eip155}:1/erc20:${tokenAddress.toLowerCase()}`, allTimeHigh: 20, allTimeLow: 5, circulatingSupply: 1000000, @@ -721,7 +718,6 @@ describe('TokenSearchDiscoveryDataController', () => { currency: 'USD', tokenAddress: tokenAddress as Hex, chainId: '0x1', - assetId: `${KnownCaipNamespace.Eip155}:1/erc20:${tokenAddress.toLowerCase()}`, allTimeHigh: 20, allTimeLow: 5, circulatingSupply: 1000000, diff --git a/packages/assets-controllers/src/assetsUtil.test.ts b/packages/assets-controllers/src/assetsUtil.test.ts index 2fc34104133..fc6e5d7c42c 100644 --- a/packages/assets-controllers/src/assetsUtil.test.ts +++ b/packages/assets-controllers/src/assetsUtil.test.ts @@ -5,7 +5,7 @@ import { toHex, toChecksumHexAddress, } from '@metamask/controller-utils'; -import { add0x, KnownCaipNamespace, type Hex } from '@metamask/utils'; +import { add0x, type Hex } from '@metamask/utils'; import * as assetsUtil from './assetsUtil'; import { TOKEN_PRICES_BATCH_SIZE } from './assetsUtil'; @@ -629,7 +629,6 @@ describe('assetsUtil', () => { { tokenAddress: testTokenAddress, chainId: testChainId, - assetId: `${KnownCaipNamespace.Eip155}:${testChainId}/erc20:${testTokenAddress}`, currency: testNativeCurrency, allTimeHigh: 4000, allTimeLow: 900, 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 195bd3a6133..22577107fa3 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 @@ -34,7 +34,8 @@ export type EvmAssetWithId = export type EvmAssetWithMarketData< ChainId extends Hex = Hex, Currency extends string = string, -> = EvmAssetWithId & MarketDataDetails & { currency: Currency }; +> = EvmAssetAddressWithChain & + MarketDataDetails & { currency: Currency }; /** * An ideal token prices service. All implementations must confirm to this 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 4e7dc8f9380..2311fc1eae6 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -230,45 +230,43 @@ export const getNativeTokenAddress = (chainId: Hex): Hex => // Source: https://github.com/consensys-vertical-apps/va-mmcx-price-api/blob/main/src/constants/slip44.ts // We can only support PricesAPI V3 for EVM chains that have a CAIP-19 native asset mapping. -export const HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP = { +export const SPOT_PRICES_SUPPORT_INFO = { '0x1': 'eip155:1/slip44:60', // Ethereum Mainnet - Native symbol: ETH '0xa': 'eip155:10/slip44:60', // OP Mainnet - Native symbol: ETH '0x19': 'eip155:25/slip44:394', // Cronos Mainnet - Native symbol: CRO '0x38': 'eip155:56/slip44:714', // BNB Smart Chain Mainnet - Native symbol: BNB + '0x39': null, // 'eip155:57/slip44:57', // Syscoin Mainnet - Native symbol: SYS + '0x52': null, // 'eip155:82/slip44:18000', // Meter Mainnet - Native symbol: MTR + '0x58': null, // 'eip155:88/slip44:889', // TomoChain - Native symbol: TOMO '0x64': 'eip155:100/slip44:700', // Gnosis (formerly xDAI Chain) - Native symbol: xDAI + '0x6a': null, // 'eip155:106/slip44:5655640', // Velas EVM Mainnet - Native symbol: VLX + '0x80': null, // 'eip155:128/slip44:1010', // Huobi ECO Chain Mainnet - Native symbol: HT '0x89': 'eip155:137/slip44:966', // Polygon Mainnet - Native symbol: POL '0x92': 'eip155:146/slip44:10007', // Sonic Mainnet - Native symbol: S '0xfa': 'eip155:250/slip44:1007', // Fantom Opera - Native symbol: FTM + '0x141': null, // 'eip155:321/slip44:641', // KCC Mainnet - Native symbol: KCS '0x144': 'eip155:324/slip44:60', // zkSync Era Mainnet (Ethereum L2) - Native symbol: ETH + '0x169': null, // 'eip155:361/slip44:589', // Theta Mainnet - Native symbol: TFUEL '0x44d': 'eip155:1101/slip44:60', // Polygon zkEVM mainnet - Native symbol: ETH '0x504': 'eip155:1284/slip44:1284', // Moonbeam - Native symbol: GLMR '0x505': 'eip155:1285/slip44:1285', // Moonriver - Native symbol: MOVR '0x531': 'eip155:1329/slip44:19000118', // Sei Mainnet - Native symbol: SEI '0x2105': 'eip155:8453/slip44:60', // Base - Native symbol: ETH + '0x2710': null, // 'eip155:10000/slip44:145', // Smart Bitcoin Cash - Native symbol: BCH '0xa4b1': 'eip155:42161/slip44:60', // Arbitrum One - Native symbol: ETH '0xa4ec': 'eip155:42220/slip44:52752', // Celo Mainnet - Native symbol: CELO + '0xa516': null, // 'eip155:42262/slip44:474', // Oasis Emerald - Native symbol: ROSE '0xa86a': 'eip155:43114/slip44:9005', // Avalanche C-Chain - Native symbol: AVAX '0xe708': 'eip155:59144/slip44:60', // Linea Mainnet - Native symbol: ETH + '0x13c31': null, // 'eip155:81457/slip44:60', // Blast Mainnet - Native symbol: ETH + '0x17dcd': null, // 'eip155:97741/slip44:XXX', // Pepe Unchained Mainnet - Native symbol: PEPU + '0x518af': null, // 'eip155:333999/slip44:1997', // Polis Mainnet - Native symbol: POLIS '0x82750': 'eip155:534352/slip44:60', // Scroll Mainnet - Native symbol: ETH '0x4e454152': 'eip155:60/slip44:60', // Aurora Mainnet (Ethereum L2 on NEAR) - Native symbol: ETH '0x63564c40': 'eip155:1666600000/slip44:1023', // Harmony Mainnet Shard 0 - Native symbol: ONE '0x279f': 'eip155:143/slip44:268435779', // Monad Testnet - Native symbol: MON '0x3e7': 'eip155:999/slip44:2457', // HyperEVM - Native symbol: ETH -}; - -// MISSING CHAINS WITH NATIVE ASSET PRICES IN V2 -// '0x39': 'eip155:57/slip44:57', // Syscoin Mainnet - Native symbol: SYS -// '0x52': 'eip155:82/slip44:18000', // Meter Mainnet - Native symbol: MTR -// '0x58': 'eip155:88/slip44:889', // TomoChain - Native symbol: TOMO -// '0x6a': 'eip155:106/slip44:5655640', // Velas EVM Mainnet - Native symbol: VLX -// '0x80': 'eip155:128/slip44:1010', // Huobi ECO Chain Mainnet - Native symbol: HT -// '0x141': 'eip155:321/slip44:641', // KCC Mainnet - Native symbol: KCS -// '0x169': 'eip155:361/slip44:589', // Theta Mainnet - Native symbol: TFUEL -// '0x2710': 'eip155:10000/slip44:145', // Smart Bitcoin Cash - Native symbol: BCH -// '0xa516': 'eip155:42262/slip44:474', // Oasis Emerald - Native symbol: ROSE -// '0x13c31': 'eip155:81457/slip44:60', // Blast Mainnet - Native symbol: ETH -// '0x17dcd': 'eip155:97741/slip44:XXX', // Pepe Unchained Mainnet - Native symbol: PEPU -// '0x518af': 'eip155:333999/slip44:1997', // Polis Mainnet - Native symbol: POLIS +} as const; // MISSING CHAINS WITH NO NATIVE ASSET PRICES IN V2 // '0x42': 'eip155:66/slip44:996', // OKXChain Mainnet - Native symbol: OKT @@ -296,8 +294,8 @@ type SupportedCurrency = * @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 = Object.keys( - HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP, -) as (keyof typeof HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP)[]; + SPOT_PRICES_SUPPORT_INFO, +) as (keyof typeof SPOT_PRICES_SUPPORT_INFO)[]; /** * A chain ID that can be supplied in the URL for the `/spot-prices` endpoint, @@ -307,10 +305,20 @@ export const SUPPORTED_CHAIN_IDS = Object.keys( type SupportedChainId = (typeof SUPPORTED_CHAIN_IDS)[number]; /** - * All requests to V2 of the Price API start with this. + * The list of chain IDs that are supported by V3 of the Codefi Price API. + * Only includes chain IDs from SPOT_PRICES_SUPPORT_INFO that have a non-null CAIP-19 value. */ +const SUPPORTED_CHAIN_IDS_V3 = Object.keys(SPOT_PRICES_SUPPORT_INFO).filter( + (chainId) => + SPOT_PRICES_SUPPORT_INFO[ + chainId as keyof typeof SPOT_PRICES_SUPPORT_INFO + ] !== null, +); + const BASE_URL_V1 = 'https://price.api.cx.metamask.io/v1'; +const BASE_URL_V2 = 'https://price.api.cx.metamask.io/v2'; + const BASE_URL_V3 = 'https://price.api.cx.metamask.io/v3'; /** @@ -438,7 +446,19 @@ export class CodefiTokenPricesServiceV2 assets: EvmAssetAddressWithChain[]; currency: SupportedCurrency; }): Promise[]> { + const v3Assets = await this.#fetchTokenPricesV3(assets, currency); + const v2Assets = await this.#fetchTokenPricesV2(assets, currency); + + return [...v3Assets, ...v2Assets]; + } + + async #fetchTokenPricesV3( + assets: EvmAssetAddressWithChain[], + currency: SupportedCurrency, + ): Promise[]> { const assetsWithIds: EvmAssetWithId[] = assets + // Filter out assets that are not supported by V3 of the Price API. + .filter((asset) => SUPPORTED_CHAIN_IDS_V3.includes(asset.chainId)) .map((asset) => { const caipChainId = toCaipChainId( KnownCaipNamespace.Eip155, @@ -449,14 +469,18 @@ export class CodefiTokenPricesServiceV2 return { ...asset, - assetId: - nativeAddress.toLowerCase() === asset.tokenAddress.toLowerCase() - ? HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP[asset.chainId] - : `${caipChainId}/erc20:${asset.tokenAddress.toLowerCase()}`, - } as EvmAssetWithId; + assetId: (nativeAddress.toLowerCase() === + asset.tokenAddress.toLowerCase() + ? SPOT_PRICES_SUPPORT_INFO[asset.chainId] + : `${caipChainId}/erc20:${asset.tokenAddress.toLowerCase()}`) as CaipAssetType, + }; }) .filter((asset) => asset.assetId); + if (assetsWithIds.length === 0) { + return []; + } + const url = new URL(`${BASE_URL_V3}/spot-prices`); url.searchParams.append( 'assetIds', @@ -491,6 +515,77 @@ export class CodefiTokenPricesServiceV2 .filter((entry): entry is NonNullable => Boolean(entry)); } + async #fetchTokenPricesV2( + assets: EvmAssetAddressWithChain[], + currency: SupportedCurrency, + ): Promise[]> { + const v2SupportedAssets = assets.filter( + (asset) => !SUPPORTED_CHAIN_IDS_V3.includes(asset.chainId), + ); + + console.log('DEBUG LEGACY ASSETS', { v2SupportedAssets, currency }); + const assetsByChainId: Record = + v2SupportedAssets.reduce( + (acc, { chainId, tokenAddress }) => { + (acc[chainId] ??= []).push(tokenAddress); + return acc; + }, + {} as Record, + ); + + const promises = Object.entries(assetsByChainId).map( + async ([chainId, tokenAddresses]) => { + if (tokenAddresses.length === 0) { + return []; + } + + const url = new URL(`${BASE_URL_V2}/chains/${chainId}/spot-prices`); + url.searchParams.append('tokenAddresses', tokenAddresses.join(',')); + url.searchParams.append('vsCurrency', currency); + url.searchParams.append('includeMarketData', 'true'); + + const addressCryptoDataMap: { + [tokenAddress: string]: Omit< + MarketDataDetails, + 'currency' | 'tokenAddress' + >; + } = await this.#policy.execute(() => + handleFetch(url, { headers: { 'Cache-Control': 'no-cache' } }), + ); + + console.log('DEBUG LEGACY CHAIN', { + chainId, + tokenAddresses, + }); + + return tokenAddresses + .map((tokenAddress) => { + const marketData = addressCryptoDataMap[tokenAddress.toLowerCase()]; + + if (!marketData) { + return undefined; + } + + return { + ...marketData, + tokenAddress, + chainId: chainId as SupportedChainId, + currency, + }; + }) + .filter((entry): entry is NonNullable => + Boolean(entry), + ); + }, + ); + + return await Promise.allSettled(promises).then((results) => + results.flatMap((result) => + result.status === 'fulfilled' ? result.value : [], + ), + ); + } + /** * Retrieves exchange rates in the given base currency. * From e879757d590693885484159ddf8a272023136f2e Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Wed, 19 Nov 2025 12:56:49 +0000 Subject: [PATCH 38/42] remove console.log --- .../src/token-prices-service/codefi-v2.ts | 6 ------ 1 file changed, 6 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 2311fc1eae6..4242841a2c0 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -523,7 +523,6 @@ export class CodefiTokenPricesServiceV2 (asset) => !SUPPORTED_CHAIN_IDS_V3.includes(asset.chainId), ); - console.log('DEBUG LEGACY ASSETS', { v2SupportedAssets, currency }); const assetsByChainId: Record = v2SupportedAssets.reduce( (acc, { chainId, tokenAddress }) => { @@ -553,11 +552,6 @@ export class CodefiTokenPricesServiceV2 handleFetch(url, { headers: { 'Cache-Control': 'no-cache' } }), ); - console.log('DEBUG LEGACY CHAIN', { - chainId, - tokenAddresses, - }); - return tokenAddresses .map((tokenAddress) => { const marketData = addressCryptoDataMap[tokenAddress.toLowerCase()]; From c2948363e7f76165a9aa34885be1f00e2ef2c6bc Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Wed, 19 Nov 2025 13:17:56 +0000 Subject: [PATCH 39/42] coverage --- .../token-prices-service/codefi-v2.test.ts | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) 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 e26a8a77985..1de56fefd81 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 @@ -236,6 +236,124 @@ describe('CodefiTokenPricesServiceV2', () => { }); describe('fetchTokenPrices', () => { + it('uses the /v2/chains/{chainId}/spot-prices endpoint to gather prices forn chains not supported by v3', async () => { + nock('https://price.api.cx.metamask.io') + .get('/v2/chains/0x39/spot-prices') + .query({ + tokenAddresses: ['0xAAA', '0xBBB'].join(','), + vsCurrency: 'ETH', + includeMarketData: 'true', + }) + .reply(200, { + '0xaaa': { + price: 148.17205755299946, + currency: 'ETH', + pricePercentChange1d: 1, + priceChange1d: 1, + marketCap: 117219.99428314982, + allTimeHigh: 0.00060467892389492, + allTimeLow: 0.00002303954000865728, + totalVolume: 5155.094053542448, + high1d: 0.00008020715848194385, + low1d: 0.00007792083564549064, + circulatingSupply: 1494269733.9526057, + dilutedMarketCap: 117669.5125951733, + marketCapPercentChange1d: 0.76671, + pricePercentChange1h: -1.0736342953259423, + pricePercentChange7d: -7.351582573655089, + pricePercentChange14d: -1.0799098946709822, + pricePercentChange30d: -25.776321124365992, + pricePercentChange200d: 46.091571238599165, + pricePercentChange1y: -2.2992517267242754, + }, + '0xbbb': { + price: 33689.98134554716, + currency: 'ETH', + pricePercentChange1d: 1, + priceChange1d: 1, + marketCap: 117219.99428314982, + allTimeHigh: 0.00060467892389492, + allTimeLow: 0.00002303954000865728, + totalVolume: 5155.094053542448, + high1d: 0.00008020715848194385, + low1d: 0.00007792083564549064, + circulatingSupply: 1494269733.9526057, + dilutedMarketCap: 117669.5125951733, + marketCapPercentChange1d: 0.76671, + pricePercentChange1h: -1.0736342953259423, + pricePercentChange7d: -7.351582573655089, + pricePercentChange14d: -1.0799098946709822, + pricePercentChange30d: -25.776321124365992, + pricePercentChange200d: 46.091571238599165, + pricePercentChange1y: -2.2992517267242754, + }, + }); + + const marketDataTokensByAddress = + await new CodefiTokenPricesServiceV2().fetchTokenPrices({ + assets: [ + { + chainId: '0x39', + tokenAddress: '0xAAA', + }, + { + chainId: '0x39', + tokenAddress: '0xBBB', + }, + ], + currency: 'ETH', + }); + + expect(marketDataTokensByAddress).toStrictEqual([ + { + allTimeHigh: 0.00060467892389492, + allTimeLow: 0.00002303954000865728, + chainId: '0x39', + circulatingSupply: 1494269733.9526057, + currency: 'ETH', + dilutedMarketCap: 117669.5125951733, + high1d: 0.00008020715848194385, + low1d: 0.00007792083564549064, + marketCap: 117219.99428314982, + marketCapPercentChange1d: 0.76671, + price: 148.17205755299946, + priceChange1d: 1, + pricePercentChange14d: -1.0799098946709822, + pricePercentChange1d: 1, + pricePercentChange1h: -1.0736342953259423, + pricePercentChange1y: -2.2992517267242754, + pricePercentChange200d: 46.091571238599165, + pricePercentChange30d: -25.776321124365992, + pricePercentChange7d: -7.351582573655089, + tokenAddress: '0xAAA', + totalVolume: 5155.094053542448, + }, + { + allTimeHigh: 0.00060467892389492, + allTimeLow: 0.00002303954000865728, + chainId: '0x39', + circulatingSupply: 1494269733.9526057, + currency: 'ETH', + dilutedMarketCap: 117669.5125951733, + high1d: 0.00008020715848194385, + low1d: 0.00007792083564549064, + marketCap: 117219.99428314982, + marketCapPercentChange1d: 0.76671, + price: 33689.98134554716, + priceChange1d: 1, + pricePercentChange14d: -1.0799098946709822, + pricePercentChange1d: 1, + pricePercentChange1h: -1.0736342953259423, + pricePercentChange1y: -2.2992517267242754, + pricePercentChange200d: 46.091571238599165, + pricePercentChange30d: -25.776321124365992, + pricePercentChange7d: -7.351582573655089, + tokenAddress: '0xBBB', + totalVolume: 5155.094053542448, + }, + ]); + }); + it('uses the /spot-prices endpoint of the Codefi Price API to gather prices for the given tokens', async () => { nock('https://price.api.cx.metamask.io') .get('/v3/spot-prices') From 197e72239a4bb893b89ad1e1ebf776b55549c6a7 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Wed, 19 Nov 2025 19:24:55 +0000 Subject: [PATCH 40/42] update list --- .../src/token-prices-service/codefi-v2.ts | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 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 4242841a2c0..2288bb08731 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -214,7 +214,9 @@ export const ZERO_ADDRESS: Hex = * Only for chains whose native tokens have a specific address. */ const chainIdToNativeTokenAddress: Record = { - '0x89': '0x0000000000000000000000000000000000001010', + '0x89': '0x0000000000000000000000000000000000001010', // Polygon + '0x440': '0xdeaddeaddeaddeaddeaddeaddeaddeaddead0000', // Metis Andromeda + '0x1388': '0xdeaddeaddeaddeaddeaddeaddeaddeaddead0000', // Mantle }; /** @@ -235,31 +237,33 @@ export const SPOT_PRICES_SUPPORT_INFO = { '0xa': 'eip155:10/slip44:60', // OP Mainnet - Native symbol: ETH '0x19': 'eip155:25/slip44:394', // Cronos Mainnet - Native symbol: CRO '0x38': 'eip155:56/slip44:714', // BNB Smart Chain Mainnet - Native symbol: BNB - '0x39': null, // 'eip155:57/slip44:57', // Syscoin Mainnet - Native symbol: SYS + '0x39': 'eip155:57/erc20:0x0000000000000000000000000000000000000000', // 'eip155:57/slip44:57', // Syscoin Mainnet - Native symbol: SYS '0x52': null, // 'eip155:82/slip44:18000', // Meter Mainnet - Native symbol: MTR - '0x58': null, // 'eip155:88/slip44:889', // TomoChain - Native symbol: TOMO + '0x58': 'eip155:88/erc20:0x0000000000000000000000000000000000000000', // 'eip155:88/slip44:889', // TomoChain - Native symbol: TOMO '0x64': 'eip155:100/slip44:700', // Gnosis (formerly xDAI Chain) - Native symbol: xDAI - '0x6a': null, // 'eip155:106/slip44:5655640', // Velas EVM Mainnet - Native symbol: VLX - '0x80': null, // 'eip155:128/slip44:1010', // Huobi ECO Chain Mainnet - Native symbol: HT + '0x6a': 'eip155:106/erc20:0x0000000000000000000000000000000000000000', // 'eip155:106/slip44:5655640', // Velas EVM Mainnet - Native symbol: VLX + '0x80': 'eip155:128/erc20:0x0000000000000000000000000000000000000000', // 'eip155:128/slip44:1010', // Huobi ECO Chain Mainnet - Native symbol: HT '0x89': 'eip155:137/slip44:966', // Polygon Mainnet - Native symbol: POL '0x92': 'eip155:146/slip44:10007', // Sonic Mainnet - Native symbol: S '0xfa': 'eip155:250/slip44:1007', // Fantom Opera - Native symbol: FTM - '0x141': null, // 'eip155:321/slip44:641', // KCC Mainnet - Native symbol: KCS + '0x141': 'eip155:321/erc20:0x0000000000000000000000000000000000000000', // 'eip155:321/slip44:641', // KCC Mainnet - Native symbol: KCS '0x144': 'eip155:324/slip44:60', // zkSync Era Mainnet (Ethereum L2) - Native symbol: ETH - '0x169': null, // 'eip155:361/slip44:589', // Theta Mainnet - Native symbol: TFUEL + '0x169': 'eip155:361/erc20:0x0000000000000000000000000000000000000000', // 'eip155:361/slip44:589', // Theta Mainnet - Native symbol: TFUEL + '0x440': 'eip155:1088/erc20:0xdeaddeaddeaddeaddeaddeaddeaddeaddead0000', // 'eip155:1088/slip44:XXX', // Metis Andromeda Mainnet (Ethereum L2) - Native symbol: METIS '0x44d': 'eip155:1101/slip44:60', // Polygon zkEVM mainnet - Native symbol: ETH '0x504': 'eip155:1284/slip44:1284', // Moonbeam - Native symbol: GLMR '0x505': 'eip155:1285/slip44:1285', // Moonriver - Native symbol: MOVR '0x531': 'eip155:1329/slip44:19000118', // Sei Mainnet - Native symbol: SEI + '0x1388': 'eip155:5000/erc20:0xdeaddeaddeaddeaddeaddeaddeaddeaddead0000', // 'eip155:5000/slip44:XXX', // Mantle - Native symbol: MNT '0x2105': 'eip155:8453/slip44:60', // Base - Native symbol: ETH - '0x2710': null, // 'eip155:10000/slip44:145', // Smart Bitcoin Cash - Native symbol: BCH + '0x2710': 'eip155:10000/erc20:0x0000000000000000000000000000000000000000', // 'eip155:10000/slip44:145', // Smart Bitcoin Cash - Native symbol: BCH '0xa4b1': 'eip155:42161/slip44:60', // Arbitrum One - Native symbol: ETH '0xa4ec': 'eip155:42220/slip44:52752', // Celo Mainnet - Native symbol: CELO - '0xa516': null, // 'eip155:42262/slip44:474', // Oasis Emerald - Native symbol: ROSE + '0xa516': 'eip155:42262/erc20:0x0000000000000000000000000000000000000000', // 'eip155:42262/slip44:474', // Oasis Emerald - Native symbol: ROSE '0xa86a': 'eip155:43114/slip44:9005', // Avalanche C-Chain - Native symbol: AVAX '0xe708': 'eip155:59144/slip44:60', // Linea Mainnet - Native symbol: ETH - '0x13c31': null, // 'eip155:81457/slip44:60', // Blast Mainnet - Native symbol: ETH - '0x17dcd': null, // 'eip155:97741/slip44:XXX', // Pepe Unchained Mainnet - Native symbol: PEPU + '0x13c31': 'eip155:81457/erc20:0x0000000000000000000000000000000000000000', // 'eip155:81457/slip44:60', // Blast Mainnet - Native symbol: ETH + '0x17dcd': 'eip155:97741/erc20:0x0000000000000000000000000000000000000000', // 'eip155:97741/slip44:XXX', // Pepe Unchained Mainnet - Native symbol: PEPU '0x518af': null, // 'eip155:333999/slip44:1997', // Polis Mainnet - Native symbol: POLIS '0x82750': 'eip155:534352/slip44:60', // Scroll Mainnet - Native symbol: ETH '0x4e454152': 'eip155:60/slip44:60', // Aurora Mainnet (Ethereum L2 on NEAR) - Native symbol: ETH @@ -274,8 +278,6 @@ export const SPOT_PRICES_SUPPORT_INFO = { // '0x7a': 'eip155:122/slip44:XXX', // Fuse Mainnet - Native symbol: FUSE // '0x120': 'eip155:288/slip44:60', // Boba Network (Ethereum L2) - Native symbol: ETH // '0x150': 'eip155:336/slip44:809', // Shiden - Native symbol: SDN -// '0x440': 'eip155:1088/slip44:60', // Metis Andromeda Mainnet (Ethereum L2) - Native symbol: ETH -// '0x1388': 'eip155:5000/slip44:XXX', // Mantle - Native symbol: MNT // '0x28c58': 'eip155:167000/slip44:60', // Taiko Mainnet - Native symbol: ETH /** From f83c2058f365aa128b4990d30a27ab5a56a1ac79 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Wed, 19 Nov 2025 19:35:46 +0000 Subject: [PATCH 41/42] fix test --- .../src/token-prices-service/codefi-v2.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 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 1de56fefd81..44da38b1b62 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 @@ -238,7 +238,7 @@ describe('CodefiTokenPricesServiceV2', () => { describe('fetchTokenPrices', () => { it('uses the /v2/chains/{chainId}/spot-prices endpoint to gather prices forn chains not supported by v3', async () => { nock('https://price.api.cx.metamask.io') - .get('/v2/chains/0x39/spot-prices') + .get('/v2/chains/0x52/spot-prices') .query({ tokenAddresses: ['0xAAA', '0xBBB'].join(','), vsCurrency: 'ETH', @@ -293,11 +293,11 @@ describe('CodefiTokenPricesServiceV2', () => { await new CodefiTokenPricesServiceV2().fetchTokenPrices({ assets: [ { - chainId: '0x39', + chainId: '0x52', tokenAddress: '0xAAA', }, { - chainId: '0x39', + chainId: '0x52', tokenAddress: '0xBBB', }, ], @@ -308,7 +308,7 @@ describe('CodefiTokenPricesServiceV2', () => { { allTimeHigh: 0.00060467892389492, allTimeLow: 0.00002303954000865728, - chainId: '0x39', + chainId: '0x52', circulatingSupply: 1494269733.9526057, currency: 'ETH', dilutedMarketCap: 117669.5125951733, @@ -331,7 +331,7 @@ describe('CodefiTokenPricesServiceV2', () => { { allTimeHigh: 0.00060467892389492, allTimeLow: 0.00002303954000865728, - chainId: '0x39', + chainId: '0x52', circulatingSupply: 1494269733.9526057, currency: 'ETH', dilutedMarketCap: 117669.5125951733, From 19323adaeda786e4bb2a992b80afd65843303f88 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Wed, 19 Nov 2025 20:12:17 +0000 Subject: [PATCH 42/42] put chains in order --- .../assets-controllers/src/token-prices-service/codefi-v2.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 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 2288bb08731..99140b2bf63 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -244,11 +244,13 @@ export const SPOT_PRICES_SUPPORT_INFO = { '0x6a': 'eip155:106/erc20:0x0000000000000000000000000000000000000000', // 'eip155:106/slip44:5655640', // Velas EVM Mainnet - Native symbol: VLX '0x80': 'eip155:128/erc20:0x0000000000000000000000000000000000000000', // 'eip155:128/slip44:1010', // Huobi ECO Chain Mainnet - Native symbol: HT '0x89': 'eip155:137/slip44:966', // Polygon Mainnet - Native symbol: POL + '0x8f': null, // 'eip155:143/slip44:268435779', // Monad Mainnet - Native symbol: MON '0x92': 'eip155:146/slip44:10007', // Sonic Mainnet - Native symbol: S '0xfa': 'eip155:250/slip44:1007', // Fantom Opera - Native symbol: FTM '0x141': 'eip155:321/erc20:0x0000000000000000000000000000000000000000', // 'eip155:321/slip44:641', // KCC Mainnet - Native symbol: KCS '0x144': 'eip155:324/slip44:60', // zkSync Era Mainnet (Ethereum L2) - Native symbol: ETH '0x169': 'eip155:361/erc20:0x0000000000000000000000000000000000000000', // 'eip155:361/slip44:589', // Theta Mainnet - Native symbol: TFUEL + '0x3e7': 'eip155:999/slip44:2457', // HyperEVM - Native symbol: ETH '0x440': 'eip155:1088/erc20:0xdeaddeaddeaddeaddeaddeaddeaddeaddead0000', // 'eip155:1088/slip44:XXX', // Metis Andromeda Mainnet (Ethereum L2) - Native symbol: METIS '0x44d': 'eip155:1101/slip44:60', // Polygon zkEVM mainnet - Native symbol: ETH '0x504': 'eip155:1284/slip44:1284', // Moonbeam - Native symbol: GLMR @@ -268,8 +270,6 @@ export const SPOT_PRICES_SUPPORT_INFO = { '0x82750': 'eip155:534352/slip44:60', // Scroll Mainnet - Native symbol: ETH '0x4e454152': 'eip155:60/slip44:60', // Aurora Mainnet (Ethereum L2 on NEAR) - Native symbol: ETH '0x63564c40': 'eip155:1666600000/slip44:1023', // Harmony Mainnet Shard 0 - Native symbol: ONE - '0x279f': 'eip155:143/slip44:268435779', // Monad Testnet - Native symbol: MON - '0x3e7': 'eip155:999/slip44:2457', // HyperEVM - Native symbol: ETH } as const; // MISSING CHAINS WITH NO NATIVE ASSET PRICES IN V2