import { type Address, type PublicClient } from "viem" import type { XmtpClient, XmtpMessage, XmtpSender } from "../types" import { AddressResolver } from "./address-resolver" import { type BaseName, BasenameResolver, type BasenameTextRecordKey } from "./basename-resolver" import { ENSResolver } from "./ens-resolver" import { XmtpResolver } from "./xmtp-resolver" import { logger } from "@hybrd/utils" interface ResolverOptions { /** * XMTP Client for message and address resolution */ xmtpClient: XmtpClient /** * Mainnet public client for ENS resolution */ mainnetClient: PublicClient /** * Base network public client for basename resolution */ baseClient: PublicClient /** * Maximum cache size for each resolver * @default 1000 */ maxCacheSize?: number /** * Cache TTL in milliseconds * @default 3600000 (1 hour) */ cacheTtl?: number } /** * Master Resolver that wraps all individual resolvers * Provides a unified interface for basename, ENS, address, and XMTP resolution */ export class Resolver { private addressResolver: AddressResolver private ensResolver: ENSResolver private basenameResolver: BasenameResolver private xmtpResolver: XmtpResolver constructor(options: ResolverOptions) { const resolverOptions = { maxCacheSize: options.maxCacheSize ?? 1000, cacheTtl: options.cacheTtl ?? 3600000 } this.addressResolver = new AddressResolver( options.xmtpClient, resolverOptions ) this.xmtpResolver = new XmtpResolver(options.xmtpClient, resolverOptions) // Type assertions needed due to viem version differences across monorepo packages // Both clients are PublicClient-compatible but TypeScript sees them as incompatible types this.ensResolver = new ENSResolver({ ...resolverOptions, mainnetClient: options.mainnetClient as PublicClient }) this.basenameResolver = new BasenameResolver({ ...resolverOptions, publicClient: options.baseClient as PublicClient }) } // === Address Resolution Methods === /** * Resolve user address from inbox ID with caching * Uses both AddressResolver and XmtpResolver for redundancy */ async resolveAddress( inboxId: string, conversationId?: string ): Promise<`0x${string}` | null> { // Try AddressResolver first, fallback to XmtpResolver let result = await this.addressResolver.resolveAddress( inboxId, conversationId ) if (!result) { result = await this.xmtpResolver.resolveAddress(inboxId, conversationId) } return result } // === ENS Resolution Methods === /** * Resolve an ENS name to an Ethereum address */ async resolveENSName(ensName: string): Promise
{ return this.ensResolver.resolveENSName(ensName) } /** * Resolve an address to its primary ENS name (reverse resolution) */ async resolveAddressToENS(address: Address): Promise { return this.ensResolver.resolveAddressToENS(address) } /** * Get ENS avatar for a given ENS name */ async getENSAvatar(ensName: string): Promise { return this.ensResolver.getENSAvatar(ensName) } /** * Get ENS text record for a given ENS name and key */ async getENSTextRecord(ensName: string, key: string): Promise { return this.ensResolver.getENSTextRecord(ensName, key) } /** * Get complete ENS profile for a given ENS name */ async getENSProfile(ensName: string) { return this.ensResolver.getENSProfile(ensName) } // === Basename Resolution Methods === /** * Get basename from an Ethereum address */ async getBasename(address: Address): Promise { return this.basenameResolver.getBasename(address) } /** * Get basename avatar for a given basename */ async getBasenameAvatar(basename: BaseName): Promise { return this.basenameResolver.getBasenameAvatar(basename) } /** * Get basename text record for a given basename and key */ async getBasenameTextRecord( basename: BaseName, key: BasenameTextRecordKey ): Promise { return this.basenameResolver.getBasenameTextRecord(basename, key) } /** * Resolve basename to an Ethereum address */ async getBasenameAddress(basename: BaseName): Promise
{ return this.basenameResolver.getBasenameAddress(basename) } /** * Get basename metadata for a given basename */ async getBasenameMetadata(basename: BaseName) { return this.basenameResolver.getBasenameMetadata(basename) } /** * Get complete basename profile for a given address */ async resolveBasenameProfile(address: Address) { return this.basenameResolver.resolveBasenameProfile(address) } // === XMTP Message Methods === /** * Find any message by ID with caching */ async findMessage(messageId: string): Promise { return this.xmtpResolver.findMessage(messageId) } /** * Find root message by ID (traverses reply chain) */ async findRootMessage(messageId: string): Promise { return this.xmtpResolver.findRootMessage(messageId) } // === Universal Resolution Methods === /** * Universal name resolution - tries to resolve any name (ENS or basename) to an address */ async resolveName(name: string): Promise
{ // Try ENS first (more common) if (name.endsWith(".eth")) { return this.resolveENSName(name) } // Try basename if (name.endsWith(".base.eth")) { return this.getBasenameAddress(name) } // If no TLD, try both const ensResult = await this.resolveENSName(name) if (ensResult) { return ensResult } return this.getBasenameAddress(name) } /** * Universal reverse resolution - tries to resolve an address to any name (ENS or basename) */ async resolveAddressToName(address: Address): Promise { // Try basename first (more relevant for this project) const basename = await this.getBasename(address) if (basename) { return basename } // Try ENS as fallback return this.resolveAddressToENS(address) } /** * Get complete profile for an address (combines ENS and basename data) */ async getCompleteProfile(address: Address) { const [ensName, basename, ensProfile, basenameProfile] = await Promise.allSettled([ this.resolveAddressToENS(address), this.getBasename(address), this.resolveAddressToENS(address).then((name) => name ? this.getENSProfile(name) : null ), this.resolveBasenameProfile(address) ]) return { address, ensName: ensName.status === "fulfilled" ? ensName.value : null, basename: basename.status === "fulfilled" ? basename.value : null, ensProfile: ensProfile.status === "fulfilled" ? ensProfile.value : null, basenameProfile: basenameProfile.status === "fulfilled" ? basenameProfile.value : null } } // === Cache Management Methods === /** * Pre-populate all resolver caches */ async prePopulateAllCaches(): Promise { await Promise.allSettled([ this.addressResolver.prePopulateCache(), this.xmtpResolver.prePopulateCache() ]) } /** * Create a complete XmtpSender object from an address or inboxId * Uses the resolver to get the best available name and profile information */ async createXmtpSender( addressOrInboxId: string, conversationId?: string ): Promise { let address: `0x${string}` | null = null let inboxId = addressOrInboxId // Check if input looks like an Ethereum address if (addressOrInboxId.startsWith("0x") && addressOrInboxId.length === 42) { address = addressOrInboxId as `0x${string}` // When we have an address, we need to find the actual inboxId // For now, use address as fallback but this should be resolved from XMTP inboxId = addressOrInboxId // This will be improved when we have proper address->inboxId resolution } else { // Assume it's an inboxId, try to resolve to address address = await this.resolveAddress(addressOrInboxId, conversationId) } // Get the best available name using universal resolution let name = "Unknown" let basename: string | undefined if (address) { // Try basename first since that's what we expect for this address const basenameResult = await this.getBasename(address) logger.debug( `🔍 [RESOLVER] Direct basename lookup for ${address}:`, basenameResult ) // Try to get a human-readable name const resolvedName = await this.resolveAddressToName(address) logger.debug( `🔍 [RESOLVER] Universal name resolution for ${address}:`, resolvedName ) if (resolvedName) { name = resolvedName // Check if it's a basename specifically if (resolvedName.endsWith(".base.eth")) { basename = resolvedName } } else { // Fallback to shortened address name = `${address.slice(0, 6)}...${address.slice(-4)}` } // Always try to get basename even if ENS was found if (!basename) { const resolvedBasename = await this.getBasename(address) basename = resolvedBasename || undefined } } else { // No address resolution available, use inboxId name = `${inboxId.slice(0, 8)}...${inboxId.slice(-4)}` } return { address: address || addressOrInboxId, inboxId, name, basename } } }