diff --git a/public/assets/icons/external-button-link.svg b/public/assets/icons/external-button-link.svg new file mode 100644 index 00000000000..d2500a01efe --- /dev/null +++ b/public/assets/icons/external-button-link.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/assets/icons/plus.svg b/public/assets/icons/plus.svg new file mode 100644 index 00000000000..53b4fe4ff2e --- /dev/null +++ b/public/assets/icons/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/CCIP/Chain/Chain.astro b/src/components/CCIP/Chain/Chain.astro index 7d421e23d16..f4f5ec766ac 100644 --- a/src/components/CCIP/Chain/Chain.astro +++ b/src/components/CCIP/Chain/Chain.astro @@ -3,6 +3,7 @@ import CcipLayout from "~/layouts/CcipLayout.astro" import { getEntry, render } from "astro:content" import { Environment, + Network, getAllNetworkLanes, getAllNetworks, getSearchLanes, @@ -16,7 +17,7 @@ import ChainTokenGrid from "./ChainTokenGrid" interface Props { environment: Environment - network: any + network: Network } const { environment, network } = Astro.props as Props @@ -79,7 +80,18 @@ const searchLanes = getSearchLanes({ environment })

Tokens ({allTokens.length})

- Add my token + { + network.chainType !== "solana" && ( + + Add + Add my token + + ) + }
diff --git a/src/components/CCIP/Chain/ChainTokenGrid.tsx b/src/components/CCIP/Chain/ChainTokenGrid.tsx index 703d27672f8..ad2502efcff 100644 --- a/src/components/CCIP/Chain/ChainTokenGrid.tsx +++ b/src/components/CCIP/Chain/ChainTokenGrid.tsx @@ -1,4 +1,4 @@ -import { Environment, Version, PoolType } from "~/config/data/ccip/types.ts" +import { Environment, Version, Network } from "~/config/data/ccip/types.ts" import { getAllTokenLanes, getTokenData } from "~/config/data/ccip/data.ts" import TokenCard from "../Cards/TokenCard.tsx" import { drawerContentStore } from "../Drawer/drawerStore.ts" @@ -7,7 +7,6 @@ import { directoryToSupportedChain, getChainIcon, getChainTypeAndFamily, getTitl import { useState } from "react" import "./ChainTokenGrid.css" import SeeMore from "../SeeMore/SeeMore.tsx" -import { ExplorerInfo } from "~/config/types.ts" interface ChainTokenGridProps { tokens: { @@ -15,20 +14,7 @@ interface ChainTokenGridProps { logo: string totalNetworks: number }[] - network: { - name: string - key: string - logo: string - tokenId: string - tokenLogo: string - tokenName: string - tokenSymbol: string - tokenDecimals: number - tokenAddress: string - tokenPoolType: PoolType - tokenPoolAddress: string - explorer: ExplorerInfo - } + network: Network environment: Environment } diff --git a/src/components/CCIP/ChainHero/ChainHero.tsx b/src/components/CCIP/ChainHero/ChainHero.tsx index 89ff9848c0b..b797482737f 100644 --- a/src/components/CCIP/ChainHero/ChainHero.tsx +++ b/src/components/CCIP/ChainHero/ChainHero.tsx @@ -1,4 +1,4 @@ -import { Environment, LaneConfig, Version } from "~/config/data/ccip/types.ts" +import { Environment, LaneConfig, Network, Version } from "~/config/data/ccip/types.ts" import { getTokenData } from "~/config/data/ccip/data.ts" import Address from "~/components/AddressReact.tsx" import Breadcrumb from "../Breadcrumb/Breadcrumb.tsx" @@ -44,35 +44,7 @@ interface ChainHeroProps { } lane: LaneConfig }[] - network?: { - name: string - logo: string - totalLanes: number - totalTokens: number - chain: string - chainType: ChainType - tokenAdminRegistry?: string - registryModule?: string - router?: { - name: string - address: string - } - explorer: ExplorerInfo - routerExplorerUrl: string - chainSelector: string - feeTokens?: string[] - nativeToken?: { - name: string - symbol: string - logo: string - } - armProxy: { - address: string - version: string - } - feeQuoter?: string - rmnPermeable?: boolean - } + network?: Network token?: { id: string name: string @@ -85,11 +57,11 @@ interface ChainHeroProps { function ChainHero({ chains, tokens, network, token, environment, lanes }: ChainHeroProps) { const feeTokensWithAddress = network?.feeTokens?.map((feeToken) => { - const logo = getTokenIconUrl(feeToken) + const logo = feeToken.logo const token = getTokenData({ environment, version: Version.V1_2_0, - tokenId: feeToken, + tokenId: feeToken.name, }) const explorer = network.explorer || {} const address = token[network.chain]?.tokenAddress @@ -113,7 +85,7 @@ function ChainHero({ chains, tokens, network, token, environment, lanes }: Chain if (!network) return // We making sure the Native Currency is not already part of the FeeToken return feeTokensWithAddress?.some((feeToken) => { - return feeToken.token.toLowerCase() === nativeCurrency?.symbol.toLowerCase() + return feeToken.token.name.toLowerCase() === nativeCurrency?.symbol.toLowerCase() }) } @@ -346,9 +318,9 @@ function ChainHero({ chains, tokens, network, token, environment, lanes }: Chain height="20px" className="ccip-chain-hero__feeTokens__item__logo" > - {token} + {token.name} -
{token}
+
{token.name}
))} diff --git a/src/components/CCIP/Tables/ChainTable.tsx b/src/components/CCIP/Tables/ChainTable.tsx index 492295373ac..c6a7cf871ff 100644 --- a/src/components/CCIP/Tables/ChainTable.tsx +++ b/src/components/CCIP/Tables/ChainTable.tsx @@ -7,9 +7,8 @@ import { getExplorerAddressUrl } from "~/features/utils/index.ts" import { drawerContentStore } from "../Drawer/drawerStore.ts" import LaneDrawer from "../Drawer/LaneDrawer.tsx" import { Environment, Version, LaneFilter } from "~/config/data/ccip/types.ts" -import { getLane, getOperationalState } from "~/config/data/ccip/data.ts" +import { getLane } from "~/config/data/ccip/data.ts" import { ExplorerInfo, SupportedChain, ChainType } from "~/config/types.ts" -import { clsx } from "~/lib/clsx/clsx.ts" import SeeMore from "../SeeMore/SeeMore.tsx" import { Tooltip } from "~/features/common/Tooltip/Tooltip.tsx" @@ -38,14 +37,12 @@ interface TableProps { explorer: ExplorerInfo } -const BEFORE_SEE_MORE = 12 // Number of networks to show before the "See more" button, 7 rows +const BEFORE_SEE_MORE = 12 function ChainTable({ lanes, explorer, sourceNetwork, environment }: TableProps) { const [inOutbound, setInOutbound] = useState(LaneFilter.Outbound) const [search, setSearch] = useState("") const [seeMore, setSeeMore] = useState(lanes.length <= BEFORE_SEE_MORE) - const [statuses, setStatuses] = useState>({}) - const [loadingStatuses, setLoadingStatuses] = useState(true) useEffect(() => { if (search.length > 0) { @@ -53,20 +50,9 @@ function ChainTable({ lanes, explorer, sourceNetwork, environment }: TableProps) } }, [search]) - useEffect(() => { - const fetchOperationalState = async (network) => { - if (network) { - const result = await getOperationalState(network) - setStatuses(result) - setLoadingStatuses(false) - } - } - fetchOperationalState(sourceNetwork.key) - }, [sourceNetwork]) - return ( <> -
+
setInOutbound(key as LaneFilter)} /> - +
- - @@ -150,7 +148,10 @@ function ChainTable({ lanes, explorer, sourceNetwork, environment }: TableProps) {network.name} - - ))} diff --git a/src/components/CCIP/Tables/Table.css b/src/components/CCIP/Tables/Table.css index cfac6cd8533..e0c6734178d 100644 --- a/src/components/CCIP/Tables/Table.css +++ b/src/components/CCIP/Tables/Table.css @@ -192,3 +192,64 @@ display: inline-block !important; vertical-align: middle !important; } + +/* ChainTable specific filter layout */ +.ccip-table__filters--chain { + flex-wrap: nowrap; +} + +.ccip-table__filters__actions { + display: flex; + align-items: center; + gap: var(--space-3x); + flex-shrink: 0; +} + +.ccip-table__filters__search-container { + max-width: 150px; + flex-shrink: 0; +} + +.ccip-table__filters__external-button { + white-space: nowrap; + display: flex; + align-items: center; + gap: var(--space-2x); +} + +.ccip-table__filters__external-icon { + width: 1em; + height: 1em; +} + +/* Responsive behavior for ChainTable */ +@media (max-width: 768px) { + .ccip-table__filters--chain { + flex-direction: column; + align-items: stretch; + gap: var(--space-3x); + } + + .ccip-table__filters__actions { + justify-content: space-between; + width: 100%; + } + + .ccip-table__filters__search-container { + min-width: 0; + max-width: none; + flex: 1; + } +} + +@media (max-width: 480px) { + .ccip-table__filters__actions { + flex-direction: column; + gap: var(--space-3x); + } + + .ccip-table__filters__external-button { + width: 100%; + justify-content: center; + } +} diff --git a/src/components/DocsNavigation/DocsNavigationDesktop/DocsNavigationDesktop.tsx b/src/components/DocsNavigation/DocsNavigationDesktop/DocsNavigationDesktop.tsx index 6d4a2482118..ac24d6ab91a 100644 --- a/src/components/DocsNavigation/DocsNavigationDesktop/DocsNavigationDesktop.tsx +++ b/src/components/DocsNavigation/DocsNavigationDesktop/DocsNavigationDesktop.tsx @@ -44,6 +44,7 @@ function DocsNavigationDesktop({ diff --git a/src/config/data/ccip/data.ts b/src/config/data/ccip/data.ts index f52c5d29527..efd6eab4dcd 100644 --- a/src/config/data/ccip/data.ts +++ b/src/config/data/ccip/data.ts @@ -21,6 +21,8 @@ import { getTitle, getChainTypeAndFamily, supportedChainToChainInRdd, + getTokenIconUrl, + getNativeCurrency, } from "@features/utils/index.ts" // For mainnet @@ -409,6 +411,8 @@ export const getAllNetworks = ({ filter }: { filter: Environment }): Network[] = const router = chains[chain].router if (!explorer) throw Error(`Explorer not found for ${supportedChain}`) const routerExplorerUrl = getExplorerAddressUrl(explorer)(router.address) + const nativeToken = getNativeCurrency(supportedChain) + if (!nativeToken) throw Error(`Native token not found for ${supportedChain}`) // Determine chain type based on chain name const { chainType } = getChainTypeAndFamily(supportedChain) @@ -428,11 +432,14 @@ export const getAllNetworks = ({ filter }: { filter: Environment }): Network[] = routerExplorerUrl, chainSelector: chains[chain].chainSelector, nativeToken: { - name: chains[chain]?.nativeToken?.name || "", - symbol: chains[chain]?.nativeToken?.symbol || "", - logo: chains[chain]?.nativeToken?.logo || "", + name: nativeToken.name, + symbol: nativeToken.symbol, + logo: getTokenIconUrl(nativeToken.symbol), }, - feeTokens: chains[chain].feeTokens, + feeTokens: chains[chain].feeTokens?.map((tokenName: string) => ({ + name: tokenName, + logo: getTokenIconUrl(tokenName), + })), armProxy: chains[chain].armProxy, feeQuoter: chainType === "solana" ? chains[chain]?.feeQuoter : undefined, rmnPermeable: chains[chain]?.rmnPermeable, @@ -667,12 +674,3 @@ export function getSearchLanes({ environment }: { environment: Environment }) { return 0 }) } - -export async function getOperationalState(chain: string) { - const url = `/api/ccip/lane-statuses?sourceNetworkId=${chain}` - const response = await fetch(url) - if (response.status !== 200) { - return {} - } - return response.json() -} diff --git a/src/features/utils/index.ts b/src/features/utils/index.ts index e3ee0e61116..85652327c5d 100644 --- a/src/features/utils/index.ts +++ b/src/features/utils/index.ts @@ -133,7 +133,7 @@ const transformTokenName = (token: string): string => { } export const getTokenIconUrl = (token: string) => { - if (!token) return + if (!token) return "" return `https://d2f70xi62kby8n.cloudfront.net/tokens/${transformTokenName(token)}.webp?auto=compress%2Cformat` } diff --git a/src/pages/api/ccip/lane-statuses.ts b/src/pages/api/ccip/lane-statuses.ts deleted file mode 100644 index 50bd6cc888c..00000000000 --- a/src/pages/api/ccip/lane-statuses.ts +++ /dev/null @@ -1,318 +0,0 @@ -import type { APIRoute } from "astro" -import { client } from "@graphql/graphqlClient.ts" -import { - LaneStatusesFilteredDocument, - LaneStatusesFilteredQuery, - LaneStatusesFilteredQueryVariables, -} from "@graphql/generated.ts" -import { - commonHeaders, - getEnvironmentAndConfig, - resolveChainOrThrow, - checkIfChainIsCursed, - withTimeout, - structuredLog, - LogLevel, -} from "./utils.ts" -import { ChainType, SupportedChain } from "@config/index.ts" -import { getProviderForChain } from "@config/web3Providers.ts" -import { Environment, getSelectorEntry, LaneStatus } from "@config/data/ccip/index.ts" -import { getChainId, getChainTypeAndFamily } from "@features/utils/index.ts" - -export const prerender = false -const timeoutCurseCheck = 10000 - -export const GET: APIRoute = async ({ request }) => { - try { - const url = new URL(request.url) - const sourceNetworkId = url.searchParams.get("sourceNetworkId") - const requestId = request.headers.get("x-request-id") || "unknown" - - structuredLog(LogLevel.INFO, { - message: "Fetching lane statuses", - requestId, - sourceNetworkId, - }) - - // Validate required parameters - if (!sourceNetworkId) { - return new Response( - JSON.stringify({ - errorType: "MissingParameters", - errorMessage: "Missing required parameters: sourceNetworkId is required.", - }), - { status: 400, headers: commonHeaders } - ) - } - - // Determine the environment and load the configuration - const envConfig = getEnvironmentAndConfig(sourceNetworkId) - if (!envConfig) { - structuredLog(LogLevel.ERROR, { - message: "Invalid source network ID", - requestId, - sourceNetworkId, - }) - return new Response( - JSON.stringify({ - errorType: "InvalidNetwork", - errorMessage: `Invalid source network ID: ${sourceNetworkId}`, - }), - { status: 400, headers: commonHeaders } - ) - } - - const { environment, chainsConfig, sourceRouterAddress, destinationNetworkIds } = envConfig - - // Resolve the source chain - let sourceChain: SupportedChain - let sourceChainAtlas: string - let sourceChainType: ChainType - try { - sourceChain = resolveChainOrThrow(sourceNetworkId) - const { chainType } = getChainTypeAndFamily(sourceChain) - sourceChainType = chainType - const sourceChainId = getChainId(sourceChain) - sourceChainAtlas = sourceChainId ? getSelectorEntry(sourceChainId, chainType)?.name || "" : "" - } catch (error) { - structuredLog(LogLevel.ERROR, { - message: "Error resolving source chain", - requestId, - sourceNetworkId, - error: error instanceof Error ? error.message : String(error), - }) - return new Response( - JSON.stringify({ - errorType: "InvalidNetwork", - errorMessage: error.message, - }), - { status: 500, headers: commonHeaders } - ) - } - - // Check if the source chain is cursed - let isSourceChainCursed = false - if (sourceChainType === "evm") { - try { - const sourceProvider = getProviderForChain(sourceChain) - isSourceChainCursed = await withTimeout( - checkIfChainIsCursed(sourceProvider, sourceChain, sourceRouterAddress), - timeoutCurseCheck, - `Timeout while checking if source chain ${sourceChain} is cursed` - ) - } catch (error) { - structuredLog(LogLevel.ERROR, { - message: "Error checking if source chain is cursed", - requestId, - sourceChain, - error: error instanceof Error ? error.message : String(error), - }) - // Continue execution instead of returning 500 - } - } - - const statuses: Record = {} - const failedCurseChecks: string[] = [] - - if (isSourceChainCursed) { - destinationNetworkIds.forEach((id) => { - statuses[id] = LaneStatus.CURSED - }) - return new Response(JSON.stringify(statuses), { - status: 200, - headers: { - ...commonHeaders, - "Cache-Control": "s-max-age=300, stale-while-revalidate", - "CDN-Cache-Control": "max-age=300", - "Vercel-CDN-Cache-Control": "max-age=300", - }, - }) - } - - const validDestinationNetworkIds: string[] = [] - const destinationChecks: Promise[] = [] - - const atlasNameToIdMap: Record = {} - - for (const id of destinationNetworkIds) { - const destinationCheck = async () => { - try { - const destinationChain = resolveChainOrThrow(id) - const { chainType: destinationChainType } = getChainTypeAndFamily(destinationChain) - const destinationChainId = getChainId(destinationChain) - const destinationChainAtlas = destinationChainId - ? getSelectorEntry(destinationChainId, destinationChainType)?.name || "" - : "" - - atlasNameToIdMap[destinationChainAtlas] = id - - // Attempt to get the provider and check if the chain is cursed - if (destinationChainType === "evm") { - try { - const provider = getProviderForChain(destinationChain) - const destinationRouterAddress = chainsConfig[id].router.address - - const isDestinationCursed = await withTimeout( - checkIfChainIsCursed(provider, destinationChain, destinationRouterAddress), - timeoutCurseCheck, - `Timeout while checking if destination chain ${destinationChain} is cursed` - ) - - if (isDestinationCursed) { - statuses[id] = LaneStatus.CURSED - } else { - validDestinationNetworkIds.push(destinationChainAtlas) // Push if no curse detected - } - } catch (innerError) { - console.error( - `Error during provider resolution or curse check for destination network ID ${id}:`, - innerError - ) - failedCurseChecks.push(id) // Track failed curse checks - validDestinationNetworkIds.push(destinationChainAtlas) // Push if curse check fails - } - } - } catch (outerError) { - console.error(`Error resolving destination chain or mapping to atlas for network ID ${id}:`, outerError) - } - } - - destinationChecks.push(destinationCheck()) - } - - await Promise.all(destinationChecks) - - if (validDestinationNetworkIds.length === 0) { - return new Response(JSON.stringify(statuses), { - status: 200, - headers: { - ...commonHeaders, - "Cache-Control": "s-max-age=300, stale-while-revalidate", - "CDN-Cache-Control": "max-age=300", - "Vercel-CDN-Cache-Control": "max-age=300", - }, - }) - } - const variables: LaneStatusesFilteredQueryVariables = { - sourceRouterAddress: sourceRouterAddress.toLowerCase(), - sourceNetworkId: sourceChainAtlas, - destinationNetworkIds: validDestinationNetworkIds, - } - - const response = await client.query({ - query: LaneStatusesFilteredDocument, - variables, - }) - - const graphqlReturnedNetworkNames = response.data.allCcipAllLaneStatuses?.nodes.map((node) => node.destNetworkName) - const missingFromGraphQL = validDestinationNetworkIds.filter( - (network) => !graphqlReturnedNetworkNames?.includes(network) - ) - - if (failedCurseChecks.length > 0) { - structuredLog(LogLevel.WARN, { - message: "Curse check failed for destination chains", - requestId, - failedChains: failedCurseChecks, - }) - } - - if (missingFromGraphQL.length > 0) { - structuredLog(LogLevel.WARN, { - message: "Destination chains missing from GraphQL response", - requestId, - missingChains: missingFromGraphQL, - }) - - // Add missing networks as OPERATIONAL by default - missingFromGraphQL.forEach((network) => { - const networkId = atlasNameToIdMap[network] - if (networkId) { - statuses[networkId] = LaneStatus.OPERATIONAL - } - }) - } - - if (response.data.allCcipAllLaneStatuses?.nodes.length) { - for (const node of response.data.allCcipAllLaneStatuses.nodes) { - let status = LaneStatus.OPERATIONAL - - if (node.successRate === 0) { - const newStatus = environment === Environment.Testnet ? LaneStatus.MAINTENANCE : LaneStatus.DEGRADED - status = newStatus - structuredLog(LogLevel.WARN, { - message: "Lane status changed due to zero success rate", - requestId, - lane: { - source: sourceChainAtlas, - destination: node.destNetworkName || "unknown", - status: newStatus, - successRate: node.successRate, - }, - }) - } - - if (node.destNetworkName) { - const destNetworkId = atlasNameToIdMap[node.destNetworkName] - if (destNetworkId) { - statuses[destNetworkId] = status - } else { - structuredLog(LogLevel.ERROR, { - message: "Could not find destination network ID for network name", - requestId, - destNetworkName: node.destNetworkName, - }) - } - } else { - structuredLog(LogLevel.ERROR, { - message: "No destination network name found for lane", - requestId, - node, - }) - } - } - return new Response(JSON.stringify(statuses), { - status: 200, - headers: { - ...commonHeaders, - "Cache-Control": "s-max-age=300, stale-while-revalidate", - "CDN-Cache-Control": "max-age=300", - "Vercel-CDN-Cache-Control": "max-age=300", - }, - }) - } else { - structuredLog(LogLevel.WARN, { - message: "No lane statuses found", - requestId, - sourceNetworkId, - destinationNetworkIds, - }) - destinationNetworkIds.forEach((id) => { - statuses[id] = LaneStatus.OPERATIONAL - }) - - return new Response(JSON.stringify(statuses), { - status: 200, - headers: { - ...commonHeaders, - "Cache-Control": "s-max-age=300, stale-while-revalidate", - "CDN-Cache-Control": "max-age=300", - "Vercel-CDN-Cache-Control": "max-age=300", - }, - }) - } - } catch (error) { - structuredLog(LogLevel.ERROR, { - message: "Error fetching lane statuses", - requestId: request.headers.get("x-request-id") || "unknown", - error: error instanceof Error ? error.message : String(error), - }) - return new Response( - JSON.stringify({ - errorType: "ServerError", - errorMessage: "Failed to fetch lane statuses", - }), - { status: 500, headers: commonHeaders } - ) - } -} diff --git a/src/pages/ccip/directory/mainnet/chain/[...chain].astro b/src/pages/ccip/directory/mainnet/chain/[...chain].astro index dbbca92d434..461c1c2f3ab 100644 --- a/src/pages/ccip/directory/mainnet/chain/[...chain].astro +++ b/src/pages/ccip/directory/mainnet/chain/[...chain].astro @@ -1,6 +1,6 @@ --- import Chain from "~/components/CCIP/Chain/Chain.astro" -import { Environment, getAllNetworks } from "~/config/data/ccip" +import { Environment, getAllNetworks, Network } from "~/config/data/ccip" export async function getStaticPaths() { const networks = getAllNetworks({ filter: Environment.Mainnet }) @@ -10,7 +10,7 @@ export async function getStaticPaths() { return { params: { chain }, props: { - network: networks.find((network) => network.chain === chain), + network: networks.find((network) => network.chain === chain) as Network, environment: Environment.Mainnet, }, }
{inOutbound === LaneFilter.Outbound ? "Destination" : "Source"} network + {inOutbound === LaneFilter.Outbound ? ( <> OnRamp address @@ -95,13 +99,8 @@ function ChainTable({ lanes, explorer, sourceNetwork, environment }: TableProps) )} @@ -109,7 +108,6 @@ function ChainTable({ lanes, explorer, sourceNetwork, environment }: TableProps) "OffRamp address" )} Status
+
- {loadingStatuses ? ( - "Loading..." - ) : ( - - {statuses[network.key]?.toLocaleLowerCase() && ( - Cursed - )} - {statuses[network.key]?.toLocaleLowerCase() || "Status unavailable"} - - )} -