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 {}; diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 8163ab3be20..3024190b6ed 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, @@ -92,6 +95,11 @@ export type MarketDataDetails = { */ export type ContractMarketData = Record; +type ChainIdAndNativeCurrency = { + chainId: Hex; + nativeCurrency: string; +}; + enum PollState { Active = 'Active', Inactive = 'Inactive', @@ -250,6 +258,8 @@ export class TokenRatesController extends StaticIntervalPollingController string; + #allTokens: TokensControllerState['allTokens']; #allDetectedTokens: TokensControllerState['allDetectedTokens']; @@ -263,6 +273,7 @@ export class TokenRatesController extends StaticIntervalPollingController; + getSelectedCurrency: () => string; }) { super({ name: controllerName, @@ -288,6 +301,7 @@ 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', + ); + + 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]; + + this.#getTokenAddresses(chainId).forEach((tokenAddress) => { + (assetsByNativeCurrency[nativeCurrency] ??= []).push({ + chainId, + tokenAddress, + }); + }); + } else { + marketData[chainId] = {}; + } + } + + 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, + }); + }, + ), + ); + + if (Object.keys(marketData).length > 0) { + this.update((state) => { + state.marketData = { + ...state.marketData, + ...marketData, + }; + }); + } + } + /** * Updates exchange rates for all tokens. * @@ -515,10 +664,7 @@ export class TokenRatesController extends StaticIntervalPollingController { if (this.#disabled) { return; @@ -655,28 +801,7 @@ 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; - }, []); - - await this.updateExchangeRatesByChainId(chainIdAndNativeCurrency); + await this.updateExchangeRatesToNative(chainIds); } /** @@ -700,20 +825,28 @@ export class TokenRatesController extends StaticIntervalPollingController { - let contractNativeInformations; - const tokenPricesByTokenAddress = await reduceInBatchesSerially< + return await reduceInBatchesSerially< Hex, - Awaited> + Record >({ - values: [...tokenAddresses].sort(), + values: [...tokenAddresses, getNativeTokenAddress(chainId)].sort(), batchSize: TOKEN_PRICES_BATCH_SIZE, eachBatch: async (allTokenPricesByTokenAddress, batch) => { - const tokenPricesByTokenAddressForBatch = + const tokenPricesByTokenAddressForBatch = ( await this.#tokenPricesService.fetchTokenPrices({ - tokenAddresses: batch, - chainId, + assets: batch.map((tokenAddress) => ({ + chainId, + tokenAddress, + })), currency: nativeCurrency, - }); + }) + ).reduce( + (acc, tokenPrice) => { + acc[tokenPrice.tokenAddress] = tokenPrice; + return acc; + }, + {} as Record, + ); return { ...allTokenPricesByTokenAddress, @@ -722,35 +855,6 @@ export class TokenRatesController extends StaticIntervalPollingController { - obj = { - ...obj, - [tokenAddress]: { ...token }, - }; - - return obj; - }, - {}, - ); } /** diff --git a/packages/assets-controllers/src/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.ts b/packages/assets-controllers/src/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.ts index 489a58d29eb..6036f2650b9 100644 --- a/packages/assets-controllers/src/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.ts +++ b/packages/assets-controllers/src/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.ts @@ -11,7 +11,6 @@ import type { TokenDisplayData } from './types'; import { formatIconUrlWithProxy } from '../assetsUtil'; import type { GetCurrencyRateState } from '../CurrencyRateController'; import type { AbstractTokenPricesService } from '../token-prices-service'; -import type { TokenPrice } from '../token-prices-service/abstract-token-prices-service'; import { fetchTokenMetadata, TOKEN_METADATA_NO_SUPPORT_ERROR, @@ -172,22 +171,18 @@ export class TokenSearchDiscoveryDataController extends BaseController< this.#fetchSwapsTokensThresholdMs = fetchSwapsTokensThresholdMs; } - async #fetchPriceData( - chainId: Hex, - address: string, - ): Promise | 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, tokenAddress: 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..8cfa5dd2b0f 100644 --- a/packages/assets-controllers/src/assetsUtil.ts +++ b/packages/assets-controllers/src/assetsUtil.ts @@ -14,8 +14,12 @@ import BN from 'bn.js'; import { CID } from 'multiformats/cid'; import type { Nft, NftMetadata } from './NftController'; -import type { AbstractTokenPricesService } from './token-prices-service'; -import { type ContractExchangeRates } from './TokenRatesController'; +import { + getNativeTokenAddress, + type AbstractTokenPricesService, +} from './token-prices-service'; +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 +374,26 @@ export async function fetchTokenContractExchangeRates({ const tokenPricesByTokenAddress = await reduceInBatchesSerially< Hex, - Awaited> + Record >({ - values: [...tokenAddresses].sort(), + values: [...tokenAddresses, getNativeTokenAddress(chainId)].sort(), batchSize: TOKEN_PRICES_BATCH_SIZE, eachBatch: async (allTokenPricesByTokenAddress, batch) => { - const tokenPricesByTokenAddressForBatch = + const tokenPricesByTokenAddressForBatch = ( await tokenPricesService.fetchTokenPrices({ - tokenAddresses: batch, - chainId, + assets: batch.map((tokenAddress) => ({ + chainId, + tokenAddress, + })), currency: nativeCurrency, - }); + }) + ).reduce( + (acc, tokenPrice) => { + acc[tokenPrice.tokenAddress] = 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 ddc7a3e159b..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,31 +1,7 @@ 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; - 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; -}; +import type { MarketDataDetails } from '../TokenRatesController'; /** * Represents an exchange rate. @@ -38,15 +14,15 @@ export type ExchangeRate = { usd?: number; }; -/** - * A map of token address to its price. - */ -export type TokenPricesByTokenAddress< - TokenAddress extends Hex, - Currency extends string, -> = { - [A in TokenAddress]: 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. @@ -55,22 +31,32 @@ export type ExchangeRatesByCurrency = { [C in Currency]: ExchangeRate; }; +export type EvmAssetAddressWithChain = { + tokenAddress: Hex; + chainId: ChainId; +}; + +export type EvmAssetWithId = + EvmAssetAddressWithChain & { + assetId: CaipAssetType; + }; + +export type EvmAssetWithMarketData< + ChainId extends Hex = Hex, + Currency extends string = string, +> = EvmAssetWithId & MarketDataDetails & { currency: Currency }; + /** * 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,20 +64,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, + assets, currency, }: { - chainId: ChainId; - tokenAddresses: TokenAddress[]; + assets: EvmAssetAddressWithChain[]; currency: Currency; - }): Promise>>; + }): 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 5ec2ddcee3c..acf31eaecd7 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -7,15 +7,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 { CaipAssetType, Hex } from '@metamask/utils'; +import { + hexToNumber, + KnownCaipNamespace, + toCaipChainId, +} from '@metamask/utils'; import type { AbstractTokenPricesService, + EvmAssetAddressWithChain, + EvmAssetWithId, + EvmAssetWithMarketData, ExchangeRatesByCurrency, - TokenPrice, - TokenPricesByTokenAddress, } from './abstract-token-prices-service'; +import type { MarketDataDetails } from '../TokenRatesController'; /** * The list of currencies that can be supplied as the `vsCurrency` parameter to @@ -76,6 +82,8 @@ export const SUPPORTED_CURRENCIES = [ 'eur', // British Pound Sterling 'gbp', + // Georgian Lari + 'gel', // Hong Kong Dollar 'hkd', // Hungarian Forint @@ -144,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; /** @@ -174,6 +228,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. @@ -276,96 +374,16 @@ 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'; -/** - * 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; -}; +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. */ export class CodefiTokenPricesServiceV2 - implements - AbstractTokenPricesService + implements AbstractTokenPricesService { readonly #policy: ServicePolicy; @@ -474,64 +492,68 @@ 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, + assets, currency, }: { - chainId: SupportedChainId; - tokenAddresses: Hex[]; + assets: EvmAssetAddressWithChain[]; currency: SupportedCurrency; - }): Promise>> { - const chainIdAsNumber = hexToNumber(chainId); + }): Promise[]> { + const assetsWithIds: EvmAssetWithId[] = assets.map( + (asset) => { + const caipChainId = toCaipChainId( + KnownCaipNamespace.Eip155, + hexToNumber(asset.chainId).toString(), + ); + + const nativeAddress = getNativeTokenAddress(asset.chainId); - const url = new URL(`${BASE_URL}/chains/${chainIdAsNumber}/spot-prices`); + return { + ...asset, + assetId: + nativeAddress.toLowerCase() === asset.tokenAddress.toLowerCase() + ? HEX_CHAIN_ID_TO_CAIP19_NATIVE_ASSET_MAP[asset.chainId] + : `${caipChainId}/erc20:${asset.tokenAddress.toLowerCase()}`, + }; + }, + ); + + const url = new URL(`${BASE_URL_V3}/spot-prices`); url.searchParams.append( - 'tokenAddresses', - [getNativeTokenAddress(chainId), ...tokenAddresses].join(','), + 'assetIds', + assetsWithIds.map((asset) => asset.assetId).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 addressCryptoDataMap: { + [assetId: CaipAssetType]: Omit< + MarketDataDetails, + 'currency' | 'tokenAddress' + >; + } = await this.#policy.execute(() => + handleFetch(url, { headers: { 'Cache-Control': 'no-cache' } }), + ); - const marketData = addressCryptoDataMap[lowercasedTokenAddress]; + return assetsWithIds + .map((assetWithId) => { + const marketData = addressCryptoDataMap[assetWithId.assetId]; if (!marketData) { - return obj; + return undefined; } - const token: TokenPrice = { - tokenAddress, - currency, - ...marketData, - }; - return { - ...obj, - [tokenAddress]: token, + ...marketData, + ...assetWithId, + currency, }; - }, - {}, - ) as Partial>; + }) + .filter((entry): entry is NonNullable => Boolean(entry)); } /**