diff --git a/src/components/CCIP/Cards/TokenCard.css b/src/components/CCIP/Cards/TokenCard.css index d8e2b7d6e2d..ae035e97a19 100644 --- a/src/components/CCIP/Cards/TokenCard.css +++ b/src/components/CCIP/Cards/TokenCard.css @@ -51,3 +51,58 @@ height: 124px; } } + +/* Square variant styles */ +.token-card__square-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--space-6x); + width: 100%; + background: var(--white); + border: 1px solid var(--gray-200); + border-radius: var(--space-1x); + text-align: center; +} + +.token-card__square-container:hover { + background-color: var(--gray-50); +} + +.token-card__square-logo { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: var(--space-4x); +} + +.token-card__square-logo object, +.token-card__square-logo object img { + width: var(--space-16x); + height: var(--space-16x); + border-radius: 50%; +} + +.token-card__square-content { + display: flex; + flex-direction: column; + align-items: center; +} + +.token-card__square-content h3 { + font-size: var(--space-4x); + font-weight: var(--font-weight-medium); + line-height: var(--space-6x); + color: var(--gray-950); + margin-bottom: var(--space-1x); + text-align: center; +} + +.token-card__square-content p { + margin-bottom: 0; + font-size: var(--space-3x); + line-height: var(--space-5x); + color: var(--gray-500); + text-align: center; +} diff --git a/src/components/CCIP/Cards/TokenCard.tsx b/src/components/CCIP/Cards/TokenCard.tsx index 7810da7f4f7..51372209112 100644 --- a/src/components/CCIP/Cards/TokenCard.tsx +++ b/src/components/CCIP/Cards/TokenCard.tsx @@ -9,9 +9,17 @@ interface TokenCardProps { link?: string onClick?: () => void totalNetworks?: number + variant?: "default" | "square" } -const TokenCard = memo(function TokenCard({ id, logo, link, onClick, totalNetworks }: TokenCardProps) { +const TokenCard = memo(function TokenCard({ + id, + logo, + link, + onClick, + totalNetworks, + variant = "default", +}: TokenCardProps) { const logoElement = ( {`${id} @@ -21,6 +29,41 @@ const TokenCard = memo(function TokenCard({ id, logo, link, onClick, totalNetwor const subtitle = totalNetworks !== undefined ? `${totalNetworks} ${totalNetworks === 1 ? "network" : "networks"}` : undefined + if (variant === "square") { + const content = ( + <> +
{logoElement}
+
+

{id}

+ {subtitle &&

{subtitle}

} +
+ + ) + + if (link) { + return ( + +
{content}
+
+ ) + } + + if (onClick) { + return ( + + ) + } + + return
{content}
+ } + return (
@@ -187,8 +194,12 @@ const chainStructuredData = generateChainStructuredData( display: grid; --doc-padding: var(--space-10x); padding: var(--doc-padding) var(--space-8x); - grid-template-columns: 1fr 1fr; - gap: var(--space-24x); + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: var(--space-8x); + } + + .layout > div { + min-width: 0; } .networks__grid { diff --git a/src/components/CCIP/Chain/ChainTokenGrid.css b/src/components/CCIP/Chain/ChainTokenGrid.css index 9f61a1e8472..aa0bcdbcc86 100644 --- a/src/components/CCIP/Chain/ChainTokenGrid.css +++ b/src/components/CCIP/Chain/ChainTokenGrid.css @@ -6,7 +6,6 @@ @media (min-width: 992px) { .tokens__grid { - min-height: 420px; grid-template-columns: 1fr 1fr 1fr 1fr; gap: var(--space-4x); } diff --git a/src/components/CCIP/Chain/ChainTokenGrid.tsx b/src/components/CCIP/Chain/ChainTokenGrid.tsx index fd267bcfd56..1459faf37f4 100644 --- a/src/components/CCIP/Chain/ChainTokenGrid.tsx +++ b/src/components/CCIP/Chain/ChainTokenGrid.tsx @@ -36,6 +36,7 @@ function ChainTokenGrid({ tokens, network, environment }: ChainTokenGridProps) { id={token.id} logo={token.logo} key={token.id} + variant="square" onClick={() => { const selectedNetwork = Object.keys(data) .map((key) => { diff --git a/src/components/CCIP/ChainHero/ChainHero.tsx b/src/components/CCIP/ChainHero/ChainHero.tsx index 1b0550380ee..bc4b18ab7c6 100644 --- a/src/components/CCIP/ChainHero/ChainHero.tsx +++ b/src/components/CCIP/ChainHero/ChainHero.tsx @@ -46,6 +46,13 @@ interface ChainHeroProps { } lane: LaneConfig }[] + verifiers?: { + id: string + name: string + type: string + logo: string + totalNetworks: number + }[] network?: Network token?: { id: string @@ -60,7 +67,16 @@ interface ChainHeroProps { }> } -function ChainHero({ chains, tokens, network, token, environment, lanes, breadcrumbItems }: ChainHeroProps) { +function ChainHero({ + chains, + tokens, + network, + token, + environment, + lanes, + verifiers = [], + breadcrumbItems, +}: ChainHeroProps) { // Get chain-specific tooltip configuration const chainTooltipConfig = network?.chain ? getChainTooltip(network.chain) : null @@ -119,7 +135,14 @@ function ChainHero({ chains, tokens, network, token, environment, lanes, breadcr } />
- +
diff --git a/src/components/CCIP/Drawer/LaneDrawer.tsx b/src/components/CCIP/Drawer/LaneDrawer.tsx index 126f230033e..693277167cf 100644 --- a/src/components/CCIP/Drawer/LaneDrawer.tsx +++ b/src/components/CCIP/Drawer/LaneDrawer.tsx @@ -1,14 +1,17 @@ import Address from "~/components/AddressReact.tsx" import "../Tables/Table.css" -import { Environment, LaneConfig, LaneFilter, Version } from "~/config/data/ccip/types.ts" -import { getNetwork, getTokenData } from "~/config/data/ccip/data.ts" +import { Environment, LaneConfig, LaneFilter } from "~/config/data/ccip/types.ts" +import { getNetwork } from "~/config/data/ccip/data.ts" import { determineTokenMechanism } from "~/config/data/ccip/utils.ts" import { useState } from "react" import LaneDetailsHero from "../ChainHero/LaneDetailsHero.tsx" -import { getExplorerAddressUrl, getTokenIconUrl, fallbackTokenIconUrl } from "~/features/utils/index.ts" +import { getExplorerAddressUrl, fallbackTokenIconUrl } from "~/features/utils/index.ts" import TableSearchInput from "../Tables/TableSearchInput.tsx" import { Tooltip } from "~/features/common/Tooltip/Tooltip.tsx" import { ChainType, ExplorerInfo } from "@config/types.ts" +import { useTokenRateLimits } from "~/hooks/useTokenRateLimits.ts" +import { RateLimitCell } from "~/components/CCIP/RateLimitCell.tsx" +import { useLaneTokens } from "~/hooks/useLaneTokens.ts" function LaneDrawer({ lane, @@ -26,6 +29,7 @@ function LaneDrawer({ inOutbound: LaneFilter }) { const [search, setSearch] = useState("") + const destinationNetworkDetails = getNetwork({ filter: environment, chain: destinationNetwork.key, @@ -36,6 +40,22 @@ function LaneDrawer({ chain: sourceNetwork.key, }) + // Determine source and destination based on inOutbound filter + const source = inOutbound === LaneFilter.Outbound ? sourceNetwork.key : destinationNetwork.key + const destination = inOutbound === LaneFilter.Outbound ? destinationNetwork.key : sourceNetwork.key + + // Fetch rate limits data using custom hook + const { rateLimits, isLoading: isLoadingRateLimits } = useTokenRateLimits(source, destination, environment) + + // Process tokens with hook + const { tokens: processedTokens, count: tokenCount } = useLaneTokens({ + tokens: lane.supportedTokens, + environment, + rateLimitsData: rateLimits, + inOutbound, + searchQuery: search, + }) + return ( <>

Lane Details

@@ -62,7 +82,7 @@ function LaneDrawer({
- Tokens ({lane?.supportedTokens ? lane.supportedTokens.length : 0}) + Tokens ({tokenCount})
@@ -172,99 +192,79 @@ function LaneDrawer({ - {lane.supportedTokens && - lane.supportedTokens - .filter((token) => token.toLowerCase().includes(search.toLowerCase())) - .map((token, index) => { - const data = getTokenData({ - environment, - version: Version.V1_2_0, - tokenId: token || "", - }) - if (!Object.keys(data).length) return null - const logo = getTokenIconUrl(token) - - // TODO: Fetch rate limits from API for both inbound and outbound - // Token pause detection requires rate limiter data from API - // A token is paused when rate limit capacity is 0 - const tokenPaused = false - - return ( - - - -
- {`${token} { - currentTarget.onerror = null // prevents looping - currentTarget.src = fallbackTokenIconUrl - }} - /> - {token} - {tokenPaused && ( - - ⏸️ - - )} -
-
- - -
- - {data[sourceNetwork.key].decimals} - - {inOutbound === LaneFilter.Outbound - ? determineTokenMechanism( - data[sourceNetwork.key].pool.type, - data[destinationNetwork.key].pool.type - ) - : determineTokenMechanism( - data[destinationNetwork.key].pool.type, - data[sourceNetwork.key].pool.type - )} - + {processedTokens.map((token, index) => ( + + + +
+ {`${token.id} { + currentTarget.onerror = null // prevents looping + currentTarget.src = fallbackTokenIconUrl + }} + /> + {token.id} + {token.isPaused && ( + + ⏸️ + + )} +
+
+ + +
+ + {token.data[sourceNetwork.key].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 + )} + - - {/* TODO: Fetch rate limits from API for both inbound and outbound - GET /api/ccip/v1/lanes/by-internal-id/{source}/{destination}/supported-tokens?environment={environment} - Response will contain both standard and custom rate limits per token */} - Disabled - - - {/* TODO: Fetch rate limits from API for both inbound and outbound - Display refill rate from standard.in/out or custom.in/out based on inOutbound filter */} - Disabled - - - {/* Placeholder for FTF Rate limit capacity - data not yet available */} - TBC - - - {/* Placeholder for FTF Rate limit refill rate - data not yet available */} - TBC - - - ) - })} + + + + + + + + + + + + + + ))}
-
- {lane.supportedTokens && - lane.supportedTokens.filter((token) => token.toLowerCase().includes(search.toLowerCase())).length === 0 && ( - <>No tokens found - )} -
+
{processedTokens.length === 0 && <>No tokens found}
) diff --git a/src/components/CCIP/Drawer/TokenDrawer.tsx b/src/components/CCIP/Drawer/TokenDrawer.tsx index e65ff4f65f9..19d31f1cbbe 100644 --- a/src/components/CCIP/Drawer/TokenDrawer.tsx +++ b/src/components/CCIP/Drawer/TokenDrawer.tsx @@ -12,13 +12,26 @@ import { PoolType, getTokenData, LaneConfig, + getVerifiersByNetwork, + getVerifierTypeDisplay, } from "~/config/data/ccip/index.ts" -import { useState } from "react" +import { useState, useMemo } from "react" import { ChainType, ExplorerInfo, SupportedChain } from "~/config/index.ts" +import { getExplorerAddressUrl } from "~/features/utils/index.ts" +import Address from "~/components/AddressReact.tsx" import LaneDrawer from "../Drawer/LaneDrawer.tsx" import TableSearchInput from "../Tables/TableSearchInput.tsx" import Tabs from "../Tables/Tabs.tsx" import { Tooltip } from "~/features/common/Tooltip/Tooltip.tsx" +import { useMultiLaneRateLimits } from "~/hooks/useMultiLaneRateLimits.ts" +import { RateLimitCell } from "~/components/CCIP/RateLimitCell.tsx" +import { realtimeDataService } from "~/lib/ccip/services/realtime-data-instance.ts" + +enum TokenTab { + Outbound = "outbound", + Inbound = "inbound", + Verifiers = "verifiers", +} function TokenDrawer({ token, @@ -53,7 +66,26 @@ function TokenDrawer({ environment: Environment }) { const [search, setSearch] = useState("") - const [inOutbound, setInOutbound] = useState(LaneFilter.Outbound) + const [activeTab, setActiveTab] = useState(TokenTab.Outbound) + + // Get verifiers for the current network + const verifiers = getVerifiersByNetwork({ + networkId: network.key, + environment, + version: Version.V1_2_0, + }) + + // Filter verifiers based on search + const filteredVerifiers = useMemo(() => { + if (!search) return verifiers + const searchLower = search.toLowerCase() + return verifiers.filter( + (verifier) => + verifier.name.toLowerCase().includes(searchLower) || + verifier.address.toLowerCase().includes(searchLower) || + verifier.type.toLowerCase().includes(searchLower) + ) + }, [verifiers, search]) type LaneRow = { networkDetails: { @@ -65,6 +97,17 @@ function TokenDrawer({ destinationPoolType: PoolType } + // Build lane configurations for fetching rate limits + const laneConfigs = useMemo(() => { + return Object.keys(destinationLanes).map((destinationChain) => ({ + source: activeTab === TokenTab.Outbound ? network.key : destinationChain, + destination: activeTab === TokenTab.Outbound ? destinationChain : network.key, + })) + }, [destinationLanes, network.key, activeTab]) + + // Fetch rate limits for all lanes using custom hook + const { rateLimitsMap, isLoading: isLoadingRateLimits } = useMultiLaneRateLimits(laneConfigs, environment) + const laneRows: LaneRow[] = Object.keys(destinationLanes) .map((destinationChain) => { const networkDetails = getNetwork({ @@ -138,141 +181,212 @@ function TokenDrawer({ tabs={[ { name: "Outbound lanes", - key: LaneFilter.Outbound, + key: TokenTab.Outbound, }, { name: "Inbound lanes", - key: LaneFilter.Inbound, + key: TokenTab.Inbound, + }, + { + name: "Verifiers", + key: TokenTab.Verifiers, }, ]} - onChange={(key) => setInOutbound(key as LaneFilter)} + onChange={(key) => setActiveTab(key as TokenTab)} /> -
- {" "} - - - - - - - - {/* */} - - - - {laneRows - ?.filter( - ({ networkDetails }) => - networkDetails && networkDetails.name.toLowerCase().includes(search.toLowerCase()) - ) - .map(({ networkDetails, laneData, destinationChain, destinationPoolType }) => { - if (!laneData || !networkDetails) return null - - // TODO: Fetch rate limits from API for both inbound and outbound - // Token pause detection requires rate limiter data from API - // A token is paused when rate limit capacity is 0 - const tokenPaused = false - - return ( - + {activeTab === TokenTab.Verifiers ? ( +
+
{inOutbound === LaneFilter.Inbound ? "Source" : "Destination"} network - Rate limit capacity - - - Rate limit refill rate - - - Mechanism - - Status
+ + + + + + + + + + {verifiers.length === 0 ? ( + + + + ) : ( + filteredVerifiers.map((verifier) => ( + - - - {/* + + + )) + )} + +
VerifierVerifier addressVerifier typeThreshold amount
+ No verifiers configured +
- - - {/* TODO: Fetch rate limits from API for both inbound and outbound - GET /api/ccip/v1/lanes/by-internal-id/{source}/{destination}/supported-tokens?environment={environment} - Response will contain both standard and custom rate limits per token */} - Disabled + {verifier.name} + - {/* TODO: Fetch rate limits from API for both inbound and outbound - Display refill rate from standard.in/out or custom.in/out based on inOutbound filter */} - Disabled +
- {inOutbound === LaneFilter.Outbound - ? determineTokenMechanism(network.tokenPoolType, destinationPoolType) - : determineTokenMechanism(destinationPoolType, network.tokenPoolType)} - + {getVerifierTypeDisplay(verifier.type)}N/A
+
+ ) : ( +
+ {" "} + + + + + + + + + + {/* */} + + + + {laneRows + ?.filter( + ({ networkDetails }) => + networkDetails && networkDetails.name.toLowerCase().includes(search.toLowerCase()) + ) + .map(({ networkDetails, laneData, destinationChain, destinationPoolType }) => { + if (!laneData || !networkDetails) return null + + // Get rate limit data for this lane + const source = activeTab === TokenTab.Outbound ? network.key : destinationChain + const destination = activeTab === TokenTab.Outbound ? destinationChain : network.key + const laneKey = `${source}-${destination}` + const laneRateLimits = rateLimitsMap[laneKey] + const tokenRateLimits = laneRateLimits?.[token.id] + + const direction = activeTab === TokenTab.Outbound ? "out" : "in" + + // Get standard and FTF rate limits + const allLimits = realtimeDataService.getAllRateLimitsForDirection(tokenRateLimits, direction) + + // Token is paused if standard rate limit capacity is 0 + const tokenPaused = allLimits.standard?.capacity === "0" + + return ( + + + + + + + + {/* */} - - ) - })} - -
{activeTab === TokenTab.Inbound ? "Source" : "Destination"} network + Rate limit capacity + + + Rate limit refill rate + + FTF Rate limit capacityFTF Rate limit refill rate + Mechanism + + Status
+ + + + + + + + + + + {activeTab === TokenTab.Outbound + ? determineTokenMechanism(network.tokenPoolType, destinationPoolType) + : determineTokenMechanism(destinationPoolType, network.tokenPoolType)} +
-
+ + ) + })} + + + + )} -
- {laneRows?.filter( - ({ networkDetails }) => networkDetails && networkDetails.name.toLowerCase().includes(search.toLowerCase()) - ).length === 0 && <>No lanes found} -
+ {activeTab !== TokenTab.Verifiers && ( +
+ {laneRows?.filter( + ({ networkDetails }) => networkDetails && networkDetails.name.toLowerCase().includes(search.toLowerCase()) + ).length === 0 && <>No lanes found} +
+ )} ) diff --git a/src/components/CCIP/Hero/Hero.tsx b/src/components/CCIP/Hero/Hero.tsx index 5f479f22828..2886a4c2a95 100644 --- a/src/components/CCIP/Hero/Hero.tsx +++ b/src/components/CCIP/Hero/Hero.tsx @@ -33,17 +33,24 @@ interface HeroProps { } lane: LaneConfig }[] + verifiers?: { + id: string + name: string + type: string + logo: string + totalNetworks: number + }[] environment: Environment } -function Hero({ chains, tokens, environment, lanes }: HeroProps) { +function Hero({ chains, tokens, environment, lanes, verifiers = [] }: HeroProps) { return (
CCIP Directory - +
) diff --git a/src/components/CCIP/Landing/ccip-landing.astro b/src/components/CCIP/Landing/ccip-landing.astro index 4e101df4cbe..f65de34c258 100644 --- a/src/components/CCIP/Landing/ccip-landing.astro +++ b/src/components/CCIP/Landing/ccip-landing.astro @@ -72,7 +72,14 @@ const directoryStructuredData = generateDirectoryStructuredData(environment, net }} suppressDefaultStructuredData={true} > - +
diff --git a/src/components/CCIP/RateLimitCell.tsx b/src/components/CCIP/RateLimitCell.tsx new file mode 100644 index 00000000000..61a4a8c7aa1 --- /dev/null +++ b/src/components/CCIP/RateLimitCell.tsx @@ -0,0 +1,46 @@ +import { Tooltip } from "~/features/common/Tooltip/Tooltip.tsx" +import type { RateLimiterConfig } from "~/lib/ccip/types/index.ts" +import { formatRateLimit } from "~/lib/ccip/utils/rate-limit-formatter.ts" + +interface RateLimitCellProps { + isLoading: boolean + rateLimit: RateLimiterConfig | null | undefined + type: "capacity" | "rate" + showUnavailableTooltip?: boolean +} + +/** + * Component for displaying rate limit values in table cells + * Handles loading, disabled, unavailable, and value states + */ +export function RateLimitCell({ isLoading, rateLimit, type, showUnavailableTooltip = false }: RateLimitCellProps) { + if (isLoading) { + return <>Loading... + } + + if (!rateLimit) { + if (showUnavailableTooltip) { + return ( + + Unavailable + + + ) + } + return <>N/A + } + + if (!rateLimit.isEnabled) { + return <>Disabled + } + + const value = type === "capacity" ? rateLimit.capacity : rateLimit.rate + return <>{formatRateLimit(value)} +} diff --git a/src/components/CCIP/Search/Search.tsx b/src/components/CCIP/Search/Search.tsx index b53ab4cdbfc..7d908596af7 100644 --- a/src/components/CCIP/Search/Search.tsx +++ b/src/components/CCIP/Search/Search.tsx @@ -38,11 +38,18 @@ interface SearchProps { } lane: LaneConfig }[] + verifiers?: { + id: string + name: string + type: string + logo: string + totalNetworks: number + }[] small?: boolean environment: Environment } -function Search({ chains, tokens, small, environment, lanes }: SearchProps) { +function Search({ chains, tokens, small, environment, lanes, verifiers = [] }: SearchProps) { const [search, setSearch] = useState("") const [debouncedSearch, setDebouncedSearch] = useState("") const [openSearchMenu, setOpenSearchMenu] = useState(false) @@ -50,6 +57,7 @@ function Search({ chains, tokens, small, environment, lanes }: SearchProps) { const [networksResults, setNetworksResults] = useState([]) const [tokensResults, setTokensResults] = useState([]) const [lanesResults, setLanesResults] = useState([]) + const [verifiersResults, setVerifiersResults] = useState([]) const searchRef = useRef(null) const workerRef = useRef(null) const workerReadyRef = useRef(false) @@ -59,10 +67,11 @@ function Search({ chains, tokens, small, environment, lanes }: SearchProps) { if (workerReadyRef.current || typeof window === "undefined") return workerRef.current = new Worker(new URL("~/workers/data-worker.ts", import.meta.url), { type: "module" }) workerRef.current.onmessage = (event: MessageEvent) => { - const { networks, tokens: workerTokens, lanes: workerLanes } = event.data + const { networks, tokens: workerTokens, lanes: workerLanes, verifiers: workerVerifiers } = event.data setNetworksResults(networks || []) setTokensResults(workerTokens || []) setLanesResults(workerLanes || []) + setVerifiersResults(workerVerifiers || []) } workerReadyRef.current = true } @@ -90,6 +99,7 @@ function Search({ chains, tokens, small, environment, lanes }: SearchProps) { setNetworksResults([]) setTokensResults([]) setLanesResults([]) + setVerifiersResults([]) return } @@ -102,11 +112,12 @@ function Search({ chains, tokens, small, environment, lanes }: SearchProps) { chains, tokens, lanes, + verifiers, }, } workerRef.current.postMessage(message) } - }, [debouncedSearch, chains, tokens, lanes]) + }, [debouncedSearch, chains, tokens, lanes, verifiers]) // Handle menu visibility useEffect(() => { @@ -146,7 +157,7 @@ function Search({ chains, tokens, small, environment, lanes }: SearchProps) { Search icon setSearch(e.target.value)} onFocus={() => { @@ -154,7 +165,7 @@ function Search({ chains, tokens, small, environment, lanes }: SearchProps) { ensureWorker() }} onBlur={() => setIsActive(false)} - aria-label="Search networks, tokens, and lanes" + aria-label="Search networks, tokens, lanes, and verifiers" aria-describedby={openSearchMenu ? "search-results" : undefined} /> {openSearchMenu && ( @@ -167,9 +178,12 @@ function Search({ chains, tokens, small, environment, lanes }: SearchProps) { aria-live="polite" aria-label="Search results" > - {networksResults.length === 0 && tokensResults.length === 0 && ( - No results found - )} + {networksResults.length === 0 && + tokensResults.length === 0 && + lanesResults.length === 0 && + verifiersResults.length === 0 && ( + No results found + )} {networksResults.length > 0 && ( <> Networks @@ -284,6 +298,36 @@ function Search({ chains, tokens, small, environment, lanes }: SearchProps) { )} + + {verifiersResults.length > 0 && ( + <> + Verifiers + + + )}
)}
diff --git a/src/components/CCIP/Tables/TokenChainsTable.tsx b/src/components/CCIP/Tables/TokenChainsTable.tsx index 5cc4c24778b..7614b0b32d4 100644 --- a/src/components/CCIP/Tables/TokenChainsTable.tsx +++ b/src/components/CCIP/Tables/TokenChainsTable.tsx @@ -8,6 +8,8 @@ import TableSearchInput from "./TableSearchInput.tsx" import { useState } from "react" import { getExplorerAddressUrl, fallbackTokenIconUrl } from "~/features/utils/index.ts" import TokenDrawer from "../Drawer/TokenDrawer.tsx" +import { Tooltip } from "~/features/common/Tooltip/Tooltip.tsx" +import { useTokenFinality } from "~/hooks/useTokenFinality.ts" interface TableProps { networks: { @@ -42,6 +44,10 @@ interface TableProps { function TokenChainsTable({ networks, token, lanes, environment }: TableProps) { const [search, setSearch] = useState("") + + // Fetch finality data using custom hook + const { finalityData, isLoading: loading } = useTokenFinality(token.id, environment, "internal_id") + return ( <>
@@ -145,15 +151,38 @@ function TokenChainsTable({ networks, token, lanes, environment }: TableProps) { {network.tokenPoolVersion} - {/* TODO: Fetch from API - GET /api/ccip/v1/tokens/{tokenCanonicalSymbol}/finality?environment={environment} - Custom finality is derived from minBlockConfirmation > 0 - Display: "Yes" | "No" | "N/A" (with tooltip for unavailable) */} - - + {loading ? ( + "-" + ) : finalityData[network.key] ? ( + finalityData[network.key].hasCustomFinality === null ? ( + + ) : finalityData[network.key].hasCustomFinality ? ( + "Yes" + ) : ( + "No" + ) + ) : ( + + )} - {/* TODO: Fetch from API - GET /api/ccip/v1/tokens/{tokenCanonicalSymbol}/finality?environment={environment} - Display minBlockConfirmation value or "-" if custom finality is disabled/unavailable */} - - + {loading + ? "-" + : finalityData[network.key] + ? finalityData[network.key].minBlockConfirmation === null + ? "-" + : finalityData[network.key].minBlockConfirmation + : "-"} ) diff --git a/src/components/CCIP/Token/Token.astro b/src/components/CCIP/Token/Token.astro index 7d82846121e..874fbdc18f2 100644 --- a/src/components/CCIP/Token/Token.astro +++ b/src/components/CCIP/Token/Token.astro @@ -5,6 +5,7 @@ import { getAllNetworks, getAllSupportedTokens, getAllTokenLanes, + getAllUniqueVerifiers, getChainsOfToken, getSearchLanes, getTokenData, @@ -76,6 +77,11 @@ const tokenLanes = getAllTokenLanes({ const searchLanes = getSearchLanes({ environment }) +const allVerifiers = getAllUniqueVerifiers({ + environment, + version: Version.V1_2_0, +}) + // Generate dynamic metadata for this specific token const environmentText = environment === Environment.Mainnet ? "Mainnet" : "Testnet" const tokenMetadata = { @@ -117,6 +123,7 @@ const tokenStructuredData = generateTokenStructuredData(token, environment, chai tokens={allTokens} client:load lanes={searchLanes} + verifiers={allVerifiers} token={{ id: token, name: data[firstSupportedChain]?.name || "", diff --git a/src/components/CCIP/Verifiers/Verifiers.astro b/src/components/CCIP/Verifiers/Verifiers.astro index 2672f983332..dd3dc730de4 100644 --- a/src/components/CCIP/Verifiers/Verifiers.astro +++ b/src/components/CCIP/Verifiers/Verifiers.astro @@ -1,11 +1,20 @@ --- import CcipDirectoryLayout from "~/layouts/CcipDirectoryLayout.astro" import { getEntry, render } from "astro:content" -import { getAllNetworks, getAllVerifiers, getSearchLanes, Version, Environment } from "~/config/data/ccip" +import { + getAllNetworks, + getAllVerifiers, + getSearchLanes, + Version, + Environment, + getAllSupportedTokens, + getChainsOfToken, +} from "~/config/data/ccip" import Table from "~/components/CCIP/Tables/VerifiersTable" import { getAllUniqueVerifiers } from "~/config/data/ccip/data.ts" import { DOCS_BASE_URL } from "~/utils/structuredData" import ChainHero from "~/components/CCIP/ChainHero/ChainHero" +import { getTokenIconUrl } from "~/features/utils" import "./Verifiers.css" interface Props { @@ -35,6 +44,20 @@ const uniqueVerifiers = getAllUniqueVerifiers({ const searchLanes = getSearchLanes({ environment }) +const supportedTokens = getAllSupportedTokens({ + environment, + version: Version.V1_2_0, +}) +const tokens = Object.keys(supportedTokens).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" })) +const allTokens = tokens.map((token) => { + const logo = getTokenIconUrl(token) || "" + return { + id: token, + logo, + totalNetworks: getChainsOfToken({ token, filter: environment }).length, + } +}) + // Generate dynamic metadata for verifiers page const environmentText = environment === Environment.Mainnet ? "Mainnet" : "Testnet" const verifiersMetadata = { @@ -70,12 +93,9 @@ const canonicalForJsonLd = `${DOCS_BASE_URL}${currentPath}` > ({ - id: verifier.id, - totalNetworks: verifier.totalNetworks, - logo: verifier.logo, - }))} + tokens={allTokens} lanes={searchLanes} + verifiers={uniqueVerifiers} environment={environment} breadcrumbItems={[ { diff --git a/src/hooks/useLaneTokens.ts b/src/hooks/useLaneTokens.ts new file mode 100644 index 00000000000..12fcdc3e808 --- /dev/null +++ b/src/hooks/useLaneTokens.ts @@ -0,0 +1,64 @@ +import { useMemo } from "react" +import { Environment, LaneFilter, Version } from "~/config/data/ccip/types.ts" +import { getTokenData } from "~/config/data/ccip/data.ts" +import { getTokenIconUrl } from "~/features/utils/index.ts" +import { realtimeDataService } from "~/lib/ccip/services/realtime-data-instance.ts" + +export interface ProcessedToken { + id: string + data: ReturnType + logo: string + rateLimits: { + standard: { capacity: string; rate: string; isEnabled: boolean } | null + ftf: { capacity: string; rate: string; isEnabled: boolean } | null + } + isPaused: boolean +} + +interface UseLaneTokensParams { + tokens: string[] | undefined + environment: Environment + rateLimitsData: Record + inOutbound: LaneFilter + searchQuery: string +} + +export function useLaneTokens({ tokens, environment, rateLimitsData, inOutbound, searchQuery }: UseLaneTokensParams) { + const processedTokens = useMemo(() => { + if (!tokens) return [] + + const direction = inOutbound === LaneFilter.Outbound ? "out" : "in" + + 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 logo = getTokenIconUrl(token) + const tokenRateLimits = rateLimitsData[token] + const allLimits = realtimeDataService.getAllRateLimitsForDirection(tokenRateLimits, direction) + const isPaused = allLimits.standard?.capacity === "0" + + return { + id: token, + data, + logo, + rateLimits: allLimits, + isPaused, + } + }) + .filter((token): token is ProcessedToken => token !== null) + }, [tokens, environment, rateLimitsData, inOutbound, searchQuery]) + + return { + tokens: processedTokens, + count: tokens?.length ?? 0, + } +} diff --git a/src/hooks/useMultiLaneRateLimits.ts b/src/hooks/useMultiLaneRateLimits.ts new file mode 100644 index 00000000000..2f0274e810f --- /dev/null +++ b/src/hooks/useMultiLaneRateLimits.ts @@ -0,0 +1,78 @@ +import { useState, useEffect } from "react" +import type { TokenRateLimits, Environment } from "~/lib/ccip/types/index.ts" +import { realtimeDataService } from "~/lib/ccip/services/realtime-data-instance.ts" + +interface LaneConfig { + source: string + destination: string +} + +interface UseMultiLaneRateLimitsResult { + rateLimitsMap: Record> + isLoading: boolean + error: Error | null +} + +/** + * Custom hook to fetch rate limits for multiple lanes + * Useful for components that need to display rate limits across multiple lanes + * @param lanes - Array of lane configurations with source and destination + * @param environment - Network environment (mainnet/testnet) + * @returns Map of rate limits keyed by lane (source-destination), loading state, and error state + */ +export function useMultiLaneRateLimits(lanes: LaneConfig[], environment: Environment): UseMultiLaneRateLimitsResult { + const [rateLimitsMap, setRateLimitsMap] = useState>>({}) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + let isMounted = true + + const fetchAllRateLimits = async () => { + setIsLoading(true) + setError(null) + + try { + const newRateLimits: Record> = {} + + // Fetch all lanes in parallel + const promises = lanes.map(async ({ source, destination }) => { + const laneKey = `${source}-${destination}` + const response = await realtimeDataService.getLaneSupportedTokens(source, destination, environment) + + if (response?.data) { + newRateLimits[laneKey] = response.data + } + }) + + await Promise.all(promises) + + if (isMounted) { + setRateLimitsMap(newRateLimits) + } + } catch (err) { + if (isMounted) { + console.error("Error fetching multi-lane rate limits:", err) + setError(err instanceof Error ? err : new Error("Failed to fetch rate limits")) + setRateLimitsMap({}) + } + } finally { + if (isMounted) { + setIsLoading(false) + } + } + } + + if (lanes.length > 0) { + fetchAllRateLimits() + } else { + setIsLoading(false) + } + + return () => { + isMounted = false + } + }, [lanes, environment]) + + return { rateLimitsMap, isLoading, error } +} diff --git a/src/hooks/useTokenFinality.ts b/src/hooks/useTokenFinality.ts new file mode 100644 index 00000000000..5036a3848f0 --- /dev/null +++ b/src/hooks/useTokenFinality.ts @@ -0,0 +1,66 @@ +import { useState, useEffect } from "react" +import type { TokenFinalityData, Environment, OutputKeyType } from "~/lib/ccip/types/index.ts" +import { realtimeDataService } from "~/lib/ccip/services/realtime-data-instance.ts" + +interface UseTokenFinalityResult { + finalityData: Record + isLoading: boolean + error: Error | null +} + +/** + * Custom hook to fetch token finality data across all chains + * @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 + */ +export function useTokenFinality( + tokenCanonicalSymbol: string, + environment: Environment, + outputKey?: OutputKeyType +): UseTokenFinalityResult { + const [finalityData, setFinalityData] = useState>({}) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + let isMounted = true + + const fetchFinalityData = async () => { + setIsLoading(true) + setError(null) + + try { + const result = await realtimeDataService.getTokenFinality(tokenCanonicalSymbol, environment, outputKey) + + if (isMounted) { + if (result?.data) { + setFinalityData(result.data) + } else { + console.warn("[useTokenFinality] No data received") + setFinalityData({}) + } + } + } catch (err) { + if (isMounted) { + console.error("Failed to fetch token finality data:", err) + setError(err instanceof Error ? err : new Error("Failed to fetch token finality")) + setFinalityData({}) + } + } finally { + if (isMounted) { + setIsLoading(false) + } + } + } + + fetchFinalityData() + + return () => { + isMounted = false + } + }, [tokenCanonicalSymbol, environment, outputKey]) + + return { finalityData, isLoading, error } +} diff --git a/src/hooks/useTokenRateLimits.ts b/src/hooks/useTokenRateLimits.ts new file mode 100644 index 00000000000..cbfd8ce04df --- /dev/null +++ b/src/hooks/useTokenRateLimits.ts @@ -0,0 +1,65 @@ +import { useState, useEffect } from "react" +import type { TokenRateLimits, Environment } from "~/lib/ccip/types/index.ts" +import { realtimeDataService } from "~/lib/ccip/services/realtime-data-instance.ts" + +interface UseTokenRateLimitsResult { + rateLimits: Record + isLoading: boolean + error: Error | null +} + +/** + * Custom hook to fetch token rate limits for a specific lane + * @param source - Source chain internal ID + * @param destination - Destination chain internal ID + * @param environment - Network environment (mainnet/testnet) + * @returns Rate limits data, loading state, and error state + */ +export function useTokenRateLimits( + source: string, + destination: string, + environment: Environment +): UseTokenRateLimitsResult { + const [rateLimits, setRateLimits] = useState>({}) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + let isMounted = true + + const fetchRateLimits = async () => { + setIsLoading(true) + setError(null) + + try { + const response = await realtimeDataService.getLaneSupportedTokens(source, destination, environment) + + if (isMounted) { + if (response?.data) { + setRateLimits(response.data) + } else { + setRateLimits({}) + } + } + } catch (err) { + if (isMounted) { + console.error("Error fetching rate limits:", err) + setError(err instanceof Error ? err : new Error("Failed to fetch rate limits")) + setRateLimits({}) + } + } finally { + if (isMounted) { + setIsLoading(false) + } + } + } + + fetchRateLimits() + + return () => { + isMounted = false + } + }, [source, destination, environment]) + + return { rateLimits, isLoading, error } +} diff --git a/src/lib/ccip/services/realtime-data-instance.ts b/src/lib/ccip/services/realtime-data-instance.ts new file mode 100644 index 00000000000..b1ac8df61bd --- /dev/null +++ b/src/lib/ccip/services/realtime-data-instance.ts @@ -0,0 +1,7 @@ +import { RealtimeDataService } from "./realtime-data.ts" + +/** + * Singleton instance of RealtimeDataService + * Use this shared instance across all components to avoid creating multiple instances + */ +export const realtimeDataService = new RealtimeDataService() diff --git a/src/lib/ccip/services/realtime-data.ts b/src/lib/ccip/services/realtime-data.ts new file mode 100644 index 00000000000..a0b7a84b470 --- /dev/null +++ b/src/lib/ccip/services/realtime-data.ts @@ -0,0 +1,259 @@ +import { Environment } from "~/lib/ccip/types/index.ts" +import type { + TokenRateLimits, + RateLimiterEntry, + RateLimiterConfig, + TokenFinalityData, + OutputKeyType, +} from "~/lib/ccip/types/index.ts" + +export const prerender = false + +/** + * Base URL for CCIP realtime API + * For client-side calls, use relative URLs to hit the local API endpoints + */ +const getApiBaseUrl = () => { + // In browser context, use relative URLs + if (typeof window !== "undefined") { + return "" + } + // In server context, use environment variable or default + return process.env.CCIP_REALTIME_API_BASE_URL || "https://api.ccip.chainlink.com" +} + +/** + * Response structure for lane supported tokens endpoint + */ +export interface LaneSupportedTokensResponse { + metadata: { + environment: Environment + timestamp: string + requestId: string + sourceChain: string + destinationChain: string + tokenCount: number + } + data: Record +} + +/** + * Response structure for token finality endpoint + */ +export interface TokenFinalityResponse { + metadata: { + environment: Environment + timestamp: string + requestId: string + tokenSymbol: string + chainCount: number + } + data: Record +} + +/** + * Service class for handling CCIP realtime data operations + * Provides functionality to fetch live data from the CCIP API + */ +export class RealtimeDataService { + private readonly requestId: string + + /** + * Creates a new instance of RealtimeDataService + */ + constructor() { + // Generate UUID - handle both browser and server environments + if (typeof crypto !== "undefined" && crypto.randomUUID) { + this.requestId = crypto.randomUUID() + } else { + this.requestId = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}` + } + } + + /** + * Fetches supported tokens with rate limits for a specific lane + * + * @param sourceInternalId - Source chain internal ID + * @param destinationInternalId - Destination chain internal ID + * @param environment - Network environment (mainnet/testnet) + * @returns Supported tokens with rate limits + */ + async getLaneSupportedTokens( + sourceInternalId: string, + destinationInternalId: string, + environment: Environment + ): Promise { + try { + const baseUrl = getApiBaseUrl() + const url = `${baseUrl}/api/ccip/v1/lanes/by-internal-id/${sourceInternalId}/${destinationInternalId}/supported-tokens?environment=${environment}` + + const response = await fetch(url) + + if (!response.ok) { + return null + } + + const data = await response.json() + return data + } catch (error) { + console.error("Error fetching lane supported tokens:", error) + return null + } + } + + /** + * Fetches token finality details across all chains + * + * @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 Token finality data for all chains + */ + async getTokenFinality( + tokenCanonicalSymbol: string, + environment: Environment, + outputKey?: OutputKeyType + ): Promise { + try { + const baseUrl = getApiBaseUrl() + let url = `${baseUrl}/api/ccip/v1/tokens/${tokenCanonicalSymbol}/finality?environment=${environment}` + + if (outputKey) { + url += `&output_key=${outputKey}` + } + + const response = await fetch(url) + + if (!response.ok) { + console.error("Failed to fetch token finality:", response.status) + return null + } + + const data = await response.json() + return data + } catch (error) { + console.error("Error fetching token finality:", error) + return null + } + } + + /** + * Checks if rate limiter data is unavailable (null) + * + * @param entry - Rate limiter entry to check + * @returns True if unavailable (null) + */ + isRateLimiterUnavailable(entry: RateLimiterEntry): entry is null { + return entry === null + } + + /** + * Checks if rate limiter is enabled + * + * @param config - Rate limiter configuration + * @returns True if enabled + */ + isRateLimiterEnabled(config: RateLimiterConfig): boolean { + return config.isEnabled + } + + /** + * Gets the request ID for this service instance + */ + getRequestId(): string { + return this.requestId + } + + /** + * Extracts FTF (custom) rate limit data for a specific token and direction + * + * @param tokenRateLimits - Token rate limits containing standard and custom entries + * @param direction - Direction ("in" for inbound, "out" for outbound) + * @returns FTF rate limiter config or null if unavailable + */ + getFTFRateLimit(tokenRateLimits: TokenRateLimits, direction: "in" | "out"): RateLimiterConfig | null { + if (!tokenRateLimits.custom || this.isRateLimiterUnavailable(tokenRateLimits.custom)) { + return null + } + + const customEntry = tokenRateLimits.custom + return customEntry[direction] || null + } + + /** + * Gets FTF capacity for a specific token and direction + * + * @param tokenRateLimits - Token rate limits containing standard and custom entries + * @param direction - Direction ("in" for inbound, "out" for outbound) + * @returns FTF capacity value or null if unavailable + */ + getFTFCapacity(tokenRateLimits: TokenRateLimits, direction: "in" | "out"): string | null { + const ftfLimit = this.getFTFRateLimit(tokenRateLimits, direction) + return ftfLimit?.capacity || null + } + + /** + * Gets FTF refill rate for a specific token and direction + * + * @param tokenRateLimits - Token rate limits containing standard and custom entries + * @param direction - Direction ("in" for inbound, "out" for outbound) + * @returns FTF refill rate value or null if unavailable + */ + getFTFRefillRate(tokenRateLimits: TokenRateLimits, direction: "in" | "out"): string | null { + const ftfLimit = this.getFTFRateLimit(tokenRateLimits, direction) + return ftfLimit?.rate || null + } + + /** + * Checks if FTF rate limiting is enabled for a specific token and direction + * + * @param tokenRateLimits - Token rate limits containing standard and custom entries + * @param direction - Direction ("in" for inbound, "out" for outbound) + * @returns True if FTF is enabled, false otherwise + */ + isFTFEnabled(tokenRateLimits: TokenRateLimits, direction: "in" | "out"): boolean { + const ftfLimit = this.getFTFRateLimit(tokenRateLimits, direction) + return ftfLimit?.isEnabled || false + } + + /** + * Gets both standard and FTF rate limits for a specific token and direction + * + * @param tokenRateLimits - Token rate limits containing standard and custom entries (can be null/undefined) + * @param direction - Direction ("in" for inbound, "out" for outbound) + * @returns Object containing both standard and FTF rate limits + */ + getAllRateLimitsForDirection( + tokenRateLimits: TokenRateLimits | null | undefined, + direction: "in" | "out" + ): { + standard: RateLimiterConfig | null + ftf: RateLimiterConfig | null + } { + if (!tokenRateLimits) { + return { standard: null, ftf: null } + } + + const standardLimit = + tokenRateLimits.standard && !this.isRateLimiterUnavailable(tokenRateLimits.standard) + ? tokenRateLimits.standard[direction] || null + : null + + const ftfLimit = this.getFTFRateLimit(tokenRateLimits, direction) + + return { + standard: standardLimit, + ftf: ftfLimit, + } + } + + /** + * Checks if a token has FTF rate limiting available + * + * @param tokenRateLimits - Token rate limits to check + * @returns True if FTF data is available (not null/unavailable) + */ + hasFTFRateLimits(tokenRateLimits: TokenRateLimits): boolean { + return tokenRateLimits.custom !== null && !this.isRateLimiterUnavailable(tokenRateLimits.custom) + } +} diff --git a/src/lib/ccip/utils/rate-limit-formatter.ts b/src/lib/ccip/utils/rate-limit-formatter.ts new file mode 100644 index 00000000000..d7431f39f31 --- /dev/null +++ b/src/lib/ccip/utils/rate-limit-formatter.ts @@ -0,0 +1,72 @@ +import type { RateLimiterConfig } from "~/lib/ccip/types/index.ts" + +/** + * Formats a rate limit value from wei to tokens + * @param value - Rate limit value in wei (as string) + * @returns Formatted string with proper number formatting + */ +export function formatRateLimit(value: string | null | undefined): string { + if (!value || value === "0") return "0" + + try { + // Convert from wei to tokens (divide by 1e18) + const numValue = BigInt(value) + const formatted = Number(numValue) / 1e18 + return formatted.toLocaleString(undefined, { maximumFractionDigits: 2 }) + } catch (error) { + console.error("Error formatting rate limit:", error) + return "0" + } +} + +/** + * Checks if a token is paused based on rate limit configuration + * A token is considered paused if the capacity is "0" + * @param rateLimit - Rate limiter configuration + * @returns True if token is paused + */ +export function isTokenPaused(rateLimit: RateLimiterConfig | null | undefined): boolean { + return rateLimit?.capacity === "0" +} + +/** + * Gets display value for a rate limit + * @param rateLimit - Rate limiter configuration + * @param isLoading - Whether data is still loading + * @returns Display string for the rate limit + */ +export function getRateLimitDisplay(rateLimit: RateLimiterConfig | null | undefined, isLoading: boolean): string { + if (isLoading) return "Loading..." + if (!rateLimit) return "N/A" + if (!rateLimit.isEnabled) return "Disabled" + return formatRateLimit(rateLimit.capacity) +} + +/** + * Gets display value for a rate limit capacity + * @param rateLimit - Rate limiter configuration + * @param isLoading - Whether data is still loading + * @returns Display string for capacity + */ +export function getRateLimitCapacityDisplay( + rateLimit: RateLimiterConfig | null | undefined, + isLoading: boolean +): string { + if (isLoading) return "Loading..." + if (!rateLimit) return "Unavailable" + if (!rateLimit.isEnabled) return "Disabled" + return formatRateLimit(rateLimit.capacity) +} + +/** + * Gets display value for a rate limit refill rate + * @param rateLimit - Rate limiter configuration + * @param isLoading - Whether data is still loading + * @returns Display string for refill rate + */ +export function getRateLimitRateDisplay(rateLimit: RateLimiterConfig | null | undefined, isLoading: boolean): string { + if (isLoading) return "Loading..." + if (!rateLimit) return "N/A" + if (!rateLimit.isEnabled) return "Disabled" + return formatRateLimit(rateLimit.rate) +} diff --git a/src/workers/data-worker.ts b/src/workers/data-worker.ts index ac75689d4ba..db3cf94b45f 100644 --- a/src/workers/data-worker.ts +++ b/src/workers/data-worker.ts @@ -33,6 +33,13 @@ interface SearchData { } lane: LaneConfig }> + verifiers: Array<{ + id: string + name: string + type: string + logo: string + totalNetworks: number + }> } interface WorkerMessage { @@ -44,6 +51,7 @@ interface WorkerResponse { networks: SearchData["chains"] tokens: SearchData["tokens"] lanes: SearchData["lanes"] + verifiers: SearchData["verifiers"] } self.onmessage = (event: MessageEvent) => { @@ -51,7 +59,7 @@ self.onmessage = (event: MessageEvent) => { const { search, data } = event.data if (!search || !data) { - self.postMessage({ networks: [], tokens: [], lanes: [] } as WorkerResponse) + self.postMessage({ networks: [], tokens: [], lanes: [], verifiers: [] } as WorkerResponse) return } @@ -74,7 +82,15 @@ self.onmessage = (event: MessageEvent) => { return matchesNetwork && hasTokens }) - self.postMessage({ networks, tokens, lanes } as WorkerResponse) + // Filter verifiers + const verifiers = data.verifiers.filter( + (verifier) => + verifier.name.toLowerCase().includes(searchLower) || + verifier.id.toLowerCase().includes(searchLower) || + verifier.type.toLowerCase().includes(searchLower) + ) + + self.postMessage({ networks, tokens, lanes, verifiers } as WorkerResponse) } // Export types for use in main thread