@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.
- package/.cache/tsbuildinfo.json +1 -0
- package/.turbo/turbo-typecheck.log +5 -0
- package/README.md +380 -0
- package/biome.jsonc +4 -0
- package/package.json +46 -0
- package/scripts/generate-keys.ts +25 -0
- package/scripts/refresh-identity.ts +119 -0
- package/scripts/register-wallet.ts +95 -0
- package/scripts/revoke-all-installations.ts +91 -0
- package/scripts/revoke-installations.ts +94 -0
- package/src/abi/l2_resolver.ts +699 -0
- package/src/client.ts +940 -0
- package/src/constants.ts +6 -0
- package/src/index.ts +129 -0
- package/src/lib/message-listener.test.ts +369 -0
- package/src/lib/message-listener.ts +343 -0
- package/src/lib/subjects.ts +89 -0
- package/src/localStorage.ts.old +203 -0
- package/src/resolver/address-resolver.ts +221 -0
- package/src/resolver/basename-resolver.ts +585 -0
- package/src/resolver/ens-resolver.ts +324 -0
- package/src/resolver/index.ts +1 -0
- package/src/resolver/resolver.ts +336 -0
- package/src/resolver/xmtp-resolver.ts +436 -0
- package/src/service-client.ts +286 -0
- package/src/transactionMonitor.ts.old +275 -0
- package/src/types.ts +157 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import type { XmtpClient, XmtpMessage } from "../types"
|
|
2
|
+
|
|
3
|
+
interface XmtpResolverOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Maximum number of addresses to cache
|
|
6
|
+
* @default 1000
|
|
7
|
+
*/
|
|
8
|
+
maxCacheSize?: number
|
|
9
|
+
/**
|
|
10
|
+
* Cache TTL in milliseconds
|
|
11
|
+
* @default 86400000 (24 hours)
|
|
12
|
+
*/
|
|
13
|
+
cacheTtl?: number
|
|
14
|
+
/**
|
|
15
|
+
* Maximum number of messages to cache
|
|
16
|
+
* @default 1000
|
|
17
|
+
*/
|
|
18
|
+
maxMessageCacheSize?: number
|
|
19
|
+
/**
|
|
20
|
+
* Message cache TTL in milliseconds
|
|
21
|
+
* @default 3600000 (1 hour)
|
|
22
|
+
*/
|
|
23
|
+
messageCacheTtl?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface AddressCacheEntry {
|
|
27
|
+
address: string
|
|
28
|
+
timestamp: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface MessageCacheEntry {
|
|
32
|
+
message: XmtpMessage | null
|
|
33
|
+
timestamp: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class XmtpResolver {
|
|
37
|
+
private addressCache = new Map<string, AddressCacheEntry>()
|
|
38
|
+
private messageCache = new Map<string, MessageCacheEntry>()
|
|
39
|
+
private readonly maxCacheSize: number
|
|
40
|
+
private readonly cacheTtl: number
|
|
41
|
+
private readonly maxMessageCacheSize: number
|
|
42
|
+
private readonly messageCacheTtl: number
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
private client: XmtpClient,
|
|
46
|
+
options: XmtpResolverOptions = {}
|
|
47
|
+
) {
|
|
48
|
+
this.maxCacheSize = options.maxCacheSize ?? 1000
|
|
49
|
+
this.cacheTtl = options.cacheTtl ?? 86400000 // 24 hours
|
|
50
|
+
this.maxMessageCacheSize = options.maxMessageCacheSize ?? 1000
|
|
51
|
+
this.messageCacheTtl = options.messageCacheTtl ?? 3600000 // 1 hour
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve user address from inbox ID with caching
|
|
56
|
+
*/
|
|
57
|
+
async resolveAddress(
|
|
58
|
+
inboxId: string,
|
|
59
|
+
conversationId?: string
|
|
60
|
+
): Promise<`0x${string}` | null> {
|
|
61
|
+
// Check cache first (fastest)
|
|
62
|
+
const cached = this.getCachedAddress(inboxId)
|
|
63
|
+
if (cached) {
|
|
64
|
+
console.log(
|
|
65
|
+
`✅ [XmtpResolver] Resolved user address from cache: ${cached}`
|
|
66
|
+
)
|
|
67
|
+
return cached
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let userAddress = undefined
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Try conversation members lookup first (faster than network call)
|
|
74
|
+
if (conversationId) {
|
|
75
|
+
const conversation =
|
|
76
|
+
await this.client.conversations.getConversationById(conversationId)
|
|
77
|
+
if (conversation) {
|
|
78
|
+
userAddress = await this.resolveFromConversation(
|
|
79
|
+
conversation,
|
|
80
|
+
inboxId
|
|
81
|
+
)
|
|
82
|
+
if (userAddress) {
|
|
83
|
+
this.setCachedAddress(inboxId, userAddress)
|
|
84
|
+
console.log(
|
|
85
|
+
`✅ [XmtpResolver] Resolved user address: ${userAddress}`
|
|
86
|
+
)
|
|
87
|
+
return userAddress
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Fallback to inboxStateFromInboxIds
|
|
93
|
+
userAddress = await this.resolveFromInboxState(inboxId)
|
|
94
|
+
if (userAddress) {
|
|
95
|
+
this.setCachedAddress(inboxId, userAddress)
|
|
96
|
+
console.log(
|
|
97
|
+
`✅ [XmtpResolver] Resolved user address via fallback: ${userAddress}`
|
|
98
|
+
)
|
|
99
|
+
return userAddress
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(`⚠️ [XmtpResolver] No identifiers found for inbox ${inboxId}`)
|
|
103
|
+
return null
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error(
|
|
106
|
+
`❌ [XmtpResolver] Error resolving user address for ${inboxId}:`,
|
|
107
|
+
error
|
|
108
|
+
)
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Find any message by ID with caching
|
|
115
|
+
*/
|
|
116
|
+
async findMessage(messageId: string): Promise<XmtpMessage | null> {
|
|
117
|
+
// Check cache first
|
|
118
|
+
const cached = this.getCachedMessage(messageId)
|
|
119
|
+
if (cached !== undefined) {
|
|
120
|
+
console.log(
|
|
121
|
+
cached
|
|
122
|
+
? `✅ [XmtpResolver] Found message from cache: ${cached.id}`
|
|
123
|
+
: `✅ [XmtpResolver] Found cached null message for: ${messageId}`
|
|
124
|
+
)
|
|
125
|
+
return cached
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
console.log(`🔍 [XmtpResolver] Finding message: ${messageId}`)
|
|
130
|
+
const message = await this.client.conversations.getMessageById(messageId)
|
|
131
|
+
|
|
132
|
+
if (message) {
|
|
133
|
+
this.setCachedMessage(messageId, message)
|
|
134
|
+
console.log(`✅ [XmtpResolver] Found and cached message: ${message.id}`)
|
|
135
|
+
return message
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(`⚠️ [XmtpResolver] Message not found: ${messageId}`)
|
|
139
|
+
this.setCachedMessage(messageId, null)
|
|
140
|
+
return null
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error(
|
|
143
|
+
`❌ [XmtpResolver] Error finding message ${messageId}:`,
|
|
144
|
+
error
|
|
145
|
+
)
|
|
146
|
+
this.setCachedMessage(messageId, null)
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Find root message with caching
|
|
153
|
+
*/
|
|
154
|
+
async findRootMessage(messageId: string): Promise<XmtpMessage | null> {
|
|
155
|
+
// Check if we already have the root cached with a special key
|
|
156
|
+
const rootCacheKey = `root:${messageId}`
|
|
157
|
+
const cached = this.getCachedMessage(rootCacheKey)
|
|
158
|
+
if (cached !== undefined) {
|
|
159
|
+
console.log(
|
|
160
|
+
cached
|
|
161
|
+
? `✅ [XmtpResolver] Found root message from cache: ${cached.id}`
|
|
162
|
+
: `✅ [XmtpResolver] Found cached null root for: ${messageId}`
|
|
163
|
+
)
|
|
164
|
+
return cached
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
console.log(`🔍 [XmtpResolver] Finding root message for: ${messageId}`)
|
|
169
|
+
const rootMessage = await this.findRootMessageRecursive(messageId)
|
|
170
|
+
|
|
171
|
+
if (rootMessage) {
|
|
172
|
+
this.setCachedMessage(rootCacheKey, rootMessage)
|
|
173
|
+
console.log(
|
|
174
|
+
`✅ [XmtpResolver] Found and cached root message: ${rootMessage.id}`
|
|
175
|
+
)
|
|
176
|
+
return rootMessage
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log(`⚠️ [XmtpResolver] No root message found for: ${messageId}`)
|
|
180
|
+
this.setCachedMessage(rootCacheKey, null)
|
|
181
|
+
return null
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.error(
|
|
184
|
+
`❌ [XmtpResolver] Error finding root message for ${messageId}:`,
|
|
185
|
+
error
|
|
186
|
+
)
|
|
187
|
+
this.setCachedMessage(rootCacheKey, null)
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Recursively finds the root message in a reply chain by following reply references
|
|
194
|
+
*/
|
|
195
|
+
private async findRootMessageRecursive(
|
|
196
|
+
messageId: string,
|
|
197
|
+
visitedIds = new Set<string>()
|
|
198
|
+
): Promise<XmtpMessage | null> {
|
|
199
|
+
// Prevent infinite loops
|
|
200
|
+
if (visitedIds.has(messageId)) {
|
|
201
|
+
console.warn(
|
|
202
|
+
`⚠️ Circular reference detected in message chain at ${messageId}`
|
|
203
|
+
)
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
visitedIds.add(messageId)
|
|
207
|
+
|
|
208
|
+
const message = await this.client.conversations.getMessageById(messageId)
|
|
209
|
+
|
|
210
|
+
if (!message) {
|
|
211
|
+
console.warn(`⚠️ [findRootMessage] Message not found: ${messageId}`)
|
|
212
|
+
return null
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Debug: Log the raw message structure as returned by XMTP client
|
|
216
|
+
console.log(`🔍 [findRootMessage] Raw message ${messageId}:`, {
|
|
217
|
+
id: message.id,
|
|
218
|
+
contentType: message.contentType,
|
|
219
|
+
content: message.content,
|
|
220
|
+
sentAt: message.sentAt
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
// Method 1: Try the parameters (as seen in webhook data)
|
|
224
|
+
if ((message as any).content?.reference) {
|
|
225
|
+
return this.findRootMessageRecursive(
|
|
226
|
+
(message as any).content.reference,
|
|
227
|
+
visitedIds
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return message
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Resolve address from conversation members
|
|
236
|
+
*/
|
|
237
|
+
private async resolveFromConversation(
|
|
238
|
+
conversation: any,
|
|
239
|
+
inboxId: string
|
|
240
|
+
): Promise<`0x${string}` | null> {
|
|
241
|
+
try {
|
|
242
|
+
const members = await conversation.members()
|
|
243
|
+
const sender = members.find(
|
|
244
|
+
(member: any) => member.inboxId.toLowerCase() === inboxId.toLowerCase()
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if (sender) {
|
|
248
|
+
const ethIdentifier = sender.accountIdentifiers.find(
|
|
249
|
+
(id: any) => id.identifierKind === 0 // IdentifierKind.Ethereum
|
|
250
|
+
)
|
|
251
|
+
if (ethIdentifier) {
|
|
252
|
+
return ethIdentifier.identifier
|
|
253
|
+
} else {
|
|
254
|
+
console.log(
|
|
255
|
+
`⚠️ [XmtpResolver] No Ethereum identifier found for inbox ${inboxId}`
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
console.log(
|
|
260
|
+
`⚠️ [XmtpResolver] Sender not found in conversation members for inbox ${inboxId}`
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error(
|
|
265
|
+
`❌ [XmtpResolver] Error resolving from conversation members:`,
|
|
266
|
+
error
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return null
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Resolve address from inbox state (network fallback)
|
|
275
|
+
*/
|
|
276
|
+
private async resolveFromInboxState(
|
|
277
|
+
inboxId: string
|
|
278
|
+
): Promise<`0x${string}` | null> {
|
|
279
|
+
try {
|
|
280
|
+
const inboxState = await this.client.preferences.inboxStateFromInboxIds([
|
|
281
|
+
inboxId
|
|
282
|
+
])
|
|
283
|
+
const firstState = inboxState?.[0]
|
|
284
|
+
if (firstState?.identifiers && firstState.identifiers.length > 0) {
|
|
285
|
+
const firstIdentifier = firstState.identifiers[0]
|
|
286
|
+
return firstIdentifier?.identifier as `0x${string}`
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error(
|
|
290
|
+
`❌ [XmtpResolver] Error resolving from inbox state:`,
|
|
291
|
+
error
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return null
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get cached address if not expired
|
|
300
|
+
*/
|
|
301
|
+
private getCachedAddress(inboxId: string): `0x${string}` | null {
|
|
302
|
+
const entry = this.addressCache.get(inboxId)
|
|
303
|
+
if (!entry) return null
|
|
304
|
+
|
|
305
|
+
const now = Date.now()
|
|
306
|
+
if (now - entry.timestamp > this.cacheTtl) {
|
|
307
|
+
this.addressCache.delete(inboxId)
|
|
308
|
+
return null
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return entry.address as `0x${string}`
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Cache address with LRU eviction
|
|
316
|
+
*/
|
|
317
|
+
private setCachedAddress(inboxId: string, address: `0x${string}`): void {
|
|
318
|
+
// Simple LRU: if cache is full, remove oldest entry
|
|
319
|
+
if (this.addressCache.size >= this.maxCacheSize) {
|
|
320
|
+
const firstKey = this.addressCache.keys().next().value
|
|
321
|
+
if (firstKey) {
|
|
322
|
+
this.addressCache.delete(firstKey)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this.addressCache.set(inboxId, {
|
|
327
|
+
address,
|
|
328
|
+
timestamp: Date.now()
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get cached message if not expired
|
|
334
|
+
*/
|
|
335
|
+
private getCachedMessage(messageId: string): XmtpMessage | null | undefined {
|
|
336
|
+
const entry = this.messageCache.get(messageId)
|
|
337
|
+
if (!entry) return undefined
|
|
338
|
+
|
|
339
|
+
const now = Date.now()
|
|
340
|
+
if (now - entry.timestamp > this.messageCacheTtl) {
|
|
341
|
+
this.messageCache.delete(messageId)
|
|
342
|
+
return undefined
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return entry.message
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Cache message with LRU eviction
|
|
350
|
+
*/
|
|
351
|
+
private setCachedMessage(
|
|
352
|
+
messageId: string,
|
|
353
|
+
message: XmtpMessage | null
|
|
354
|
+
): void {
|
|
355
|
+
// Simple LRU: if cache is full, remove oldest entry
|
|
356
|
+
if (this.messageCache.size >= this.maxMessageCacheSize) {
|
|
357
|
+
const firstKey = this.messageCache.keys().next().value
|
|
358
|
+
if (firstKey) {
|
|
359
|
+
this.messageCache.delete(firstKey)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
this.messageCache.set(messageId, {
|
|
364
|
+
message,
|
|
365
|
+
timestamp: Date.now()
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Pre-populate address cache from existing conversations
|
|
371
|
+
*/
|
|
372
|
+
async prePopulateCache(): Promise<void> {
|
|
373
|
+
console.log("🔄 [XmtpResolver] Pre-populating address cache...")
|
|
374
|
+
try {
|
|
375
|
+
const conversations = await this.client.conversations.list()
|
|
376
|
+
let cachedCount = 0
|
|
377
|
+
|
|
378
|
+
for (const conversation of conversations) {
|
|
379
|
+
try {
|
|
380
|
+
const members = await conversation.members()
|
|
381
|
+
for (const member of members) {
|
|
382
|
+
const ethIdentifier = member.accountIdentifiers.find(
|
|
383
|
+
(id: any) => id.identifierKind === 0 // IdentifierKind.Ethereum
|
|
384
|
+
)
|
|
385
|
+
if (ethIdentifier) {
|
|
386
|
+
this.setCachedAddress(
|
|
387
|
+
member.inboxId,
|
|
388
|
+
ethIdentifier.identifier as `0x${string}`
|
|
389
|
+
)
|
|
390
|
+
cachedCount++
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} catch (error) {
|
|
394
|
+
console.error(
|
|
395
|
+
"[XmtpResolver] Error pre-caching conversation members:",
|
|
396
|
+
error
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
console.log(
|
|
402
|
+
`✅ [XmtpResolver] Pre-cached ${cachedCount} address mappings`
|
|
403
|
+
)
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.error("[XmtpResolver] Error pre-populating cache:", error)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Clear all caches
|
|
411
|
+
*/
|
|
412
|
+
clearCache(): void {
|
|
413
|
+
this.addressCache.clear()
|
|
414
|
+
this.messageCache.clear()
|
|
415
|
+
console.log("🗑️ [XmtpResolver] All caches cleared")
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Get cache statistics
|
|
420
|
+
*/
|
|
421
|
+
getCacheStats(): {
|
|
422
|
+
address: { size: number; maxSize: number }
|
|
423
|
+
message: { size: number; maxSize: number }
|
|
424
|
+
} {
|
|
425
|
+
return {
|
|
426
|
+
address: {
|
|
427
|
+
size: this.addressCache.size,
|
|
428
|
+
maxSize: this.maxCacheSize
|
|
429
|
+
},
|
|
430
|
+
message: {
|
|
431
|
+
size: this.messageCache.size,
|
|
432
|
+
maxSize: this.maxMessageCacheSize
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview XMTP Service Client Library
|
|
3
|
+
*
|
|
4
|
+
* Clean, reusable client for making HTTP calls to the XMTP listener service.
|
|
5
|
+
* Handles authentication, request formatting, and error handling.
|
|
6
|
+
*
|
|
7
|
+
* This is different from the direct XMTP client - this is for external services
|
|
8
|
+
* talking to our XMTP listener service.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
GetMessageParams,
|
|
13
|
+
SendMessageParams,
|
|
14
|
+
SendMessageResponse,
|
|
15
|
+
SendReactionParams,
|
|
16
|
+
SendReactionResponse,
|
|
17
|
+
SendReplyParams,
|
|
18
|
+
SendReplyResponse,
|
|
19
|
+
SendTransactionParams,
|
|
20
|
+
SendTransactionResponse,
|
|
21
|
+
XmtpServiceClientConfig,
|
|
22
|
+
XmtpServiceMessage,
|
|
23
|
+
XmtpServiceResponse
|
|
24
|
+
} from "./types.js"
|
|
25
|
+
|
|
26
|
+
export class XmtpServiceClient {
|
|
27
|
+
private config: XmtpServiceClientConfig
|
|
28
|
+
|
|
29
|
+
constructor(config: XmtpServiceClientConfig) {
|
|
30
|
+
this.config = config
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private async request<T = unknown>(
|
|
34
|
+
endpoint: string,
|
|
35
|
+
body?: Record<string, unknown>,
|
|
36
|
+
method: "GET" | "POST" = "POST"
|
|
37
|
+
): Promise<XmtpServiceResponse<T>> {
|
|
38
|
+
try {
|
|
39
|
+
const baseUrl = this.config.serviceUrl.replace(/\/+$/, "")
|
|
40
|
+
|
|
41
|
+
// Use Authorization header for xmtp-tools endpoints, query parameter for others
|
|
42
|
+
const isXmtpToolsEndpoint = endpoint.startsWith("/xmtp-tools/")
|
|
43
|
+
const url = `${baseUrl}${endpoint}?token=${this.config.serviceToken}`
|
|
44
|
+
|
|
45
|
+
const headers: Record<string, string> = {
|
|
46
|
+
"Content-Type": "application/json"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Add Authorization header for xmtp-tools endpoints
|
|
50
|
+
if (isXmtpToolsEndpoint) {
|
|
51
|
+
headers.Authorization = `Bearer ${this.config.serviceToken}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fetchOptions: RequestInit = {
|
|
55
|
+
method,
|
|
56
|
+
headers
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (method === "POST" && body) {
|
|
60
|
+
fetchOptions.body = JSON.stringify(body)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const response = await fetch(url, fetchOptions)
|
|
64
|
+
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
let errorMessage = `HTTP ${response.status}`
|
|
67
|
+
try {
|
|
68
|
+
const responseText = await response.text()
|
|
69
|
+
try {
|
|
70
|
+
const errorData = JSON.parse(responseText) as { error?: string }
|
|
71
|
+
errorMessage = errorData.error || errorMessage
|
|
72
|
+
} catch {
|
|
73
|
+
errorMessage = responseText || errorMessage
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// If we can't read the response at all, use the status
|
|
77
|
+
}
|
|
78
|
+
throw new Error(errorMessage)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
success: true,
|
|
83
|
+
data: (await response.json()) as T
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error(
|
|
87
|
+
`❌ [XmtpServiceClient] Request to ${endpoint} failed:`,
|
|
88
|
+
error
|
|
89
|
+
)
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async sendMessage(
|
|
98
|
+
params: SendMessageParams
|
|
99
|
+
): Promise<XmtpServiceResponse<SendMessageResponse>> {
|
|
100
|
+
return this.request<SendMessageResponse>("/xmtp-tools/send", {
|
|
101
|
+
content: params.content
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async sendReply(
|
|
106
|
+
params: SendReplyParams
|
|
107
|
+
): Promise<XmtpServiceResponse<SendReplyResponse>> {
|
|
108
|
+
return this.request<SendReplyResponse>("/xmtp-tools/reply", {
|
|
109
|
+
content: params.content,
|
|
110
|
+
messageId: params.messageId
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async sendReaction(
|
|
115
|
+
params: SendReactionParams
|
|
116
|
+
): Promise<XmtpServiceResponse<SendReactionResponse>> {
|
|
117
|
+
return this.request<SendReactionResponse>("/xmtp-tools/react", {
|
|
118
|
+
messageId: params.messageId,
|
|
119
|
+
emoji: params.emoji,
|
|
120
|
+
action: params.action
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async sendTransaction(
|
|
125
|
+
params: SendTransactionParams
|
|
126
|
+
): Promise<XmtpServiceResponse<SendTransactionResponse>> {
|
|
127
|
+
return this.request<SendTransactionResponse>("/xmtp-tools/transaction", {
|
|
128
|
+
fromAddress: params.fromAddress,
|
|
129
|
+
chainId: params.chainId,
|
|
130
|
+
calls: params.calls.map((call) => ({
|
|
131
|
+
to: call.to,
|
|
132
|
+
data: call.data,
|
|
133
|
+
...(call.gas && { gas: call.gas }),
|
|
134
|
+
value: call.value || "0x0",
|
|
135
|
+
metadata: {
|
|
136
|
+
...call.metadata,
|
|
137
|
+
chainId: params.chainId,
|
|
138
|
+
from: params.fromAddress,
|
|
139
|
+
version: "1"
|
|
140
|
+
}
|
|
141
|
+
}))
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get a single message by ID
|
|
147
|
+
*/
|
|
148
|
+
async getMessage(
|
|
149
|
+
params: GetMessageParams
|
|
150
|
+
): Promise<XmtpServiceResponse<XmtpServiceMessage>> {
|
|
151
|
+
return this.request<XmtpServiceMessage>(
|
|
152
|
+
`/xmtp-tools/messages/${params.messageId}`,
|
|
153
|
+
undefined,
|
|
154
|
+
"GET"
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// getConversationMessages removed - superseded by thread-based approach
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Create an XMTP service client from runtime context
|
|
163
|
+
* Expects the runtime context to have xmtpServiceUrl and xmtpServiceToken
|
|
164
|
+
*/
|
|
165
|
+
export function createXmtpServiceClient(
|
|
166
|
+
serviceUrl: string,
|
|
167
|
+
serviceToken: string
|
|
168
|
+
): XmtpServiceClient {
|
|
169
|
+
if (!serviceUrl || !serviceToken) {
|
|
170
|
+
throw new Error("Missing XMTP service URL or token from runtime context")
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return new XmtpServiceClient({
|
|
174
|
+
serviceUrl,
|
|
175
|
+
serviceToken
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface XmtpAuthConfig {
|
|
180
|
+
serviceUrl: string
|
|
181
|
+
serviceToken: string
|
|
182
|
+
source: "callback" | "environment"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get XMTP authentication configuration from multiple sources
|
|
187
|
+
* Priority: callback credentials > environment credentials
|
|
188
|
+
*/
|
|
189
|
+
export function getXmtpAuthConfig(
|
|
190
|
+
callbackUrl?: string,
|
|
191
|
+
callbackToken?: string
|
|
192
|
+
): XmtpAuthConfig | null {
|
|
193
|
+
// Priority 1: Use callback credentials if available
|
|
194
|
+
if (callbackUrl && callbackToken) {
|
|
195
|
+
console.log("🔑 [XmtpAuth] Using callback-provided credentials")
|
|
196
|
+
return {
|
|
197
|
+
serviceUrl: callbackUrl,
|
|
198
|
+
serviceToken: callbackToken,
|
|
199
|
+
source: "callback"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Priority 2: Use environment credentials
|
|
204
|
+
const envUrl = process.env.XMTP_HOST
|
|
205
|
+
const envToken = process.env.XMTP_API_KEY
|
|
206
|
+
|
|
207
|
+
if (envUrl && envToken) {
|
|
208
|
+
console.log("🔑 [XmtpAuth] Using environment credentials")
|
|
209
|
+
return {
|
|
210
|
+
serviceUrl: envUrl,
|
|
211
|
+
serviceToken: envToken,
|
|
212
|
+
source: "environment"
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// No valid credentials found
|
|
217
|
+
console.error(
|
|
218
|
+
"❌ [XmtpAuth] No XMTP credentials found in callback or environment"
|
|
219
|
+
)
|
|
220
|
+
console.error(
|
|
221
|
+
"💡 [XmtpAuth] Expected: XMTP_HOST + XMTP_API_KEY or callback credentials"
|
|
222
|
+
)
|
|
223
|
+
return null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Create an authenticated XMTP service client
|
|
228
|
+
* Handles both callback and environment credential sources
|
|
229
|
+
*/
|
|
230
|
+
export function createAuthenticatedXmtpClient(
|
|
231
|
+
callbackUrl?: string,
|
|
232
|
+
callbackToken?: string
|
|
233
|
+
): XmtpServiceClient {
|
|
234
|
+
const authConfig = getXmtpAuthConfig(callbackUrl, callbackToken)
|
|
235
|
+
|
|
236
|
+
if (!authConfig) {
|
|
237
|
+
throw new Error("No XMTP credentials found")
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log(
|
|
241
|
+
`🔗 [XmtpAuth] Creating XMTP client (${authConfig.source} credentials)`
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return createXmtpServiceClient(authConfig.serviceUrl, authConfig.serviceToken)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Constructs a URL for XMTP tools API endpoints with token authentication
|
|
249
|
+
*
|
|
250
|
+
* @param {string} baseUrl - The base URL of the XMTP service (e.g., "https://api.example.com")
|
|
251
|
+
* @param {string} action - The specific action/endpoint to call (e.g., "send", "receive", "status")
|
|
252
|
+
* @param {string} token - Authentication token (either JWT or API key)
|
|
253
|
+
* @returns {string} Complete URL with token as query parameter
|
|
254
|
+
*
|
|
255
|
+
* @description
|
|
256
|
+
* Builds URLs for XMTP tools endpoints using query parameter authentication.
|
|
257
|
+
* The token is appended as a query parameter for GET request authentication,
|
|
258
|
+
* following the pattern: `/xmtp-tools/{action}?token={token}`
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```typescript
|
|
262
|
+
* const url = getXMTPToolsUrl(
|
|
263
|
+
* "https://api.hybrid.dev",
|
|
264
|
+
* "send",
|
|
265
|
+
* "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
266
|
+
* );
|
|
267
|
+
* // Returns: "https://api.hybrid.dev/xmtp-tools/send?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
268
|
+
* ```
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```typescript
|
|
272
|
+
* // Using with API key
|
|
273
|
+
* const url = getXMTPToolsUrl(
|
|
274
|
+
* process.env.XMTP_BASE_URL,
|
|
275
|
+
* "status",
|
|
276
|
+
* process.env.XMTP_API_KEY
|
|
277
|
+
* );
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
export function getXMTPToolsUrl(
|
|
281
|
+
baseUrl: string,
|
|
282
|
+
action: string,
|
|
283
|
+
token: string
|
|
284
|
+
): string {
|
|
285
|
+
return `${baseUrl}/xmtp-tools/${action}?token=${token}`
|
|
286
|
+
}
|