{"version":3,"file":"ens-resolver.mjs","names":[],"sources":["../../../src/lib/ens-resolver.ts"],"sourcesContent":["/**\n * ENS / On-chain Identity Resolution — shared utility for all tools.\n *\n * Resolves ENS names (*.eth), Basenames (*.base.eth), and plain addresses\n * into normalized 0x addresses. Used by every tool that accepts an address\n * parameter, so users can type \"vitalik.eth\" instead of the raw address.\n *\n * Uses viem's built-in ENS support:\n * - publicClient.getEnsAddress() — name → address\n * - publicClient.getEnsName() — address → name (reverse resolution)\n * - publicClient.getEnsAvatar() — address → avatar URL\n *\n * ENS resolution requires an Ethereum mainnet client (ENS registry lives on L1).\n * For Base, we also check the Base ENS resolver for *.base.eth names.\n */\n\nimport { type Address, isAddress, getAddress } from 'viem';\n\n// ── L1 Client for ENS Resolution ────────────────────────────────────────────\n// The ENS registry lives on Ethereum L1. Standard *.eth names must be resolved\n// via an L1 client. Basenames (*.base.eth) resolve on Base via the L2 resolver.\n\n/**\n * Get the appropriate public client for ENS resolution.\n * - *.base.eth → use the provided client (expected to be a Base client)\n * - *.eth → get an Ethereum L1 client via RpcManager\n * Falls back to the provided client if L1 is unavailable.\n */\nasync function getEnsClient(name: string, fallback: any): Promise<any> {\n  // Basenames resolve on Base — use the provided client directly\n  if (name.endsWith('.base.eth')) return fallback;\n\n  // Standard *.eth names need an Ethereum L1 client\n  try {\n    const { getRpcManager } = await import('../services/rpc-provider.js');\n    const rpcManager = getRpcManager();\n    return await rpcManager.getClient(1); // Ethereum mainnet\n  } catch {\n    // L1 RPC unavailable — try the provided client anyway.\n    // This will fail for Base clients resolving .eth names,\n    // but at least gives a clear error message.\n    return fallback;\n  }\n}\n\n// ── Types ───────────────────────────────────────────────────────────────────\n\nexport interface ResolvedAddress {\n  /** Checksummed 0x address */\n  address: Address;\n  /** Original input (may be ENS name, address, etc.) */\n  input: string;\n  /** ENS name if resolved, or if reverse-resolved from address */\n  ensName?: string;\n  /** Whether the input was an ENS name that got resolved */\n  wasEnsResolution: boolean;\n}\n\nexport interface EnsProfile {\n  address: Address;\n  name: string | null;\n  avatar: string | null;\n}\n\n// ── Cache ───────────────────────────────────────────────────────────────────\n// ENS names change infrequently. Cache for 5 minutes to reduce RPC calls.\n\nconst ENS_CACHE = new Map<string, { address: Address; timestamp: number }>();\nconst CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes\n\nfunction getCached(name: string): Address | null {\n  const entry = ENS_CACHE.get(name.toLowerCase());\n  if (!entry) return null;\n  if (Date.now() - entry.timestamp > CACHE_TTL_MS) {\n    ENS_CACHE.delete(name.toLowerCase());\n    return null;\n  }\n  return entry.address;\n}\n\nfunction setCache(name: string, address: Address): void {\n  ENS_CACHE.set(name.toLowerCase(), { address, timestamp: Date.now() });\n  // Prune cache if too large\n  if (ENS_CACHE.size > 500) {\n    const oldest = Array.from(ENS_CACHE.entries())\n      .sort((a, b) => a[1].timestamp - b[1].timestamp)\n      .slice(0, 100);\n    for (const [key] of oldest) ENS_CACHE.delete(key);\n  }\n}\n\n/** Clear the ENS cache (for testing). */\nexport function clearEnsCache(): void {\n  ENS_CACHE.clear();\n}\n\n// ── Detection ───────────────────────────────────────────────────────────────\n\n/** Check if a string looks like an ENS name (*.eth, *.base.eth, etc.) */\nexport function isEnsName(input: string): boolean {\n  if (!input || typeof input !== 'string') return false;\n  const trimmed = input.trim().toLowerCase();\n  // Standard ENS: *.eth (resolved on Ethereum L1)\n  // Basenames: *.base.eth (resolved on Base L2)\n  return /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*\\.(eth|base\\.eth)$/i.test(trimmed);\n}\n\n/** Check if input is a valid hex address OR an ENS name. */\nexport function isAddressOrEns(input: string): boolean {\n  if (!input || typeof input !== 'string') return false;\n  return isAddress(input) || isEnsName(input);\n}\n\n// ── Resolution ──────────────────────────────────────────────────────────────\n\n/**\n * Resolve an address-or-ENS input to a checksummed 0x address.\n *\n * - If input is already a valid address, returns it checksummed.\n * - If input is an ENS name, resolves via the provided publicClient.\n * - Throws if resolution fails or input is invalid.\n *\n * @param input - Address string or ENS name (e.g., \"vitalik.eth\", \"0x1234...\")\n * @param publicClient - viem PublicClient (must be connected to Ethereum mainnet for ENS)\n */\nexport async function resolveAddressOrEns(\n  input: string,\n  publicClient: any, // Using any to avoid viem version conflicts\n): Promise<ResolvedAddress> {\n  const trimmed = input.trim();\n\n  // Fast path: already a valid address\n  if (isAddress(trimmed)) {\n    return {\n      address: getAddress(trimmed),\n      input: trimmed,\n      wasEnsResolution: false,\n    };\n  }\n\n  // ENS resolution\n  if (isEnsName(trimmed)) {\n    // Check cache first\n    const cached = getCached(trimmed);\n    if (cached) {\n      return {\n        address: cached,\n        input: trimmed,\n        ensName: trimmed,\n        wasEnsResolution: true,\n      };\n    }\n\n    try {\n      // Route to the correct client: L1 for *.eth, provided client for *.base.eth\n      const ensClient = await getEnsClient(trimmed, publicClient);\n      const address = await ensClient.getEnsAddress({ name: trimmed });\n      if (!address) {\n        throw new Error(`ENS name \"${trimmed}\" does not resolve to an address.`);\n      }\n\n      const checksummed = getAddress(address);\n      setCache(trimmed, checksummed);\n\n      return {\n        address: checksummed,\n        input: trimmed,\n        ensName: trimmed,\n        wasEnsResolution: true,\n      };\n    } catch (err) {\n      if (err instanceof Error && err.message.includes('does not resolve')) {\n        throw err; // Re-throw our own error\n      }\n      throw new Error(\n        `Failed to resolve ENS name \"${trimmed}\": ${err instanceof Error ? err.message : String(err)}`,\n      );\n    }\n  }\n\n  throw new Error(\n    `Invalid address or ENS name: \"${trimmed}\". ` +\n    `Expected a 0x address (42 chars) or an ENS name (e.g., name.eth).`,\n  );\n}\n\n/**\n * Reverse-resolve an address to an ENS name (if one exists).\n * Returns null if no reverse record is set.\n */\nexport async function reverseResolveEns(\n  address: Address,\n  publicClient: any,\n): Promise<string | null> {\n  try {\n    // Reverse resolution always uses L1 (ENS registry is on Ethereum mainnet)\n    const ensClient = await getEnsClient('reverse.eth', publicClient);\n    const name = await ensClient.getEnsName({ address });\n    return name ?? null;\n  } catch {\n    return null;\n  }\n}\n\n/**\n * Get a full ENS profile for an address (name + avatar).\n */\nexport async function getEnsProfile(\n  address: Address,\n  publicClient: any,\n): Promise<EnsProfile> {\n  const name = await reverseResolveEns(address, publicClient);\n  let avatar: string | null = null;\n\n  if (name) {\n    try {\n      const ensClient = await getEnsClient(name, publicClient);\n      avatar = await ensClient.getEnsAvatar({ name });\n    } catch {\n      // Avatar resolution is best-effort\n    }\n  }\n\n  return { address, name, avatar };\n}\n\n/**\n * Batch-resolve multiple address-or-ENS inputs.\n * Returns results in the same order as inputs.\n * Individual failures are returned as errors in the result array.\n */\nexport async function resolveMany(\n  inputs: string[],\n  publicClient: any,\n): Promise<Array<ResolvedAddress | Error>> {\n  return Promise.all(\n    inputs.map(async (input) => {\n      try {\n        return await resolveAddressOrEns(input, publicClient);\n      } catch (err) {\n        return err instanceof Error ? err : new Error(String(err));\n      }\n    }),\n  );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA4BA,eAAe,aAAa,MAAc,UAA6B;AAErE,KAAI,KAAK,SAAS,YAAY,CAAE,QAAO;AAGvC,KAAI;EACF,MAAM,EAAE,kBAAkB,MAAM,OAAO;AAEvC,SAAO,MADY,eAAe,CACV,UAAU,EAAE;SAC9B;AAIN,SAAO;;;AA0BX,MAAM,4BAAY,IAAI,KAAsD;AAC5E,MAAM,eAAe,MAAS;AAE9B,SAAS,UAAU,MAA8B;CAC/C,MAAM,QAAQ,UAAU,IAAI,KAAK,aAAa,CAAC;AAC/C,KAAI,CAAC,MAAO,QAAO;AACnB,KAAI,KAAK,KAAK,GAAG,MAAM,YAAY,cAAc;AAC/C,YAAU,OAAO,KAAK,aAAa,CAAC;AACpC,SAAO;;AAET,QAAO,MAAM;;AAGf,SAAS,SAAS,MAAc,SAAwB;AACtD,WAAU,IAAI,KAAK,aAAa,EAAE;EAAE;EAAS,WAAW,KAAK,KAAK;EAAE,CAAC;AAErE,KAAI,UAAU,OAAO,KAAK;EACxB,MAAM,SAAS,MAAM,KAAK,UAAU,SAAS,CAAC,CAC3C,MAAM,GAAG,MAAM,EAAE,GAAG,YAAY,EAAE,GAAG,UAAU,CAC/C,MAAM,GAAG,IAAI;AAChB,OAAK,MAAM,CAAC,QAAQ,OAAQ,WAAU,OAAO,IAAI;;;;AAKrD,SAAgB,gBAAsB;AACpC,WAAU,OAAO;;;AAMnB,SAAgB,UAAU,OAAwB;AAChD,KAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;CAChD,MAAM,UAAU,MAAM,MAAM,CAAC,aAAa;AAG1C,QAAO,sFAAsF,KAAK,QAAQ;;;AAI5G,SAAgB,eAAe,OAAwB;AACrD,KAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAO,UAAU,MAAM,IAAI,UAAU,MAAM;;;;;;;;;;;;AAe7C,eAAsB,oBACpB,OACA,cAC0B;CAC1B,MAAM,UAAU,MAAM,MAAM;AAG5B,KAAI,UAAU,QAAQ,CACpB,QAAO;EACL,SAAS,WAAW,QAAQ;EAC5B,OAAO;EACP,kBAAkB;EACnB;AAIH,KAAI,UAAU,QAAQ,EAAE;EAEtB,MAAM,SAAS,UAAU,QAAQ;AACjC,MAAI,OACF,QAAO;GACL,SAAS;GACT,OAAO;GACP,SAAS;GACT,kBAAkB;GACnB;AAGH,MAAI;GAGF,MAAM,UAAU,OADE,MAAM,aAAa,SAAS,aAAa,EAC3B,cAAc,EAAE,MAAM,SAAS,CAAC;AAChE,OAAI,CAAC,QACH,OAAM,IAAI,MAAM,aAAa,QAAQ,mCAAmC;GAG1E,MAAM,cAAc,WAAW,QAAQ;AACvC,YAAS,SAAS,YAAY;AAE9B,UAAO;IACL,SAAS;IACT,OAAO;IACP,SAAS;IACT,kBAAkB;IACnB;WACM,KAAK;AACZ,OAAI,eAAe,SAAS,IAAI,QAAQ,SAAS,mBAAmB,CAClE,OAAM;AAER,SAAM,IAAI,MACR,+BAA+B,QAAQ,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAC7F;;;AAIL,OAAM,IAAI,MACR,iCAAiC,QAAQ,sEAE1C;;;;;;AAOH,eAAsB,kBACpB,SACA,cACwB;AACxB,KAAI;AAIF,SADa,OADK,MAAM,aAAa,eAAe,aAAa,EACpC,WAAW,EAAE,SAAS,CAAC,IACrC;SACT;AACN,SAAO;;;;;;AAOX,eAAsB,cACpB,SACA,cACqB;CACrB,MAAM,OAAO,MAAM,kBAAkB,SAAS,aAAa;CAC3D,IAAI,SAAwB;AAE5B,KAAI,KACF,KAAI;AAEF,WAAS,OADS,MAAM,aAAa,MAAM,aAAa,EAC/B,aAAa,EAAE,MAAM,CAAC;SACzC;AAKV,QAAO;EAAE;EAAS;EAAM;EAAQ;;;;;;;AAQlC,eAAsB,YACpB,QACA,cACyC;AACzC,QAAO,QAAQ,IACb,OAAO,IAAI,OAAO,UAAU;AAC1B,MAAI;AACF,UAAO,MAAM,oBAAoB,OAAO,aAAa;WAC9C,KAAK;AACZ,UAAO,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,IAAI,CAAC;;GAE5D,CACH"}