diff --git a/src/config/cdn.ts b/src/config/cdn.ts new file mode 100644 index 00000000000..43eb8fbb53c --- /dev/null +++ b/src/config/cdn.ts @@ -0,0 +1,14 @@ +/** + * CDN Base URLs and asset path configuration + * Centralized configuration for all CDN-hosted assets + */ + +// CloudFront CDN base URL +export const CLOUDFRONT_CDN_BASE = "https://d2f70xi62kby8n.cloudfront.net" + +// Asset-specific paths +export const TOKEN_ICONS_PATH = `${CLOUDFRONT_CDN_BASE}/tokens` +export const VERIFIER_LOGOS_PATH = `${CLOUDFRONT_CDN_BASE}/verifiers` + +// Other CDNs (for future centralization if needed) +export const IMGIX_CDN_BASE = "https://smartcontract.imgix.net" diff --git a/src/config/data/ccip/data.ts b/src/config/data/ccip/data.ts index 3eec6d88f9e..cfc55662854 100644 --- a/src/config/data/ccip/data.ts +++ b/src/config/data/ccip/data.ts @@ -13,9 +13,13 @@ import { Network, DecomConfig, DecommissionedNetwork, + VerifiersConfig, + Verifier, + VerifierType, } from "./types.ts" import { determineTokenMechanism } from "./utils.ts" import { ExplorerInfo, SupportedChain, ChainType } from "@config/types.ts" +import { VERIFIER_LOGOS_PATH } from "@config/cdn.ts" import { directoryToSupportedChain, getChainIcon, @@ -42,6 +46,10 @@ import tokensTestnetv120 from "@config/data/ccip/v1_2_0/testnet/tokens.json" wit import decomMainnetv120 from "@config/data/ccip/v1_2_0/mainnet/decom.json" with { type: "json" } import decomTestnetv120 from "@config/data/ccip/v1_2_0/testnet/decom.json" with { type: "json" } +// For verifiers +import verifiersMainnetv120 from "@config/data/ccip/v1_2_0/mainnet/verifiers.json" with { type: "json" } +import verifiersTestnetv120 from "@config/data/ccip/v1_2_0/testnet/verifiers.json" with { type: "json" } + // Import errors by version // eslint-disable-next-line camelcase import * as errors_v1_5_0 from "./errors/v1_5_0/index.ts" @@ -815,3 +823,233 @@ export const getDecommissionedNetwork = ({ chain, filter }: { chain: string; fil const decommissionedChains = getAllDecommissionedNetworks({ filter }) return decommissionedChains.find((network) => network.chain === chain) } + +// ============================================================================ +// Verifier utilities +// ============================================================================ + +/** + * Load verifiers data for a specific environment and version + */ +export const loadVerifiersData = ({ environment, version }: { environment: Environment; version: Version }) => { + let verifiersReferenceData: VerifiersConfig + + if (environment === Environment.Mainnet && version === Version.V1_2_0) { + verifiersReferenceData = verifiersMainnetv120 as unknown as VerifiersConfig + } else if (environment === Environment.Testnet && version === Version.V1_2_0) { + verifiersReferenceData = verifiersTestnetv120 as unknown as VerifiersConfig + } else { + throw new Error(`Invalid environment/version combination for verifiers: ${environment}/${version}`) + } + + return { verifiersReferenceData } +} + +/** + * Get logo URL for a verifier by ID + * Uses CloudFront CDN, same infrastructure as token icons + */ +export const getVerifierLogoUrl = (verifierId: string): string => { + return `${VERIFIER_LOGOS_PATH}/${verifierId}.svg` +} + +/** + * Map verifier type to display-friendly name + */ +export const getVerifierTypeDisplay = (type: VerifierType): string => { + const VERIFIER_TYPE_DISPLAY: Record = { + committee: "Committee", + api: "API", + } + + return VERIFIER_TYPE_DISPLAY[type] || type +} + +/** + * Get all verifiers for a specific environment as a flattened list + */ +export const getAllVerifiers = ({ + environment, + version = Version.V1_2_0, +}: { + environment: Environment + version?: Version +}): Verifier[] => { + const { verifiersReferenceData } = loadVerifiersData({ environment, version }) + + const verifiers: Verifier[] = [] + + // Flatten the network -> address -> metadata structure + for (const [networkId, addressMap] of Object.entries(verifiersReferenceData)) { + for (const [address, metadata] of Object.entries(addressMap)) { + verifiers.push({ + ...metadata, + network: networkId, + address, + logo: getVerifierLogoUrl(metadata.id), + }) + } + } + + // Sort by verifier name, then by network + return verifiers.sort((a, b) => { + const nameComparison = a.name.localeCompare(b.name) + if (nameComparison !== 0) return nameComparison + return a.network.localeCompare(b.network) + }) +} + +/** + * Get all verifiers for a specific network + */ +export const getVerifiersByNetwork = ({ + networkId, + environment, + version = Version.V1_2_0, +}: { + networkId: string + environment: Environment + version?: Version +}): Verifier[] => { + const { verifiersReferenceData } = loadVerifiersData({ environment, version }) + + const addressMap = verifiersReferenceData[networkId] + if (!addressMap) { + return [] + } + + const verifiers: Verifier[] = [] + for (const [address, metadata] of Object.entries(addressMap)) { + verifiers.push({ + ...metadata, + network: networkId, + address, + logo: getVerifierLogoUrl(metadata.id), + }) + } + + return verifiers.sort((a, b) => a.name.localeCompare(b.name)) +} + +/** + * Get all verifiers of a specific type (committee or api) + */ +export const getVerifiersByType = ({ + type, + environment, + version = Version.V1_2_0, +}: { + type: VerifierType + environment: Environment + version?: Version +}): Verifier[] => { + const allVerifiers = getAllVerifiers({ environment, version }) + return allVerifiers.filter((verifier) => verifier.type === type) +} + +/** + * Get all networks where a specific verifier exists (by verifier ID) + */ +export const getVerifierById = ({ + id, + environment, + version = Version.V1_2_0, +}: { + id: string + environment: Environment + version?: Version +}): Verifier[] => { + const allVerifiers = getAllVerifiers({ environment, version }) + return allVerifiers.filter((verifier) => verifier.id === id) +} + +/** + * Get a specific verifier by network and address + */ +export const getVerifier = ({ + networkId, + address, + environment, + version = Version.V1_2_0, +}: { + networkId: string + address: string + environment: Environment + version?: Version +}): Verifier | undefined => { + const { verifiersReferenceData } = loadVerifiersData({ environment, version }) + + const addressMap = verifiersReferenceData[networkId] + if (!addressMap) { + return undefined + } + + const metadata = addressMap[address] + if (!metadata) { + return undefined + } + + return { + ...metadata, + network: networkId, + address, + logo: getVerifierLogoUrl(metadata.id), + } +} + +/** + * Get all network IDs where a specific verifier exists + * Similar to getChainsOfToken for tokens + */ +export const getNetworksOfVerifier = ({ + id, + environment, + version = Version.V1_2_0, +}: { + id: string + environment: Environment + version?: Version +}): string[] => { + const verifiers = getVerifierById({ id, environment, version }) + return verifiers.map((v) => v.network) +} + +/** + * Get unique verifiers for display (deduplicated by ID) + * Returns one entry per verifier with totalNetworks count + * Useful for landing page display where each verifier should appear once + */ +export const getAllUniqueVerifiers = ({ + environment, + version = Version.V1_2_0, +}: { + environment: Environment + version?: Version +}): Array<{ + id: string + name: string + type: VerifierType + logo: string + totalNetworks: number +}> => { + const allVerifiers = getAllVerifiers({ environment, version }) + + // Get unique verifier IDs + const uniqueIds = Array.from(new Set(allVerifiers.map((v) => v.id))) + + // Map to display format with network count + return uniqueIds + .map((id) => { + const instances = allVerifiers.filter((v) => v.id === id) + const firstInstance = instances[0] + + return { + id, + name: firstInstance.name, + type: firstInstance.type, + logo: firstInstance.logo, + totalNetworks: instances.length, + } + }) + .sort((a, b) => a.name.localeCompare(b.name)) +} diff --git a/src/config/data/ccip/types.ts b/src/config/data/ccip/types.ts index e12d25d1828..2c208a4220d 100644 --- a/src/config/data/ccip/types.ts +++ b/src/config/data/ccip/types.ts @@ -232,3 +232,24 @@ export interface DecommissionedNetwork { explorer: ExplorerInfo chainType: ChainType } + +// Verifier types +export type VerifierType = "committee" | "api" + +export interface VerifierMetadata { + id: string + name: string + type: VerifierType +} + +export interface VerifiersConfig { + [networkId: string]: { + [address: string]: VerifierMetadata + } +} + +export interface Verifier extends VerifierMetadata { + network: string + address: string + logo: string +} diff --git a/src/config/data/ccip/v1_2_0/mainnet/verifiers.json b/src/config/data/ccip/v1_2_0/mainnet/verifiers.json new file mode 100644 index 00000000000..5bac359e6ed --- /dev/null +++ b/src/config/data/ccip/v1_2_0/mainnet/verifiers.json @@ -0,0 +1,60 @@ +{ + "mainnet": { + "0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D": { + "id": "chainlink", + "name": "Chainlink", + "type": "committee" + }, + "0xF4c7E640EdA248ef95972845a62bdC74237805dB": { + "id": "lombard", + "name": "Lombard", + "type": "api" + }, + "0x768a1a3B321126A8B214d7376D48465C7f6Fa061": { + "id": "cctp", + "name": "CCTP", + "type": "api" + }, + "0xcBD48A8eB077381c3c4Eb36b402d7283aB2b11Bc": { + "id": "symbiotic", + "name": "Symbiotic", + "type": "api" + } + }, + "ethereum-mainnet-base-1": { + "0x0aA145a62153190B8f0D3cA00c441e451529f755": { + "id": "chainlink-labs", + "name": "Chainlink Labs", + "type": "committee" + }, + "0x09521B0B5BB2d4406124c0207Cf551829B45f84d": { + "id": "cctp", + "name": "CCTP", + "type": "api" + } + }, + "ethereum-mainnet-arbitrum-1": { + "0xe9c6945281028cb6530d43F998eE539dFE2a9191": { + "id": "chainlink-labs", + "name": "Chainlink Labs", + "type": "committee" + }, + "0xBF38331E34ef7f248020611bB31Be0576D06413D": { + "id": "lombard", + "name": "Lombard", + "type": "api" + } + }, + "ethereum-mainnet-optimism-1": { + "0x2edAc8B8928c4e1Ed559e619b6A8a4aaCe9Ef18A": { + "id": "cctp", + "name": "CCTP", + "type": "api" + }, + "0x76Aa17dCda9E8529149E76e9ffaE4aD1C4AD701B": { + "id": "symbiotic", + "name": "Symbiotic", + "type": "api" + } + } +} diff --git a/src/config/data/ccip/v1_2_0/testnet/verifiers.json b/src/config/data/ccip/v1_2_0/testnet/verifiers.json new file mode 100644 index 00000000000..2614058b4bd --- /dev/null +++ b/src/config/data/ccip/v1_2_0/testnet/verifiers.json @@ -0,0 +1,55 @@ +{ + "ethereum-testnet-sepolia": { + "0x91339eb99C4c2Be9A071203DD99E014A3189FD29": { + "id": "chainlink", + "name": "Chainlink", + "type": "committee" + }, + "0x56c4b06A0F59AcFAAb58FEA0d7Ca4090695F683f": { + "id": "lombard", + "name": "Lombard", + "type": "api" + }, + "0x051665f2455116e929b9972c36d23070F5054Ce0": { + "id": "cctp", + "name": "CCTP", + "type": "api" + } + }, + "ethereum-testnet-sepolia-base-1": { + "0x7EEdf2DBC74924Cb1f23fC8845CD35bF18b697de": { + "id": "chainlink-labs", + "name": "Chainlink Labs", + "type": "committee" + }, + "0xD3ED6fC9fd22412764ac2Ef64fB664b9393dF9F2": { + "id": "cctp", + "name": "CCTP", + "type": "api" + } + }, + "ethereum-testnet-sepolia-arbitrum-1": { + "0xa132F089492CcE5f1D79483a9e4552f37266ed01": { + "id": "chainlink-labs", + "name": "Chainlink Labs", + "type": "committee" + }, + "0xb0B4b5847E35033766d5B49CD9C0fC40F459321F": { + "id": "lombard", + "name": "Lombard", + "type": "api" + } + }, + "ethereum-testnet-sepolia-optimism-1": { + "0x0B8B717f8D65DeC5c9e440A9eD51f48887E83c1b": { + "id": "cctp", + "name": "CCTP", + "type": "api" + }, + "0x34E63B2B9491570FCc01CC0b288569851EF47B27": { + "id": "symbiotic", + "name": "Symbiotic", + "type": "api" + } + } +} diff --git a/src/features/utils/index.ts b/src/features/utils/index.ts index 6ee29217a5f..4a473148f7d 100644 --- a/src/features/utils/index.ts +++ b/src/features/utils/index.ts @@ -9,6 +9,7 @@ import { ChainFamily, } from "@config/index.ts" import { CCIP_TOKEN_ICON_MAPPINGS } from "@config/data/ccip/tokenIconMappings.ts" +import { TOKEN_ICONS_PATH } from "@config/cdn.ts" import { toQuantity } from "ethers" import referenceChains from "src/scripts/reference/chains.json" with { type: "json" } @@ -156,10 +157,11 @@ export const getTokenIconUrl = (token: string, size = 40) => { // Request appropriately sized images from CloudFront // For 40x40 display, request 80x80 for retina displays (2x) - return `https://d2f70xi62kby8n.cloudfront.net/tokens/${transformTokenName(iconIdentifier)}.webp?auto=compress%2Cformat&q=60&w=${size}&h=${size}&fit=cover` + return `${TOKEN_ICONS_PATH}/${transformTokenName(iconIdentifier)}.webp?auto=compress%2Cformat&q=60&w=${size}&h=${size}&fit=cover` } export const fallbackTokenIconUrl = "/assets/icons/generic-token.svg" +export const fallbackVerifierIconUrl = "/assets/icons/generic-verifier.svg" export const getChainId = (supportedChain: SupportedChain) => { const technology = chainToTechnology[supportedChain] diff --git a/src/scripts/data/detect-new-data.ts b/src/scripts/data/detect-new-data.ts index 4be709817f6..fecdd6576fe 100644 --- a/src/scripts/data/detect-new-data.ts +++ b/src/scripts/data/detect-new-data.ts @@ -12,6 +12,7 @@ import fs from "fs" import path from "path" import fetch from "node-fetch" import prettier from "prettier" +import { TOKEN_ICONS_PATH } from "../../config/cdn.js" // Network endpoints mapping for different blockchain networks // Each endpoint provides a JSON file containing feed definitions for that network @@ -79,7 +80,7 @@ interface DataItem { * @returns URL to the asset's icon image */ function buildIconUrl(baseAsset: string): string { - return `https://d2f70xi62kby8n.cloudfront.net/tokens/${baseAsset.toLowerCase()}.webp` + return `${TOKEN_ICONS_PATH}/${baseAsset.toLowerCase()}.webp` } /**