@hybrd/xmtp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,324 @@
1
+ import { type Address, PublicClient } from "viem"
2
+
3
+ interface ENSResolverOptions {
4
+ /**
5
+ * Maximum number of ENS names to cache
6
+ * @default 500
7
+ */
8
+ maxCacheSize?: number
9
+ /**
10
+ * Cache TTL in milliseconds
11
+ * @default 3600000 (1 hour)
12
+ */
13
+ cacheTtl?: number
14
+ /**
15
+ * Mainnet public client for ENS resolution
16
+ */
17
+ mainnetClient: PublicClient
18
+ }
19
+
20
+ interface CacheEntry {
21
+ address: string
22
+ timestamp: number
23
+ }
24
+
25
+ interface ReverseCacheEntry {
26
+ ensName: string
27
+ timestamp: number
28
+ }
29
+
30
+ /**
31
+ * ENS Resolver for mainnet .eth names
32
+ * Handles resolution of ENS names to addresses and reverse resolution
33
+ */
34
+ export class ENSResolver {
35
+ private cache = new Map<string, CacheEntry>()
36
+ private reverseCache = new Map<string, ReverseCacheEntry>()
37
+ private readonly maxCacheSize: number
38
+ private readonly cacheTtl: number
39
+ private readonly mainnetClient: PublicClient
40
+
41
+ constructor(options: ENSResolverOptions) {
42
+ this.maxCacheSize = options.maxCacheSize ?? 500
43
+ this.cacheTtl = options.cacheTtl ?? 3600000 // 1 hour
44
+ this.mainnetClient = options.mainnetClient
45
+ }
46
+
47
+ /**
48
+ * Resolve an ENS name to an Ethereum address
49
+ */
50
+ async resolveENSName(ensName: string): Promise<Address | null> {
51
+ console.log(`🔍 Resolving ENS name: ${ensName}`)
52
+
53
+ try {
54
+ // Check cache first
55
+ const cached = this.getCachedAddress(ensName)
56
+ if (cached) {
57
+ console.log(`✅ Resolved ENS from cache: ${ensName} → ${cached}`)
58
+ return cached as Address
59
+ }
60
+
61
+ console.log(`📭 No cached address found for ENS: ${ensName}`)
62
+
63
+ // Resolve using mainnet ENS
64
+ console.log("🔄 Reading ENS contract...")
65
+ const address = await this.mainnetClient.getEnsAddress({
66
+ name: ensName
67
+ })
68
+
69
+ console.log(`📋 ENS contract returned address: "${address}"`)
70
+
71
+ if (address && address !== "0x0000000000000000000000000000000000000000") {
72
+ this.setCachedAddress(ensName, address)
73
+ console.log(`✅ Resolved ENS: ${ensName} → ${address}`)
74
+ return address
75
+ }
76
+
77
+ console.log(`❌ No address found for ENS: ${ensName}`)
78
+ return null
79
+ } catch (error) {
80
+ console.error(`❌ Error resolving ENS name ${ensName}:`, error)
81
+ if (error instanceof Error) {
82
+ console.error(`❌ Error details: ${error.message}`)
83
+ }
84
+ return null
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Resolve an address to its primary ENS name (reverse resolution)
90
+ */
91
+ async resolveAddressToENS(address: Address): Promise<string | null> {
92
+ console.log(`🔍 Reverse resolving address to ENS: ${address}`)
93
+
94
+ try {
95
+ // Check cache first
96
+ const cached = this.getCachedENSName(address)
97
+ if (cached) {
98
+ console.log(
99
+ `✅ Resolved ENS from reverse cache: ${address} → ${cached}`
100
+ )
101
+ return cached
102
+ }
103
+
104
+ console.log(`📭 No cached ENS name found for address: ${address}`)
105
+
106
+ // Reverse resolve using mainnet ENS
107
+ console.log("🔄 Reading ENS reverse resolver...")
108
+ const ensName = await this.mainnetClient.getEnsName({
109
+ address: address
110
+ })
111
+
112
+ console.log(`📋 ENS reverse resolver returned: "${ensName}"`)
113
+
114
+ if (ensName && ensName.length > 0) {
115
+ this.setCachedENSName(address, ensName)
116
+ console.log(`✅ Reverse resolved: ${address} → ${ensName}`)
117
+ return ensName
118
+ }
119
+
120
+ console.log(`❌ No ENS name found for address: ${address}`)
121
+ return null
122
+ } catch (error) {
123
+ console.error(`❌ Error reverse resolving address ${address}:`, error)
124
+ if (error instanceof Error) {
125
+ console.error(`❌ Error details: ${error.message}`)
126
+ }
127
+ return null
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Get ENS avatar for a name
133
+ */
134
+ async getENSAvatar(ensName: string): Promise<string | null> {
135
+ console.log(`🖼️ Getting ENS avatar for: ${ensName}`)
136
+
137
+ try {
138
+ const avatar = await this.mainnetClient.getEnsAvatar({
139
+ name: ensName
140
+ })
141
+
142
+ if (avatar) {
143
+ console.log(`✅ Found ENS avatar: ${avatar}`)
144
+ return avatar
145
+ }
146
+
147
+ console.log(`❌ No avatar found for ENS: ${ensName}`)
148
+ return null
149
+ } catch (error) {
150
+ console.error(`❌ Error getting ENS avatar for ${ensName}:`, error)
151
+ return null
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Get ENS text record
157
+ */
158
+ async getENSTextRecord(ensName: string, key: string): Promise<string | null> {
159
+ console.log(`📝 Getting ENS text record "${key}" for: ${ensName}`)
160
+
161
+ try {
162
+ const textRecord = await this.mainnetClient.getEnsText({
163
+ name: ensName,
164
+ key: key
165
+ })
166
+
167
+ if (textRecord && textRecord.length > 0) {
168
+ console.log(`✅ Found ENS text record: ${key}=${textRecord}`)
169
+ return textRecord
170
+ }
171
+
172
+ console.log(`❌ No text record "${key}" found for ENS: ${ensName}`)
173
+ return null
174
+ } catch (error) {
175
+ console.error(
176
+ `❌ Error getting ENS text record ${key} for ${ensName}:`,
177
+ error
178
+ )
179
+ return null
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Get comprehensive ENS profile
185
+ */
186
+ async getENSProfile(ensName: string) {
187
+ console.log(`👤 Getting ENS profile for: ${ensName}`)
188
+
189
+ try {
190
+ const [address, avatar, description, twitter, github, url] =
191
+ await Promise.all([
192
+ this.resolveENSName(ensName),
193
+ this.getENSAvatar(ensName),
194
+ this.getENSTextRecord(ensName, "description"),
195
+ this.getENSTextRecord(ensName, "com.twitter"),
196
+ this.getENSTextRecord(ensName, "com.github"),
197
+ this.getENSTextRecord(ensName, "url")
198
+ ])
199
+
200
+ const profile = {
201
+ ensName,
202
+ address,
203
+ avatar,
204
+ description,
205
+ twitter,
206
+ github,
207
+ url
208
+ }
209
+
210
+ console.log(`✅ ENS profile for ${ensName}:`, profile)
211
+ return profile
212
+ } catch (error) {
213
+ console.error(`❌ Error getting ENS profile for ${ensName}:`, error)
214
+ return null
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Check if a name is a valid ENS name (.eth)
220
+ */
221
+ isENSName(name: string): boolean {
222
+ return name.endsWith(".eth") && !name.endsWith(".base.eth")
223
+ }
224
+
225
+ /**
226
+ * Get cached address if not expired
227
+ */
228
+ private getCachedAddress(ensName: string): string | null {
229
+ const entry = this.cache.get(ensName.toLowerCase())
230
+ if (!entry) {
231
+ return null
232
+ }
233
+
234
+ const now = Date.now()
235
+ if (now - entry.timestamp > this.cacheTtl) {
236
+ this.cache.delete(ensName.toLowerCase())
237
+ return null
238
+ }
239
+
240
+ return entry.address
241
+ }
242
+
243
+ /**
244
+ * Cache address with LRU eviction
245
+ */
246
+ private setCachedAddress(ensName: string, address: string): void {
247
+ if (this.cache.size >= this.maxCacheSize) {
248
+ const firstKey = this.cache.keys().next().value
249
+ if (firstKey) {
250
+ this.cache.delete(firstKey)
251
+ }
252
+ }
253
+
254
+ this.cache.set(ensName.toLowerCase(), {
255
+ address,
256
+ timestamp: Date.now()
257
+ })
258
+ }
259
+
260
+ /**
261
+ * Get cached ENS name if not expired
262
+ */
263
+ private getCachedENSName(address: Address): string | null {
264
+ const entry = this.reverseCache.get(address.toLowerCase())
265
+ if (!entry) {
266
+ return null
267
+ }
268
+
269
+ const now = Date.now()
270
+ if (now - entry.timestamp > this.cacheTtl) {
271
+ this.reverseCache.delete(address.toLowerCase())
272
+ return null
273
+ }
274
+
275
+ return entry.ensName
276
+ }
277
+
278
+ /**
279
+ * Cache ENS name with LRU eviction
280
+ */
281
+ private setCachedENSName(address: Address, ensName: string): void {
282
+ if (this.reverseCache.size >= this.maxCacheSize) {
283
+ const firstKey = this.reverseCache.keys().next().value
284
+ if (firstKey) {
285
+ this.reverseCache.delete(firstKey)
286
+ }
287
+ }
288
+
289
+ this.reverseCache.set(address.toLowerCase(), {
290
+ ensName,
291
+ timestamp: Date.now()
292
+ })
293
+ }
294
+
295
+ /**
296
+ * Clear all caches
297
+ */
298
+ clearCache(): void {
299
+ const addressCount = this.cache.size
300
+ const ensCount = this.reverseCache.size
301
+
302
+ this.cache.clear()
303
+ this.reverseCache.clear()
304
+
305
+ console.log(`🗑️ ENS address cache cleared (${addressCount} entries removed)`)
306
+ console.log(`🗑️ ENS reverse cache cleared (${ensCount} entries removed)`)
307
+ }
308
+
309
+ /**
310
+ * Get cache statistics
311
+ */
312
+ getCacheStats() {
313
+ return {
314
+ addressCache: {
315
+ size: this.cache.size,
316
+ maxSize: this.maxCacheSize
317
+ },
318
+ reverseCache: {
319
+ size: this.reverseCache.size,
320
+ maxSize: this.maxCacheSize
321
+ }
322
+ }
323
+ }
324
+ }
@@ -0,0 +1 @@
1
+ export * from "./resolver"
@@ -0,0 +1,336 @@
1
+ import { type Address, type PublicClient } from "viem"
2
+ import type { XmtpClient, XmtpMessage, XmtpSender } from "../types"
3
+ import { AddressResolver } from "./address-resolver"
4
+ import {
5
+ type BaseName,
6
+ BasenameResolver,
7
+ type BasenameTextRecordKey
8
+ } from "./basename-resolver"
9
+ import { ENSResolver } from "./ens-resolver"
10
+ import { XmtpResolver } from "./xmtp-resolver"
11
+
12
+ interface ResolverOptions {
13
+ /**
14
+ * XMTP Client for message and address resolution
15
+ */
16
+ xmtpClient: XmtpClient
17
+ /**
18
+ * Mainnet public client for ENS resolution
19
+ */
20
+ mainnetClient: PublicClient
21
+ /**
22
+ * Base network public client for basename resolution
23
+ */
24
+ baseClient: PublicClient
25
+ /**
26
+ * Maximum cache size for each resolver
27
+ * @default 1000
28
+ */
29
+ maxCacheSize?: number
30
+ /**
31
+ * Cache TTL in milliseconds
32
+ * @default 3600000 (1 hour)
33
+ */
34
+ cacheTtl?: number
35
+ }
36
+
37
+ /**
38
+ * Master Resolver that wraps all individual resolvers
39
+ * Provides a unified interface for basename, ENS, address, and XMTP resolution
40
+ */
41
+ export class Resolver {
42
+ private addressResolver: AddressResolver
43
+ private ensResolver: ENSResolver
44
+ private basenameResolver: BasenameResolver
45
+ private xmtpResolver: XmtpResolver
46
+
47
+ constructor(options: ResolverOptions) {
48
+ const resolverOptions = {
49
+ maxCacheSize: options.maxCacheSize ?? 1000,
50
+ cacheTtl: options.cacheTtl ?? 3600000
51
+ }
52
+
53
+ this.addressResolver = new AddressResolver(
54
+ options.xmtpClient,
55
+ resolverOptions
56
+ )
57
+ this.xmtpResolver = new XmtpResolver(options.xmtpClient, resolverOptions)
58
+
59
+ // Type assertions needed due to viem version differences across monorepo packages
60
+ // Both clients are PublicClient-compatible but TypeScript sees them as incompatible types
61
+ this.ensResolver = new ENSResolver({
62
+ ...resolverOptions,
63
+ mainnetClient: options.mainnetClient as PublicClient
64
+ })
65
+ this.basenameResolver = new BasenameResolver({
66
+ ...resolverOptions,
67
+ publicClient: options.baseClient as PublicClient
68
+ })
69
+ }
70
+
71
+ // === Address Resolution Methods ===
72
+
73
+ /**
74
+ * Resolve user address from inbox ID with caching
75
+ * Uses both AddressResolver and XmtpResolver for redundancy
76
+ */
77
+ async resolveAddress(
78
+ inboxId: string,
79
+ conversationId?: string
80
+ ): Promise<`0x${string}` | null> {
81
+ // Try AddressResolver first, fallback to XmtpResolver
82
+ let result = await this.addressResolver.resolveAddress(
83
+ inboxId,
84
+ conversationId
85
+ )
86
+ if (!result) {
87
+ result = await this.xmtpResolver.resolveAddress(inboxId, conversationId)
88
+ }
89
+ return result
90
+ }
91
+
92
+ // === ENS Resolution Methods ===
93
+
94
+ /**
95
+ * Resolve an ENS name to an Ethereum address
96
+ */
97
+ async resolveENSName(ensName: string): Promise<Address | null> {
98
+ return this.ensResolver.resolveENSName(ensName)
99
+ }
100
+
101
+ /**
102
+ * Resolve an address to its primary ENS name (reverse resolution)
103
+ */
104
+ async resolveAddressToENS(address: Address): Promise<string | null> {
105
+ return this.ensResolver.resolveAddressToENS(address)
106
+ }
107
+
108
+ /**
109
+ * Get ENS avatar for a given ENS name
110
+ */
111
+ async getENSAvatar(ensName: string): Promise<string | null> {
112
+ return this.ensResolver.getENSAvatar(ensName)
113
+ }
114
+
115
+ /**
116
+ * Get ENS text record for a given ENS name and key
117
+ */
118
+ async getENSTextRecord(ensName: string, key: string): Promise<string | null> {
119
+ return this.ensResolver.getENSTextRecord(ensName, key)
120
+ }
121
+
122
+ /**
123
+ * Get complete ENS profile for a given ENS name
124
+ */
125
+ async getENSProfile(ensName: string) {
126
+ return this.ensResolver.getENSProfile(ensName)
127
+ }
128
+
129
+ // === Basename Resolution Methods ===
130
+
131
+ /**
132
+ * Get basename from an Ethereum address
133
+ */
134
+ async getBasename(address: Address): Promise<string | null> {
135
+ return this.basenameResolver.getBasename(address)
136
+ }
137
+
138
+ /**
139
+ * Get basename avatar for a given basename
140
+ */
141
+ async getBasenameAvatar(basename: BaseName): Promise<string | null> {
142
+ return this.basenameResolver.getBasenameAvatar(basename)
143
+ }
144
+
145
+ /**
146
+ * Get basename text record for a given basename and key
147
+ */
148
+ async getBasenameTextRecord(
149
+ basename: BaseName,
150
+ key: BasenameTextRecordKey
151
+ ): Promise<string | null> {
152
+ return this.basenameResolver.getBasenameTextRecord(basename, key)
153
+ }
154
+
155
+ /**
156
+ * Resolve basename to an Ethereum address
157
+ */
158
+ async getBasenameAddress(basename: BaseName): Promise<Address | null> {
159
+ return this.basenameResolver.getBasenameAddress(basename)
160
+ }
161
+
162
+ /**
163
+ * Get basename metadata for a given basename
164
+ */
165
+ async getBasenameMetadata(basename: BaseName) {
166
+ return this.basenameResolver.getBasenameMetadata(basename)
167
+ }
168
+
169
+ /**
170
+ * Get complete basename profile for a given address
171
+ */
172
+ async resolveBasenameProfile(address: Address) {
173
+ return this.basenameResolver.resolveBasenameProfile(address)
174
+ }
175
+
176
+ // === XMTP Message Methods ===
177
+
178
+ /**
179
+ * Find any message by ID with caching
180
+ */
181
+ async findMessage(messageId: string): Promise<XmtpMessage | null> {
182
+ return this.xmtpResolver.findMessage(messageId)
183
+ }
184
+
185
+ /**
186
+ * Find root message by ID (traverses reply chain)
187
+ */
188
+ async findRootMessage(messageId: string): Promise<XmtpMessage | null> {
189
+ return this.xmtpResolver.findRootMessage(messageId)
190
+ }
191
+
192
+ // === Universal Resolution Methods ===
193
+
194
+ /**
195
+ * Universal name resolution - tries to resolve any name (ENS or basename) to an address
196
+ */
197
+ async resolveName(name: string): Promise<Address | null> {
198
+ // Try ENS first (more common)
199
+ if (name.endsWith(".eth")) {
200
+ return this.resolveENSName(name)
201
+ }
202
+
203
+ // Try basename
204
+ if (name.endsWith(".base.eth")) {
205
+ return this.getBasenameAddress(name)
206
+ }
207
+
208
+ // If no TLD, try both
209
+ const ensResult = await this.resolveENSName(name)
210
+ if (ensResult) {
211
+ return ensResult
212
+ }
213
+
214
+ return this.getBasenameAddress(name)
215
+ }
216
+
217
+ /**
218
+ * Universal reverse resolution - tries to resolve an address to any name (ENS or basename)
219
+ */
220
+ async resolveAddressToName(address: Address): Promise<string | null> {
221
+ // Try basename first (more relevant for this project)
222
+ const basename = await this.getBasename(address)
223
+ if (basename) {
224
+ return basename
225
+ }
226
+
227
+ // Try ENS as fallback
228
+ return this.resolveAddressToENS(address)
229
+ }
230
+
231
+ /**
232
+ * Get complete profile for an address (combines ENS and basename data)
233
+ */
234
+ async getCompleteProfile(address: Address) {
235
+ const [ensName, basename, ensProfile, basenameProfile] =
236
+ await Promise.allSettled([
237
+ this.resolveAddressToENS(address),
238
+ this.getBasename(address),
239
+ this.resolveAddressToENS(address).then((name) =>
240
+ name ? this.getENSProfile(name) : null
241
+ ),
242
+ this.resolveBasenameProfile(address)
243
+ ])
244
+
245
+ return {
246
+ address,
247
+ ensName: ensName.status === "fulfilled" ? ensName.value : null,
248
+ basename: basename.status === "fulfilled" ? basename.value : null,
249
+ ensProfile: ensProfile.status === "fulfilled" ? ensProfile.value : null,
250
+ basenameProfile:
251
+ basenameProfile.status === "fulfilled" ? basenameProfile.value : null
252
+ }
253
+ }
254
+
255
+ // === Cache Management Methods ===
256
+
257
+ /**
258
+ * Pre-populate all resolver caches
259
+ */
260
+ async prePopulateAllCaches(): Promise<void> {
261
+ await Promise.allSettled([
262
+ this.addressResolver.prePopulateCache(),
263
+ this.xmtpResolver.prePopulateCache()
264
+ ])
265
+ }
266
+
267
+ /**
268
+ * Create a complete XmtpSender object from an address or inboxId
269
+ * Uses the resolver to get the best available name and profile information
270
+ */
271
+ async createXmtpSender(
272
+ addressOrInboxId: string,
273
+ conversationId?: string
274
+ ): Promise<XmtpSender> {
275
+ let address: `0x${string}` | null = null
276
+ let inboxId = addressOrInboxId
277
+
278
+ // Check if input looks like an Ethereum address
279
+ if (addressOrInboxId.startsWith("0x") && addressOrInboxId.length === 42) {
280
+ address = addressOrInboxId as `0x${string}`
281
+ // When we have an address, we need to find the actual inboxId
282
+ // For now, use address as fallback but this should be resolved from XMTP
283
+ inboxId = addressOrInboxId // This will be improved when we have proper address->inboxId resolution
284
+ } else {
285
+ // Assume it's an inboxId, try to resolve to address
286
+ address = await this.resolveAddress(addressOrInboxId, conversationId)
287
+ }
288
+
289
+ // Get the best available name using universal resolution
290
+ let name = "Unknown"
291
+ let basename: string | undefined
292
+
293
+ if (address) {
294
+ // Try basename first since that's what we expect for this address
295
+ const basenameResult = await this.getBasename(address)
296
+ console.log(
297
+ `🔍 [RESOLVER] Direct basename lookup for ${address}:`,
298
+ basenameResult
299
+ )
300
+
301
+ // Try to get a human-readable name
302
+ const resolvedName = await this.resolveAddressToName(address)
303
+ console.log(
304
+ `🔍 [RESOLVER] Universal name resolution for ${address}:`,
305
+ resolvedName
306
+ )
307
+
308
+ if (resolvedName) {
309
+ name = resolvedName
310
+ // Check if it's a basename specifically
311
+ if (resolvedName.endsWith(".base.eth")) {
312
+ basename = resolvedName
313
+ }
314
+ } else {
315
+ // Fallback to shortened address
316
+ name = `${address.slice(0, 6)}...${address.slice(-4)}`
317
+ }
318
+
319
+ // Always try to get basename even if ENS was found
320
+ if (!basename) {
321
+ const resolvedBasename = await this.getBasename(address)
322
+ basename = resolvedBasename || undefined
323
+ }
324
+ } else {
325
+ // No address resolution available, use inboxId
326
+ name = `${inboxId.slice(0, 8)}...${inboxId.slice(-4)}`
327
+ }
328
+
329
+ return {
330
+ address: address || addressOrInboxId,
331
+ inboxId,
332
+ name,
333
+ basename
334
+ }
335
+ }
336
+ }