diff --git a/src/components/AddressReact.tsx b/src/components/AddressReact.tsx index 46d3e69bace..c54e3d2e300 100644 --- a/src/components/AddressReact.tsx +++ b/src/components/AddressReact.tsx @@ -16,14 +16,22 @@ const AddressComponent = ({ contractUrl, address, endLength, urlClass, urlId }: if (!address) return null - const handleClick = (e) => { + const handleClick = (e: React.MouseEvent) => { e.preventDefault() + e.stopPropagation() if (address) navigator.clipboard.writeText(address) } return ( - + e.stopPropagation()} + > {endLength && address ? address.slice(0, endLength + 2) + "..." + address.slice(-endLength) : address} + @@ -77,7 +77,8 @@ function TokenChainsTable({ networks, token, lanes, environment }: TableProps) { Token pool address Pool version Custom finality - Min Blocks required + Finality depth + CCV threshold @@ -88,23 +89,33 @@ function TokenChainsTable({ networks, token, lanes, environment }: TableProps) { const allLanesPaused = areAllLanesPaused(network.tokenDecimals, lanes[network.key] || {}) return ( - + { + drawerWidthStore.set(DrawerWidth.Wide) + drawerContentStore.set(() => ( + + )) + }} + role="button" + tabIndex={0} + aria-label={`View ${network.name} token details`} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault() + e.currentTarget.click() + } + }} + > - + {network.tokenName} {network.tokenSymbol} @@ -144,7 +155,7 @@ function TokenChainsTable({ networks, token, lanes, environment }: TableProps) {
{network.tokenPoolRawType ? formatPoolTypeForDisplay(network.tokenPoolRawType) : "—"} @@ -163,14 +174,7 @@ function TokenChainsTable({ networks, token, lanes, environment }: TableProps) { {loading ? ( "-" ) : finalityData[network.key] ? ( - finalityData[network.key].hasCustomFinality === null ? ( - - ) : finalityData[network.key].hasCustomFinality ? ( + finalityData[network.key].finalitySafe ? ( "Yes" ) : ( "No" @@ -184,14 +188,13 @@ function TokenChainsTable({ networks, token, lanes, environment }: TableProps) { /> )} + {loading ? "-" : finalityData[network.key] ? finalityData[network.key].finalityDepth : "-"} - {loading - ? "-" - : finalityData[network.key] - ? finalityData[network.key].minBlockConfirmation === null - ? "-" - : finalityData[network.key].minBlockConfirmation - : "-"} + {(() => { + if (loading) return "-" + const threshold = poolDetails[network.key]?.ccv?.thresholdAmount + return threshold && threshold !== "0" ? threshold : "-" + })()} ) diff --git a/src/config/data/ccip/types.ts b/src/config/data/ccip/types.ts index e7933093424..752f3899501 100644 --- a/src/config/data/ccip/types.ts +++ b/src/config/data/ccip/types.ts @@ -45,7 +45,7 @@ type Pool = { rawType: string type: PoolType version: string - advancedPoolHooks?: string + hook?: string } export type PoolInfo = { diff --git a/src/hooks/useTokenFinality.ts b/src/hooks/useTokenFinality.ts index fd0e521ae52..2046f3e22c4 100644 --- a/src/hooks/useTokenFinality.ts +++ b/src/hooks/useTokenFinality.ts @@ -1,9 +1,10 @@ import { useState, useEffect } from "react" -import type { CustomFinalityConfig, Environment, OutputKeyType } from "~/lib/ccip/types/index.ts" +import type { PoolFinalityConfig, ChainPoolDetails, Environment, OutputKeyType } from "~/lib/ccip/types/index.ts" import { realtimeDataService } from "~/lib/ccip/services/realtime-data-instance.ts" interface UseTokenFinalityResult { - finalityData: Record + finalityData: Record + poolDetails: Record isLoading: boolean error: Error | null } @@ -13,14 +14,15 @@ interface UseTokenFinalityResult { * @param tokenCanonicalSymbol - Token canonical symbol (e.g., "BETS", "LINK") * @param environment - Network environment (mainnet/testnet) * @param outputKey - Format to use for displaying chain keys (optional) - * @returns Finality data for all chains, loading state, and error state + * @returns Finality data, pool details, loading state, and error state */ export function useTokenFinality( tokenCanonicalSymbol: string, environment: Environment, outputKey?: OutputKeyType ): UseTokenFinalityResult { - const [finalityData, setFinalityData] = useState>({}) + const [finalityData, setFinalityData] = useState>({}) + const [poolDetails, setPoolDetails] = useState>({}) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) @@ -35,11 +37,13 @@ export function useTokenFinality( const result = await realtimeDataService.getTokenFinality(tokenCanonicalSymbol, environment, outputKey) if (isMounted) { - if (result?.data) { - setFinalityData(result.data) + if (result) { + setFinalityData(result.finality) + setPoolDetails(result.poolDetails) } else { console.warn("[useTokenFinality] No data received") setFinalityData({}) + setPoolDetails({}) } } } catch (err) { @@ -47,6 +51,7 @@ export function useTokenFinality( console.error("Failed to fetch token finality data:", err) setError(err instanceof Error ? err : new Error("Failed to fetch token finality")) setFinalityData({}) + setPoolDetails({}) } } finally { if (isMounted) { @@ -62,5 +67,5 @@ export function useTokenFinality( } }, [tokenCanonicalSymbol, environment, outputKey]) - return { finalityData, isLoading, error } + return { finalityData, poolDetails, isLoading, error } } diff --git a/src/lib/ccip/graphql/services/enrichment-data-service.ts b/src/lib/ccip/graphql/services/enrichment-data-service.ts index 0fd4c4f7796..79264eebd95 100644 --- a/src/lib/ccip/graphql/services/enrichment-data-service.ts +++ b/src/lib/ccip/graphql/services/enrichment-data-service.ts @@ -20,6 +20,7 @@ import { normalizeAddressForQuery } from "~/lib/ccip/graphql/utils/address-utils import { resolveTokenAddress, resolveAllTokenAddresses, + resolveCanonicalSymbolByAddress, toSelectorName, getAllTokenSymbols, getChainFamilyForDirectoryKey, @@ -423,12 +424,16 @@ export async function fetchAllTokensForLane( ), ]) - // Build destination pool type map: tokenSymbol → destPoolType + // Build destination pool type map keyed by canonical token symbol. + // GraphQL returns the on-chain symbol (e.g. "Bridged mswETH"), which can + // differ from the canonical key in tokens.json (e.g. "mswETH"). Normalizing + // here keeps the inbound/outbound join correct when the symbols differ + // between source and destination chains. const destPoolTypeBySymbol = new Map() for (const node of inboundResult.allCcipTokenPoolLanesWithPools?.nodes ?? []) { - if (node.tokenSymbol) { - destPoolTypeBySymbol.set(node.tokenSymbol, normalizePoolType(extractRawType(node.typeAndVersion))) - } + if (!node.tokenSymbol || !node.token) continue + const canonical = resolveCanonicalSymbolByAddress(environment, node.token, destDirectoryKey) ?? node.tokenSymbol + destPoolTypeBySymbol.set(canonical, normalizePoolType(extractRawType(node.typeAndVersion))) } const results: LaneTokenData[] = [] @@ -436,12 +441,14 @@ export async function fetchAllTokensForLane( if (!node.tokenSymbol || !node.token) continue const rawType = extractRawType(node.typeAndVersion) + const canonicalSymbol = + resolveCanonicalSymbolByAddress(environment, node.token, sourceDirectoryKey) ?? node.tokenSymbol results.push({ - tokenSymbol: node.tokenSymbol, + tokenSymbol: canonicalSymbol, tokenAddress: node.token, tokenDecimals: node.tokenDecimals ?? 18, sourcePoolType: normalizePoolType(rawType), - destPoolType: destPoolTypeBySymbol.get(node.tokenSymbol) ?? "", + destPoolType: destPoolTypeBySymbol.get(canonicalSymbol) ?? "", rateLimits: { standard: toRateLimiterDirections( node.inboundCapacity, diff --git a/src/lib/ccip/graphql/utils/reference-data-resolver.ts b/src/lib/ccip/graphql/utils/reference-data-resolver.ts index b5eaa001f95..b43a300243f 100644 --- a/src/lib/ccip/graphql/utils/reference-data-resolver.ts +++ b/src/lib/ccip/graphql/utils/reference-data-resolver.ts @@ -54,6 +54,32 @@ export function resolveTokenAddress( return tokensReferenceData[tokenSymbol]?.[directoryKey]?.tokenAddress || null } +/** + * Resolves a canonical token key from an on-chain token address on a specific chain. + * + * The GraphQL API returns the on-chain symbol (e.g. "Bridged mswETH"), which can + * differ from the canonical key in tokens.json (e.g. "mswETH"). Building directory + * links from the on-chain symbol 404s — pages are generated from the canonical key. + * + * @returns Canonical token symbol/key, or null if no match found + */ +export function resolveCanonicalSymbolByAddress( + environment: Environment, + tokenAddress: string, + directoryKey: string +): string | null { + if (!tokenAddress) return null + const { tokensReferenceData } = getRefData(environment) + const normalized = tokenAddress.toLowerCase() + for (const [canonicalSymbol, chainEntries] of Object.entries(tokensReferenceData)) { + const entry = chainEntries[directoryKey] + if (entry?.tokenAddress && entry.tokenAddress.toLowerCase() === normalized) { + return canonicalSymbol + } + } + return null +} + /** * Resolves all token addresses for a token across all chains. * @returns Map of directoryKey → tokenAddress diff --git a/src/lib/ccip/services/realtime-data.ts b/src/lib/ccip/services/realtime-data.ts index 5a68a2b26f5..fc9d3f3161b 100644 --- a/src/lib/ccip/services/realtime-data.ts +++ b/src/lib/ccip/services/realtime-data.ts @@ -4,7 +4,8 @@ import type { TokenLaneData, RateLimiterEntry, RateLimiterConfig, - CustomFinalityConfig, + PoolFinalityConfig, + ChainPoolDetails, OutputKeyType, TokenDirectoryApiResponse, } from "~/lib/ccip/types/index.ts" @@ -42,7 +43,7 @@ export interface LaneSupportedTokensResponse { /** * Response structure for token finality endpoint */ -export interface TokenFinalityResponse { +export interface TokenPoolDetailsResponse { metadata: { environment: Environment timestamp: string @@ -50,9 +51,13 @@ export interface TokenFinalityResponse { tokenSymbol: string chainCount: number } - data: Record + finality: Record + poolDetails: Record } +/** @deprecated Use TokenPoolDetailsResponse instead */ +export type TokenFinalityResponse = TokenPoolDetailsResponse + /** * Service class for handling CCIP realtime data operations * Provides functionality to fetch live data from the CCIP API @@ -138,7 +143,7 @@ export class RealtimeDataService { /** * Fetches token finality details across all chains. - * Uses /tokens/{symbol} which already returns customFinality per chain, + * Uses /tokens/{symbol} which already returns pool.finality per chain, * then extracts only the finality fields to match TokenFinalityResponse shape. * * @param tokenCanonicalSymbol - Token canonical symbol (e.g., "BETS", "LINK") @@ -150,7 +155,7 @@ export class RealtimeDataService { tokenCanonicalSymbol: string, environment: Environment, outputKey?: OutputKeyType - ): Promise { + ): Promise { try { const baseUrl = getApiBaseUrl() let url = `${baseUrl}/api/ccip/v1/tokens/${tokenCanonicalSymbol}?environment=${environment}&internalIdFormat=directory` @@ -168,12 +173,29 @@ export class RealtimeDataService { const tokenDetail = await response.json() - // Extract customFinality per chain from the full token detail response - const finalityData: Record = {} + // Extract pool details per chain from the full token detail response + const finalityData: Record = {} + const poolDetailsData: Record = {} + for (const [chainKey, chainData] of Object.entries(tokenDetail.data ?? {})) { - const cd = chainData as { customFinality?: CustomFinalityConfig | null } - if (cd.customFinality !== undefined) { - finalityData[chainKey] = cd.customFinality ?? { hasCustomFinality: null, minBlockConfirmation: null } + type PoolShape = { + finality?: PoolFinalityConfig | null + ccv?: { thresholdAmount: string | null } | null + hook?: string | null + capabilities?: { supportsV2Features: boolean } + } + const cd = chainData as { pool?: PoolShape | null } + const pool = cd.pool + + if (pool?.finality) { + finalityData[chainKey] = pool.finality + } + + poolDetailsData[chainKey] = { + finality: pool?.finality ?? null, + ccv: pool?.ccv ?? null, + hook: pool?.hook ?? null, + supportsV2Features: pool?.capabilities?.supportsV2Features ?? false, } } @@ -183,7 +205,8 @@ export class RealtimeDataService { tokenSymbol: tokenCanonicalSymbol, chainCount: Object.keys(finalityData).length, }, - data: finalityData, + finality: finalityData, + poolDetails: poolDetailsData, } } catch (error) { console.error("Error fetching token finality:", error) diff --git a/src/lib/ccip/services/token-data.ts b/src/lib/ccip/services/token-data.ts index ba8253e0edf..d05c5f3e49f 100644 --- a/src/lib/ccip/services/token-data.ts +++ b/src/lib/ccip/services/token-data.ts @@ -11,7 +11,7 @@ import { TokenDetailServiceResponse, CCVConfigData, CCVConfig, - CustomFinalityConfig, + PoolFinalityConfig, } from "~/lib/ccip/types/index.ts" import { Version } from "@config/data/ccip/types.ts" import { SupportedChain } from "@config/index.ts" @@ -447,62 +447,30 @@ export class TokenDataService { ? await shouldEnableCCVFeatures(environment, tokenCanonicalSymbol, directoryKey, actualPoolVersion) : false - // Both customFinality and ccvConfig are v2.0+ pool features only - let minBlockConfirmation: number | null = null - let hasCustomFinality: boolean | null = null - let ccvConfig: CCVConfig | null = null - let customFinalityDataAvailable = false + // Build finality and CCV config (v2.0+ pool features only) + let finality: PoolFinalityConfig | null = null + let ccv: CCVConfig | null = null if (isCCVEnabled && directoryKey) { // Fetch minBlockConfirmation from GraphQL - minBlockConfirmation = await fetchMinBlockConfirmations(environment, tokenCanonicalSymbol, directoryKey) - customFinalityDataAvailable = true - - // Derive hasCustomFinality from minBlockConfirmation - if (minBlockConfirmation === null) { - hasCustomFinality = null - } else if (minBlockConfirmation > 0) { - hasCustomFinality = true - } else { - hasCustomFinality = false + const minBlockConfirmation = await fetchMinBlockConfirmations(environment, tokenCanonicalSymbol, directoryKey) + if (minBlockConfirmation != null) { + finality = { + finalityDepth: minBlockConfirmation, + finalitySafe: minBlockConfirmation > 0, + } } // Look up CCV config using directory key - // For v2 pools: - // - Entry with thresholdAmount value: configured → {thresholdAmount: "value"} - // - Entry with thresholdAmount null: downstream error → {thresholdAmount: null} - // - No entry: not configured → {thresholdAmount: "0"} if (tokenCCVConfig && directoryKey && tokenCCVConfig[directoryKey]) { - // Entry exists - use the value (could be a string or null for downstream error) - ccvConfig = { - thresholdAmount: tokenCCVConfig[directoryKey].thresholdAmount, - } + ccv = { thresholdAmount: tokenCCVConfig[directoryKey].thresholdAmount } } else { - // No entry for this v2 pool - CCV not configured - ccvConfig = { - thresholdAmount: "0", - } - } - } - - // Build customFinality response: - // - v1 pool (isCCVEnabled=false): null (feature not supported) - // - v2 pool with data: { hasCustomFinality, minBlockConfirmation } - // - v2 pool without data: { hasCustomFinality: null, minBlockConfirmation: null } (downstream error) - let customFinality: CustomFinalityConfig | null = null - if (isCCVEnabled) { - if (customFinalityDataAvailable) { - customFinality = { hasCustomFinality, minBlockConfirmation } - } else { - // v2 pool but no data available - downstream API error - customFinality = { hasCustomFinality: null, minBlockConfirmation: null } + ccv = { thresholdAmount: "0" } } } const detailChainData: TokenDetailChainData = { ...chainData, - customFinality, - ccvConfig, pool: chainData.pool ? { address: chainData.pool.address, @@ -516,8 +484,12 @@ export class TokenDataService { chainData.pool.version || "" ) : chainData.pool.version || "", - advancedPoolHooks: chainData.pool.advancedPoolHooks || null, - supportsV2Features: isCCVEnabled, + hook: chainData.pool.hook || null, + capabilities: { + supportsV2Features: isCCVEnabled, + }, + finality, + ccv, } : null, } diff --git a/src/lib/ccip/services/token-directory.ts b/src/lib/ccip/services/token-directory.ts index 81d954601d7..7ababa03b4a 100644 --- a/src/lib/ccip/services/token-directory.ts +++ b/src/lib/ccip/services/token-directory.ts @@ -5,10 +5,11 @@ import { TokenDirectoryServiceResponse, CCVConfigData, CCVChainConfig, + CCVConfig, + PoolFinalityConfig, LaneVerifiers, NamingConvention, OutputKeyType, - CustomFinalityConfig, } from "~/lib/ccip/types/index.ts" import { loadReferenceData, Version } from "@config/data/ccip/index.ts" import type { LaneConfig, ChainConfig } from "@config/data/ccip/types.ts" @@ -200,13 +201,23 @@ export class TokenDirectoryService { // Format output based on outputKey and internalIdFormat const formattedInternalId = chainIdService.format(directoryKey, internalIdFormat) - // Build custom finality config (v2.0+ pools only) - let customFinality: CustomFinalityConfig | null = null + // Build finality config (v2.0+ pools only) + let finality: PoolFinalityConfig | null = null if (isCCVEnabled) { const minBlockConfirmation = await fetchMinBlockConfirmations(environment, tokenSymbol, directoryKey) - customFinality = this.buildCustomFinalityConfig(minBlockConfirmation) + finality = this.buildFinalityConfig(minBlockConfirmation) } + // Build CCV config + // - v1.x pool (isCCVEnabled=false): null (feature not supported) + // - v2.x pool with config entry: {thresholdAmount: value} (could be null for downstream error) + // - v2.x pool without config entry: {thresholdAmount: "0"} (not configured) + const ccv: CCVConfig | null = isCCVEnabled + ? ccvConfig + ? { thresholdAmount: ccvConfig.thresholdAmount } + : { thresholdAmount: "0" } + : null + const data: TokenDirectoryData = { internalId: formattedInternalId, chainId: chainInfo.chainId, @@ -220,19 +231,13 @@ export class TokenDirectoryService { rawType: poolInfo.rawType, type: poolInfo.type, version: await getEffectivePoolVersion(environment, tokenSymbol, directoryKey, poolInfo.version || ""), - advancedPoolHooks: null, - supportsV2Features: isCCVEnabled, + hook: null, + capabilities: { + supportsV2Features: isCCVEnabled, + }, + finality, + ccv, }, - // ccvConfig handling: - // - v1.x pool (isCCVEnabled=false): null (feature not supported) - // - v2.x pool with config entry: {thresholdAmount: value} (could be null for downstream error) - // - v2.x pool without config entry: {thresholdAmount: "0"} (not configured) - ccvConfig: isCCVEnabled - ? ccvConfig - ? { thresholdAmount: ccvConfig.thresholdAmount } - : { thresholdAmount: "0" } - : null, - customFinality, outboundLanes, inboundLanes, } @@ -594,29 +599,20 @@ export class TokenDirectoryService { } /** - * Builds custom finality configuration from minBlockConfirmation value + * Builds pool finality configuration from minBlockConfirmation value * * @param minBlockConfirmation - The minimum block confirmation value, or null/undefined if unavailable - * @returns CustomFinalityConfig object with hasCustomFinality and minBlockConfirmation + * @returns PoolFinalityConfig with finalityDepth and finalitySafe, or null if unavailable */ - private buildCustomFinalityConfig(minBlockConfirmation: number | null | undefined): CustomFinalityConfig | null { - // If minBlockConfirmation is undefined, we don't have rate limits data for this token/chain - if (minBlockConfirmation === undefined) { + private buildFinalityConfig(minBlockConfirmation: number | null | undefined): PoolFinalityConfig | null { + // If minBlockConfirmation is undefined or null, data is unavailable + if (minBlockConfirmation == null) { return null } - // If minBlockConfirmation is null, there was an issue with downstream API - if (minBlockConfirmation === null) { - return { - hasCustomFinality: null, - minBlockConfirmation: null, - } - } - - // hasCustomFinality is true if minBlockConfirmation > 0 return { - hasCustomFinality: minBlockConfirmation > 0, - minBlockConfirmation, + finalityDepth: minBlockConfirmation, + finalitySafe: minBlockConfirmation > 0, } } diff --git a/src/lib/ccip/types/index.ts b/src/lib/ccip/types/index.ts index 9b77d7b91b7..bb971c907d1 100644 --- a/src/lib/ccip/types/index.ts +++ b/src/lib/ccip/types/index.ts @@ -122,7 +122,7 @@ export type TokenPool = { rawType: string type: string version: string - advancedPoolHooks?: string | null + hook?: string | null } export type TokenChainData = { @@ -164,7 +164,7 @@ export interface TokenFilterType { /** * CCV (Cross-Chain Verifier) configuration for a pool - * Only present for v2.0+ pools (check pool.supportsV2Features) + * Only present for v2.0+ pools (check pool.capabilities.supportsV2Features) * For v1.x pools, the entire ccvConfig field is null * * Values for thresholdAmount: @@ -179,44 +179,45 @@ export interface CCVConfig { // Token Detail API Types (for /tokens/{tokenCanonicalSymbol} endpoint) /** - * Custom finality configuration (reused across token endpoints) + * Pool finality configuration + * - null: v1 pool (feature not supported) or downstream API error + * - {finalityDepth: N, finalitySafe: true}: v2 pool with custom finality enabled + * - {finalityDepth: 0, finalitySafe: false}: v2 pool without custom finality */ -export interface CustomFinalityConfig { - /** Whether custom finality is enabled (derived from minBlockConfirmation > 0) */ - hasCustomFinality: boolean | null - /** Minimum block confirmations required, null if unavailable */ - minBlockConfirmation: number | null +export interface PoolFinalityConfig { + /** Minimum block confirmations used for finalized execution */ + finalityDepth: number + /** Whether FCR-safe finality is supported */ + finalitySafe: boolean +} + +/** + * Per-chain pool details extracted from /tokens/{symbol} for UI display. + * Lightweight subset of the full TokenDetailChainData pool fields. + */ +export interface ChainPoolDetails { + finality: PoolFinalityConfig | null + ccv: CCVConfig | null + hook: string | null + supportsV2Features: boolean } /** - * Extended token chain data with custom finality and CCV information + * Extended token chain data with pool capabilities, finality, and CCV information */ export interface TokenDetailChainData extends Omit { - /** Custom finality configuration for the token on this chain - * - null: v1 pool (feature not supported) - * - {hasCustomFinality: null, minBlockConfirmation: null}: v2 pool, downstream API error - * - {hasCustomFinality: false, minBlockConfirmation: 0}: v2 pool, feature not used - * - {hasCustomFinality: true, minBlockConfirmation: N}: v2 pool, feature enabled - */ - customFinality: CustomFinalityConfig | null - /** CCV (Cross-Chain Verifier) configuration for the pool - * - null: v1 pool (feature not supported, check pool.supportsV2Features) - * - {thresholdAmount: "0"}: v2 pool, CCV not configured - * - {thresholdAmount: null}: v2 pool, downstream API error - * - {thresholdAmount: "N"}: v2 pool, CCV configured with threshold N - */ - ccvConfig: CCVConfig | null - /** Pool information including version, hooks, and v2 feature support flag */ + /** Pool information including capabilities, finality, and CCV config */ pool: { address: string rawType: string type: string version: string - advancedPoolHooks: string | null - /** Whether this pool supports v2 features (customFinality, ccvConfig). - * When true and customFinality/ccvConfig fields have null values inside, - * it indicates a downstream API error rather than feature not supported. */ - supportsV2Features: boolean + hook: string | null + capabilities: { + supportsV2Features: boolean + } + finality: PoolFinalityConfig | null + ccv: CCVConfig | null } | null } @@ -570,7 +571,7 @@ export interface RateLimitsServiceResponse { /** * Verifiers configuration for a lane with pre-computed sets for different transfer amounts - * Only present for v2.0+ pools (check pool.supportsV2Features) + * Only present for v2.0+ pools (check pool.capabilities.supportsV2Features) * For v1.x pools, the entire verifiers field is null * * Values for belowThreshold/aboveThreshold: @@ -591,11 +592,11 @@ export interface LaneVerifiers { /** * Lane data in token directory response * - * Use pool.supportsV2Features to interpret verifiers: - * - pool.supportsV2Features=false + verifiers=null → v1.x pool, feature not supported - * - pool.supportsV2Features=true + verifiers={belowThreshold: null, aboveThreshold: null} → downstream API error - * - pool.supportsV2Features=true + verifiers={belowThreshold: [], aboveThreshold: []} → not configured - * - pool.supportsV2Features=true + verifiers={belowThreshold: [...], aboveThreshold: [...]} → configured + * Use pool.capabilities.supportsV2Features to interpret verifiers: + * - supportsV2Features=false + verifiers=null → v1.x pool, feature not supported + * - supportsV2Features=true + verifiers={belowThreshold: null, aboveThreshold: null} → downstream API error + * - supportsV2Features=true + verifiers={belowThreshold: [], aboveThreshold: []} → not configured + * - supportsV2Features=true + verifiers={belowThreshold: [...], aboveThreshold: [...]} → configured */ export interface TokenDirectoryLane { internalId: string @@ -622,11 +623,12 @@ export interface TokenDirectoryPoolInfo { rawType: string type: string version: string - advancedPoolHooks: string | null - /** Whether this pool supports v2 features (customFinality, ccvConfig). - * When true and customFinality/ccvConfig fields have null values inside, - * it indicates a downstream API error rather than feature not supported. */ - supportsV2Features: boolean + hook: string | null + capabilities: { + supportsV2Features: boolean + } + finality: PoolFinalityConfig | null + ccv: CCVConfig | null } /** @@ -638,8 +640,6 @@ export interface TokenDirectoryData { selector: string token: TokenDirectoryTokenInfo pool: TokenDirectoryPoolInfo - ccvConfig: CCVConfig | null - customFinality: CustomFinalityConfig | null outboundLanes: Record inboundLanes: Record } diff --git a/src/tests/token-directory.test.ts b/src/tests/token-directory.test.ts index 4af1e34f4e1..8a864385350 100644 --- a/src/tests/token-directory.test.ts +++ b/src/tests/token-directory.test.ts @@ -7,7 +7,7 @@ import type { RateLimiterConfig, TokenRateLimits, TokenFees, - CustomFinalityConfig, + PoolFinalityConfig, } from "~/lib/ccip/types/index.ts" // Mock the logger @@ -36,15 +36,17 @@ describe("Token Directory Types", () => { rawType: "BurnMintTokenPool", type: "burnMint", version: "1.6.0", - advancedPoolHooks: null, - supportsV2Features: true, - }, - ccvConfig: { - thresholdAmount: "100000000000", - }, - customFinality: { - hasCustomFinality: true, - minBlockConfirmation: 5, + hook: null, + capabilities: { + supportsV2Features: true, + }, + finality: { + finalityDepth: 5, + finalitySafe: true, + }, + ccv: { + thresholdAmount: "100000000000", + }, }, outboundLanes: {}, inboundLanes: {}, @@ -55,15 +57,15 @@ describe("Token Directory Types", () => { expect(data.selector).toBe("5009297550715157269") expect(data.token.address).toBe("0x8236a87084f8B84306f72007F36F2618A5634494") expect(data.token.decimals).toBe(8) - expect(data.customFinality?.hasCustomFinality).toBe(true) - expect(data.customFinality?.minBlockConfirmation).toBe(5) + expect(data.pool.finality?.finalitySafe).toBe(true) + expect(data.pool.finality?.finalityDepth).toBe(5) expect(data.pool.type).toBe("burnMint") - expect(data.pool.advancedPoolHooks).toBeNull() - expect(data.ccvConfig?.thresholdAmount).toBe("100000000000") + expect(data.pool.hook).toBeNull() + expect(data.pool.ccv?.thresholdAmount).toBe("100000000000") }) - it("should allow null ccvConfig and customFinality for v1.x pools only", () => { - // For v1.x pools (supportsV2Features=false), ccvConfig and customFinality are null + it("should allow null finality and ccv for v1.x pools only", () => { + // For v1.x pools (supportsV2Features=false), finality and ccv are null const data: TokenDirectoryData = { internalId: "mainnet", chainId: 1, @@ -77,22 +79,24 @@ describe("Token Directory Types", () => { rawType: "LockReleaseTokenPool", type: "lockRelease", version: "1.6.0", - advancedPoolHooks: null, - supportsV2Features: false, // v1.x pool - ccvConfig and customFinality not supported + hook: null, + capabilities: { + supportsV2Features: false, // v1.x pool - finality and ccv not supported + }, + finality: null, + ccv: null, }, - ccvConfig: null, // null only valid for v1.x pools - customFinality: null, // null only valid for v1.x pools outboundLanes: {}, inboundLanes: {}, } - expect(data.pool.supportsV2Features).toBe(false) - expect(data.ccvConfig).toBeNull() - expect(data.customFinality).toBeNull() + expect(data.pool.capabilities.supportsV2Features).toBe(false) + expect(data.pool.finality).toBeNull() + expect(data.pool.ccv).toBeNull() }) - it("should have ccvConfig object for v2.x pools (never null at field level)", () => { - // For v2.x pools, ccvConfig is always an object: + it("should have ccv object for v2.x pools (never null at field level)", () => { + // For v2.x pools, ccv is always an object: // - {thresholdAmount: "0"} = not configured // - {thresholdAmount: "value"} = configured // - {thresholdAmount: null} = downstream API error @@ -109,18 +113,20 @@ describe("Token Directory Types", () => { rawType: "LockReleaseTokenPool", type: "lockRelease", version: "2.0.0", - advancedPoolHooks: null, - supportsV2Features: true, // v2.x pool + hook: null, + capabilities: { + supportsV2Features: true, // v2.x pool + }, + finality: { finalityDepth: 0, finalitySafe: false }, + ccv: { thresholdAmount: "0" }, // v2 pool without CCV configured }, - ccvConfig: { thresholdAmount: "0" }, // v2 pool without CCV configured - customFinality: { hasCustomFinality: false, minBlockConfirmation: 0 }, outboundLanes: {}, inboundLanes: {}, } - expect(dataNotConfigured.pool.supportsV2Features).toBe(true) - expect(dataNotConfigured.ccvConfig).not.toBeNull() - expect(dataNotConfigured.ccvConfig?.thresholdAmount).toBe("0") + expect(dataNotConfigured.pool.capabilities.supportsV2Features).toBe(true) + expect(dataNotConfigured.pool.ccv).not.toBeNull() + expect(dataNotConfigured.pool.ccv?.thresholdAmount).toBe("0") }) }) @@ -346,15 +352,10 @@ describe("Token Directory API response validation", () => { rawType: "BurnMintTokenPool", type: "burnMint", version: "1.6.0", - advancedPoolHooks: null, - supportsV2Features: true, - }, - ccvConfig: { - thresholdAmount: "100000000000", - }, - customFinality: { - hasCustomFinality: true, - minBlockConfirmation: 5, + hook: null, + capabilities: { supportsV2Features: true }, + finality: { finalityDepth: 5, finalitySafe: true }, + ccv: { thresholdAmount: "100000000000" }, }, outboundLanes: {}, inboundLanes: {}, @@ -396,7 +397,7 @@ describe("Token Directory API response validation", () => { describe("Token Directory example responses", () => { it("should represent v2.0 pool (LBTC) with CCV features enabled", () => { - // v2.0 pool - CCV features enabled (ccvConfig, threshold verifiers) + // v2.0 pool - CCV features enabled (ccv, threshold verifiers) const lbtcData: TokenDirectoryData = { internalId: "mainnet", chainId: 1, @@ -410,15 +411,10 @@ describe("Token Directory example responses", () => { rawType: "BurnMintTokenPool", type: "burnMint", version: "2.0.0", // v2.0 pool - advancedPoolHooks: null, - supportsV2Features: true, - }, - ccvConfig: { - thresholdAmount: "100000000000", // CCV config present for v2.0+ - }, - customFinality: { - hasCustomFinality: true, - minBlockConfirmation: 5, + hook: null, + capabilities: { supportsV2Features: true }, + finality: { finalityDepth: 5, finalitySafe: true }, + ccv: { thresholdAmount: "100000000000" }, }, outboundLanes: { "arbitrum-mainnet": { @@ -470,9 +466,9 @@ describe("Token Directory example responses", () => { } expect(lbtcData.pool.version).toBe("2.0.0") - expect(lbtcData.ccvConfig).not.toBeNull() - expect(lbtcData.ccvConfig?.thresholdAmount).toBeDefined() - expect(parseInt(lbtcData.ccvConfig?.thresholdAmount || "0")).toBeGreaterThan(0) + expect(lbtcData.pool.ccv).not.toBeNull() + expect(lbtcData.pool.ccv?.thresholdAmount).toBeDefined() + expect(parseInt(lbtcData.pool.ccv?.thresholdAmount || "0")).toBeGreaterThan(0) const outboundLane = lbtcData.outboundLanes["arbitrum-mainnet"] expect(outboundLane.verifiers).not.toBeNull() expect(outboundLane.verifiers!.aboveThreshold!.length).toBeGreaterThan( @@ -484,7 +480,7 @@ describe("Token Directory example responses", () => { }) it("should represent v1.6 pool (DAI) with CCV features disabled", () => { - // v1.6 pool - CCV features disabled (no ccvConfig, verifiers null) + // v1.6 pool - CCV features disabled (no ccv, verifiers null) const daiData: TokenDirectoryData = { internalId: "mainnet", chainId: 1, @@ -498,11 +494,13 @@ describe("Token Directory example responses", () => { rawType: "LockReleaseTokenPool", type: "lockRelease", version: "1.6.0", // v1.6 pool - advancedPoolHooks: null, - supportsV2Features: false, // v1.x pool does NOT support v2 features + hook: null, + capabilities: { + supportsV2Features: false, // v1.x pool does NOT support v2 features + }, + finality: null, + ccv: null, }, - ccvConfig: null, // No CCV config for v1.x pools - customFinality: null, // v1.x pool - feature not supported outboundLanes: { "arbitrum-mainnet": { internalId: "arbitrum-mainnet", @@ -523,9 +521,9 @@ describe("Token Directory example responses", () => { } expect(daiData.pool.version).toBe("1.6.0") - expect(daiData.pool.supportsV2Features).toBe(false) - expect(daiData.ccvConfig).toBeNull() - expect(daiData.customFinality).toBeNull() + expect(daiData.pool.capabilities.supportsV2Features).toBe(false) + expect(daiData.pool.ccv).toBeNull() + expect(daiData.pool.finality).toBeNull() expect(daiData.outboundLanes["arbitrum-mainnet"].verifiers).toBeNull() expect(daiData.outboundLanes["arbitrum-mainnet"].rateLimits.standard).not.toBeNull() expect(daiData.outboundLanes["arbitrum-mainnet"].rateLimits.custom).toBeNull() @@ -534,7 +532,7 @@ describe("Token Directory example responses", () => { describe("Version-conditional API behavior", () => { describe("v1.x pool response format", () => { - it("should have null ccvConfig and customFinality for v1.x pools", () => { + it("should have null finality and ccv for v1.x pools", () => { const v1PoolData: TokenDirectoryData = { internalId: "mainnet", chainId: 1, @@ -545,18 +543,20 @@ describe("Version-conditional API behavior", () => { rawType: "LockReleaseTokenPool", type: "lockRelease", version: "1.6.0", - advancedPoolHooks: null, - supportsV2Features: false, // v1.x pool does NOT support v2 features + hook: null, + capabilities: { + supportsV2Features: false, + }, + finality: null, + ccv: null, }, - ccvConfig: null, // Must be null for v1.x (feature not supported) - customFinality: null, // Must be null for v1.x (feature not supported) outboundLanes: {}, inboundLanes: {}, } - expect(v1PoolData.pool.supportsV2Features).toBe(false) - expect(v1PoolData.ccvConfig).toBeNull() - expect(v1PoolData.customFinality).toBeNull() + expect(v1PoolData.pool.capabilities.supportsV2Features).toBe(false) + expect(v1PoolData.pool.ccv).toBeNull() + expect(v1PoolData.pool.finality).toBeNull() }) it("should have null verifiers for v1.x pools", () => { @@ -574,7 +574,7 @@ describe("Version-conditional API behavior", () => { }) describe("v2.0+ pool response format", () => { - it("should have ccvConfig with thresholdAmount for v2.0+ pools", () => { + it("should have ccv with thresholdAmount for v2.0+ pools", () => { const v2PoolData: TokenDirectoryData = { internalId: "mainnet", chainId: 1, @@ -585,21 +585,18 @@ describe("Version-conditional API behavior", () => { rawType: "BurnMintTokenPool", type: "burnMint", version: "2.0.0", - advancedPoolHooks: null, - supportsV2Features: true, - }, - ccvConfig: { thresholdAmount: "100000000000" }, // Present for v2.0+ - customFinality: { - hasCustomFinality: true, - minBlockConfirmation: 5, + hook: null, + capabilities: { supportsV2Features: true }, + finality: { finalityDepth: 5, finalitySafe: true }, + ccv: { thresholdAmount: "100000000000" }, }, outboundLanes: {}, inboundLanes: {}, } - expect(v2PoolData.ccvConfig).not.toBeNull() - expect(v2PoolData.ccvConfig?.thresholdAmount).toBeDefined() - expect(v2PoolData.customFinality?.hasCustomFinality).toBe(true) + expect(v2PoolData.pool.ccv).not.toBeNull() + expect(v2PoolData.pool.ccv?.thresholdAmount).toBeDefined() + expect(v2PoolData.pool.finality?.finalitySafe).toBe(true) }) it("should have aboveThreshold include additional verifiers for v2.0+ pools", () => { @@ -633,50 +630,40 @@ describe("Version-conditional API behavior", () => { }) }) -describe("CustomFinalityConfig structure", () => { - it("should have hasCustomFinality and minBlockConfirmation", () => { - const config: CustomFinalityConfig = { - hasCustomFinality: true, - minBlockConfirmation: 5, - } - - expect(config.hasCustomFinality).toBe(true) - expect(config.minBlockConfirmation).toBe(5) - }) - - it("should allow null values when data is unavailable", () => { - const config: CustomFinalityConfig = { - hasCustomFinality: null, - minBlockConfirmation: null, +describe("PoolFinalityConfig structure", () => { + it("should have finalityDepth and finalitySafe", () => { + const config: PoolFinalityConfig = { + finalityDepth: 5, + finalitySafe: true, } - expect(config.hasCustomFinality).toBeNull() - expect(config.minBlockConfirmation).toBeNull() + expect(config.finalityDepth).toBe(5) + expect(config.finalitySafe).toBe(true) }) - it("should have hasCustomFinality=false when minBlockConfirmation is 0", () => { - const config: CustomFinalityConfig = { - hasCustomFinality: false, - minBlockConfirmation: 0, + it("should have finalitySafe=false when finalityDepth is 0", () => { + const config: PoolFinalityConfig = { + finalityDepth: 0, + finalitySafe: false, } - expect(config.hasCustomFinality).toBe(false) - expect(config.minBlockConfirmation).toBe(0) + expect(config.finalitySafe).toBe(false) + expect(config.finalityDepth).toBe(0) }) - it("should derive hasCustomFinality from minBlockConfirmation > 0", () => { - // hasCustomFinality = true when minBlockConfirmation > 0 - const enabledConfig: CustomFinalityConfig = { - hasCustomFinality: true, - minBlockConfirmation: 3, + it("should derive finalitySafe from finalityDepth > 0", () => { + // finalitySafe = true when finalityDepth > 0 + const enabledConfig: PoolFinalityConfig = { + finalityDepth: 3, + finalitySafe: true, } - expect(enabledConfig.hasCustomFinality).toBe(enabledConfig.minBlockConfirmation! > 0) + expect(enabledConfig.finalitySafe).toBe(enabledConfig.finalityDepth > 0) - // hasCustomFinality = false when minBlockConfirmation = 0 - const disabledConfig: CustomFinalityConfig = { - hasCustomFinality: false, - minBlockConfirmation: 0, + // finalitySafe = false when finalityDepth = 0 + const disabledConfig: PoolFinalityConfig = { + finalityDepth: 0, + finalitySafe: false, } - expect(disabledConfig.hasCustomFinality).toBe(disabledConfig.minBlockConfirmation! > 0) + expect(disabledConfig.finalitySafe).toBe(disabledConfig.finalityDepth > 0) }) })