diff --git a/src/components/CCIP/Chain/Chain.astro b/src/components/CCIP/Chain/Chain.astro index 484387ac050..4bf84bc4f9c 100644 --- a/src/components/CCIP/Chain/Chain.astro +++ b/src/components/CCIP/Chain/Chain.astro @@ -15,6 +15,7 @@ import ChainHero from "~/components/CCIP/ChainHero/ChainHero" import ChainTable from "~/components/CCIP/Tables/ChainTable" import { getTokenIconUrl } from "~/features/utils" import ChainTokenGrid from "./ChainTokenGrid" +import { fetchAllPoolData } from "~/lib/ccip/graphql/services/enrichment-data-service.ts" import { generateChainStructuredData } from "~/utils/ccipStructuredData" import StructuredData from "~/components/StructuredData.astro" import { DOCS_BASE_URL } from "~/utils/structuredData" @@ -50,6 +51,14 @@ const lanes = await getAllNetworkLanes({ const searchLanes = getSearchLanes({ environment }) +const allPoolData = await fetchAllPoolData(environment) +const poolDataByToken: Record = {} +for (const [tokenId, chainPoolData] of Object.entries(allPoolData)) { + if (chainPoolData[network.chain]) { + poolDataByToken[tokenId] = chainPoolData[network.chain] + } +} + const allVerifiers = getAllUniqueVerifiers({ environment, version: Version.V1_2_0, @@ -144,7 +153,13 @@ const chainStructuredData = generateChainStructuredData( ) } - + diff --git a/src/components/CCIP/Chain/ChainTokenGrid.tsx b/src/components/CCIP/Chain/ChainTokenGrid.tsx index dcfa6b3610c..ea3886c7ca2 100644 --- a/src/components/CCIP/Chain/ChainTokenGrid.tsx +++ b/src/components/CCIP/Chain/ChainTokenGrid.tsx @@ -1,4 +1,5 @@ import { Environment, Version, Network } from "~/config/data/ccip/types.ts" +import type { PoolType } from "~/config/data/ccip/types.ts" import { getTokenData } from "~/config/data/ccip/data.ts" import TokenCard from "../Cards/TokenCard.tsx" import { drawerContentStore, DrawerWidth, drawerWidthStore } from "../Drawer/drawerStore.ts" @@ -7,6 +8,7 @@ import { directoryToSupportedChain, getChainIcon, getChainTypeAndFamily, getTitl import { useState } from "react" import "./ChainTokenGrid.css" import SeeMore from "../SeeMore/SeeMore.tsx" +import type { PoolInfo } from "~/lib/ccip/graphql/services/enrichment-data-service.ts" interface ChainTokenGridProps { tokens: { @@ -16,11 +18,12 @@ interface ChainTokenGridProps { }[] network: Network environment: Environment + poolDataByToken?: Record } const BEFORE_SEE_MORE = 6 * 4 // Number of networks to show before the "See more" button, 7 rows x 4 items -function ChainTokenGrid({ tokens, network, environment }: ChainTokenGridProps) { +function ChainTokenGrid({ tokens, network, environment, poolDataByToken }: ChainTokenGridProps) { const [seeMore, setSeeMore] = useState(tokens.length <= BEFORE_SEE_MORE) return ( <> @@ -38,6 +41,7 @@ function ChainTokenGrid({ tokens, network, environment }: ChainTokenGridProps) { key={token.id} variant="square" onClick={() => { + const poolInfo = poolDataByToken?.[token.id] const selectedNetwork = Object.keys(data) .map((key) => { const supportedChain = directoryToSupportedChain(key || "") @@ -54,10 +58,10 @@ function ChainTokenGrid({ tokens, network, environment }: ChainTokenGridProps) { tokenSymbol: data[key].symbol, tokenDecimals: data[key].decimals, tokenAddress: data[key].tokenAddress, - tokenPoolType: data[key].pool?.type ?? "burnMint", - tokenPoolRawType: data[key].pool?.rawType ?? "", - tokenPoolAddress: data[key].pool?.address ?? "", - tokenPoolVersion: data[key].pool?.version ?? "", + tokenPoolType: (poolInfo?.type ?? data[key].pool?.type ?? "burnMint") as PoolType, + tokenPoolRawType: poolInfo?.rawType ?? data[key].pool?.rawType ?? "", + tokenPoolAddress: poolInfo?.address ?? data[key].pool?.address ?? "", + tokenPoolVersion: poolInfo?.version ?? data[key].pool?.version ?? "", explorer: network.explorer, chainType, } diff --git a/src/components/CCIP/ChainHero/ChainHero.tsx b/src/components/CCIP/ChainHero/ChainHero.tsx index b112340bfb6..b6199ff352f 100644 --- a/src/components/CCIP/ChainHero/ChainHero.tsx +++ b/src/components/CCIP/ChainHero/ChainHero.tsx @@ -147,39 +147,43 @@ function ChainHero({ -
- { - currentTarget.onerror = null // prevents looping - currentTarget.src = fallbackTokenIconUrl - }} - /> -

- {network?.name || token?.id} - - {token?.id === "USDC" ? "USD Coin" : token?.name} - - - {chainTooltipConfig && ( - + {(network?.logo || token?.logo) && ( + { + currentTarget.onerror = null // prevents looping + currentTarget.src = fallbackTokenIconUrl + }} /> )} -

-
+

+ {network?.name || token?.id} + + {token?.id === "USDC" ? "USD Coin" : token?.name} + + + {chainTooltipConfig && ( + + )} +

+ + )} {network && (
diff --git a/src/components/CCIP/ChainHero/TokenDetailsHero.tsx b/src/components/CCIP/ChainHero/TokenDetailsHero.tsx index dc31dab05cd..984aebd9151 100644 --- a/src/components/CCIP/ChainHero/TokenDetailsHero.tsx +++ b/src/components/CCIP/ChainHero/TokenDetailsHero.tsx @@ -2,7 +2,6 @@ import Address from "~/components/AddressReact.tsx" import { getExplorerAddressUrl, fallbackTokenIconUrl } from "~/features/utils/index.ts" import "./ChainHero.css" import { ExplorerInfo, ChainType } from "~/config/types.ts" -import { getNetworkIconUrl } from "~/config/data/ccip/data.ts" import { formatPoolTypeForDisplay } from "~/lib/ccip/graphql/utils/type-version-parser.ts" interface TokenDetailsHeroProps { @@ -31,7 +30,7 @@ function TokenDetailsHero({ network, token, inDrawer = false }: TokenDetailsHero
- +
- {token.data[sourceNetwork.key].decimals} + {token.decimals} {inOutbound === LaneFilter.Outbound - ? determineTokenMechanism( - token.data[sourceNetwork.key].pool?.type, - token.data[destinationNetwork.key].pool?.type - ) - : determineTokenMechanism( - token.data[destinationNetwork.key].pool?.type, - token.data[sourceNetwork.key].pool?.type - )} + ? determineTokenMechanism(token.sourcePoolType as PoolType, token.destPoolType as PoolType) + : determineTokenMechanism(token.destPoolType as PoolType, token.sourcePoolType as PoolType)} @@ -261,7 +255,9 @@ function LaneDrawer({
-
{processedTokens.length === 0 && <>No tokens found}
+
+ {isLoadingRateLimits ? <>Loading... : processedTokens.length === 0 && <>No tokens found} +
) diff --git a/src/hooks/useLaneTokens.ts b/src/hooks/useLaneTokens.ts index c6d503cbc0e..5321a4458d8 100644 --- a/src/hooks/useLaneTokens.ts +++ b/src/hooks/useLaneTokens.ts @@ -1,12 +1,15 @@ import { useMemo } from "react" -import { Environment, LaneFilter, Version } from "~/config/data/ccip/types.ts" -import { getTokenData } from "~/config/data/ccip/data.ts" +import { LaneFilter } from "~/config/data/ccip/types.ts" import { getTokenIconUrl } from "~/features/utils/index.ts" import { realtimeDataService } from "~/lib/ccip/services/realtime-data-instance.ts" +import type { TokenLaneData } from "~/lib/ccip/types/index.ts" export interface ProcessedToken { id: string - data: ReturnType + tokenAddress: string + decimals: number + sourcePoolType: string + destPoolType: string logo: string rateLimits: { standard: { capacity: string; rate: string; isEnabled: boolean } | null @@ -17,13 +20,12 @@ export interface ProcessedToken { interface UseLaneTokensParams { tokens: string[] | undefined - environment: Environment - rateLimitsData: Record + rateLimitsData: Record inOutbound: LaneFilter searchQuery: string } -export function useLaneTokens({ tokens, environment, rateLimitsData, inOutbound, searchQuery }: UseLaneTokensParams) { +export function useLaneTokens({ tokens, rateLimitsData, inOutbound, searchQuery }: UseLaneTokensParams) { const processedTokens = useMemo(() => { if (!tokens) return [] @@ -32,30 +34,26 @@ export function useLaneTokens({ tokens, environment, rateLimitsData, inOutbound, return tokens .filter((token) => token.toLowerCase().includes(searchQuery.toLowerCase())) .map((token) => { - const data = getTokenData({ - environment, - version: Version.V1_2_0, - tokenId: token || "", - }) - - // Skip tokens with no data - if (!Object.keys(data).length) return null + const tokenLaneData = rateLimitsData[token] + if (!tokenLaneData) return null const logo = getTokenIconUrl(token) - const tokenLaneData = rateLimitsData[token] - const allLimits = realtimeDataService.getAllRateLimitsForDirection(tokenLaneData?.rateLimits, direction) + const allLimits = realtimeDataService.getAllRateLimitsForDirection(tokenLaneData.rateLimits, direction) const isPaused = allLimits.standard?.capacity === "0" return { id: token, - data, + tokenAddress: tokenLaneData.tokenAddress ?? "", + decimals: tokenLaneData.tokenDecimals ?? 0, + sourcePoolType: tokenLaneData.sourcePoolType ?? "", + destPoolType: tokenLaneData.destPoolType ?? "", logo, rateLimits: allLimits, isPaused, } }) .filter((token): token is ProcessedToken => token !== null) - }, [tokens, environment, rateLimitsData, inOutbound, searchQuery]) + }, [tokens, rateLimitsData, inOutbound, searchQuery]) return { tokens: processedTokens, diff --git a/src/lib/ccip/graphql/services/enrichment-data-service.ts b/src/lib/ccip/graphql/services/enrichment-data-service.ts index ebeab66a15d..0fd4c4f7796 100644 --- a/src/lib/ccip/graphql/services/enrichment-data-service.ts +++ b/src/lib/ccip/graphql/services/enrichment-data-service.ts @@ -372,6 +372,105 @@ export async function fetchLaneRateLimits( } } +// ---------- Batch lane tokens ---------- + +export interface LaneTokenData { + tokenSymbol: string + tokenAddress: string + tokenDecimals: number + sourcePoolType: string + destPoolType: string + rateLimits: RawTokenRateLimits | null +} + +/** + * Fetches all tokens for a lane in two parallel batch queries (outbound + inbound). + * Replaces the previous N+1 approach of calling fetchLaneRateLimits per token. + * + * Outbound query (network=src, remoteNetworkName=dest): + * → source token address, decimals, source pool type, rate limits + * Inbound query (network=dest, remoteNetworkName=src): + * → destination pool type per token symbol + */ +export async function fetchAllTokensForLane( + environment: Environment, + sourceDirectoryKey: string, + destDirectoryKey: string +): Promise { + const srcNetwork = toSelectorName(environment, sourceDirectoryKey) + const dstNetwork = toSelectorName(environment, destDirectoryKey) + + const cacheKey = `lane-batch|${environment}|${srcNetwork}|${dstNetwork}` + + try { + return await cached(cacheKey, async () => { + const [outboundResult, inboundResult] = await Promise.all([ + executeGraphQLQuery( + TOKEN_POOL_LANES_WITH_POOLS_QUERY, + { + first: 500, + condition: { network: srcNetwork, remoteNetworkName: dstNetwork }, + filter: { removed: { notEqualTo: true } }, + } + ), + executeGraphQLQuery( + TOKEN_POOL_LANES_WITH_POOLS_QUERY, + { + first: 500, + condition: { network: dstNetwork, remoteNetworkName: srcNetwork }, + filter: { removed: { notEqualTo: true } }, + } + ), + ]) + + // Build destination pool type map: tokenSymbol → destPoolType + const destPoolTypeBySymbol = new Map() + for (const node of inboundResult.allCcipTokenPoolLanesWithPools?.nodes ?? []) { + if (node.tokenSymbol) { + destPoolTypeBySymbol.set(node.tokenSymbol, normalizePoolType(extractRawType(node.typeAndVersion))) + } + } + + const results: LaneTokenData[] = [] + for (const node of outboundResult.allCcipTokenPoolLanesWithPools?.nodes ?? []) { + if (!node.tokenSymbol || !node.token) continue + + const rawType = extractRawType(node.typeAndVersion) + results.push({ + tokenSymbol: node.tokenSymbol, + tokenAddress: node.token, + tokenDecimals: node.tokenDecimals ?? 18, + sourcePoolType: normalizePoolType(rawType), + destPoolType: destPoolTypeBySymbol.get(node.tokenSymbol) ?? "", + rateLimits: { + standard: toRateLimiterDirections( + node.inboundCapacity, + node.inboundRate, + node.inboundEnabled, + node.outboundCapacity, + node.outboundRate, + node.outboundEnabled + ), + custom: toRateLimiterDirections( + node.customInboundCapacity, + node.customInboundRate, + node.customInboundEnabled, + node.customOutboundCapacity, + node.customOutboundRate, + node.customOutboundEnabled + ), + }, + }) + } + + return results + }) + } catch (error) { + console.error(`[CCIP GraphQL] fetchAllTokensForLane failed: ${sourceDirectoryKey}->${destDirectoryKey}`, error) + return [] + } +} + // ---------- Stubs ---------- // TODO: CCV verifier data is not yet available in the GraphQL schema. diff --git a/src/lib/ccip/services/lane-data.ts b/src/lib/ccip/services/lane-data.ts index 88bed0137fc..9fd0b1b8f1b 100644 --- a/src/lib/ccip/services/lane-data.ts +++ b/src/lib/ccip/services/lane-data.ts @@ -27,10 +27,7 @@ import { } from "../../../features/utils/index.ts" import { getSelectorEntry } from "@config/data/ccip/selectors.ts" -import pLimit from "p-limit" -import { fetchLaneRateLimits } from "~/lib/ccip/graphql/services/enrichment-data-service.ts" - -const GRAPHQL_CONCURRENCY = 10 +import { fetchAllTokensForLane } from "~/lib/ccip/graphql/services/enrichment-data-service.ts" export const prerender = false @@ -657,39 +654,20 @@ export class LaneDataService { internalId: destChain.internalId, } - // Extract supported token symbols - const tokenSymbols = this.extractSupportedTokens(laneConfig) - - // Fetch rate limits per token concurrently - const limit = pLimit(GRAPHQL_CONCURRENCY) + // Fetch all tokens for the lane in a single batch GraphQL call + const laneTokens = await fetchAllTokensForLane(environment, sourceInternalId, destinationInternalId) const supportedTokensWithRateLimits: Record = {} - const tokenResults = await Promise.allSettled( - tokenSymbols.map((tokenSymbol) => - limit(async () => ({ - tokenSymbol, - rateLimits: await fetchLaneRateLimits(environment, tokenSymbol, sourceInternalId, destinationInternalId), - })) - ) - ) - - for (const settled of tokenResults) { - if (settled.status !== "fulfilled") continue - const { tokenSymbol, rateLimits: laneRateLimits } = settled.value - - if (laneRateLimits) { - supportedTokensWithRateLimits[tokenSymbol] = { - rateLimits: { standard: laneRateLimits.standard, custom: laneRateLimits.custom }, - fees: null, - } - } else { - logger.warn({ - message: "Token in lanes.json supportedTokens has no on-chain lane data — filtering out", - requestId: this.requestId, - tokenSymbol, - sourceInternalId, - destinationInternalId, - }) + for (const laneToken of laneTokens) { + supportedTokensWithRateLimits[laneToken.tokenSymbol] = { + rateLimits: laneToken.rateLimits + ? { standard: laneToken.rateLimits.standard, custom: laneToken.rateLimits.custom } + : { standard: null, custom: null }, + fees: null, + tokenAddress: laneToken.tokenAddress, + tokenDecimals: laneToken.tokenDecimals, + sourcePoolType: laneToken.sourcePoolType, + destPoolType: laneToken.destPoolType, } } @@ -773,39 +751,20 @@ export class LaneDataService { return { data: null, tokenCount: 0 } } - // Extract supported token symbols - const tokenSymbols = this.extractSupportedTokens(laneConfig) - - // Fetch rate limits per token concurrently - const limit = pLimit(GRAPHQL_CONCURRENCY) + // Fetch all tokens for the lane in a single batch GraphQL call + const laneTokens = await fetchAllTokensForLane(environment, sourceInternalId, destinationInternalId) const supportedTokensWithRateLimits: Record = {} - const tokenResults = await Promise.allSettled( - tokenSymbols.map((tokenSymbol) => - limit(async () => ({ - tokenSymbol, - rateLimits: await fetchLaneRateLimits(environment, tokenSymbol, sourceInternalId, destinationInternalId), - })) - ) - ) - - for (const settled of tokenResults) { - if (settled.status !== "fulfilled") continue - const { tokenSymbol, rateLimits: laneRateLimits } = settled.value - - if (laneRateLimits) { - supportedTokensWithRateLimits[tokenSymbol] = { - rateLimits: { standard: laneRateLimits.standard, custom: laneRateLimits.custom }, - fees: null, - } - } else { - logger.warn({ - message: "Token in lanes.json supportedTokens has no on-chain lane data — filtering out", - requestId: this.requestId, - tokenSymbol, - sourceInternalId, - destinationInternalId, - }) + for (const laneToken of laneTokens) { + supportedTokensWithRateLimits[laneToken.tokenSymbol] = { + rateLimits: laneToken.rateLimits + ? { standard: laneToken.rateLimits.standard, custom: laneToken.rateLimits.custom } + : { standard: null, custom: null }, + fees: null, + tokenAddress: laneToken.tokenAddress, + tokenDecimals: laneToken.tokenDecimals, + sourcePoolType: laneToken.sourcePoolType, + destPoolType: laneToken.destPoolType, } } diff --git a/src/lib/ccip/types/index.ts b/src/lib/ccip/types/index.ts index 46f7ceb147c..9b77d7b91b7 100644 --- a/src/lib/ccip/types/index.ts +++ b/src/lib/ccip/types/index.ts @@ -413,6 +413,10 @@ export interface TokenRateLimits { export interface TokenLaneData { rateLimits: TokenRateLimits fees: TokenFees | null + tokenAddress?: string + tokenDecimals?: number + sourcePoolType?: string + destPoolType?: string } /**