@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,6 @@
1
+ // ===================================================================
2
+ // Betting Configuration
3
+ // ===================================================================
4
+ export const DEFAULT_OPTIONS = ["yes", "no"]
5
+ export const DEFAULT_AMOUNT = "0.1"
6
+ export const MAX_USDC_AMOUNT = 10 // Maximum allowed USDC transaction amount
package/src/index.ts ADDED
@@ -0,0 +1,129 @@
1
+ // ===================================================================
2
+ // XMTP Package - Main Entry Point
3
+ // ===================================================================
4
+ // This package provides a clean interface to XMTP functionality
5
+ // Re-exports core XMTP SDK types and utilities
6
+
7
+ export * from "./client"
8
+ export * from "./constants"
9
+ export * from "./lib/message-listener"
10
+ export * from "./lib/subjects"
11
+ export * from "./resolver"
12
+ export * from "./resolver/basename-resolver"
13
+ export * from "./resolver/ens-resolver"
14
+ export * from "./resolver/xmtp-resolver"
15
+ export * from "./service-client"
16
+ export * from "./types"
17
+
18
+ // ===================================================================
19
+ // Enhanced XMTP Client & Connection Management
20
+ // ===================================================================
21
+ export {
22
+ createXMTPConnectionManager,
23
+ // Enhanced connection management
24
+ XMTPConnectionManager,
25
+ type XMTPConnectionConfig,
26
+ type XMTPConnectionHealth
27
+ } from "./client"
28
+
29
+ // ===================================================================
30
+ // XMTP Service Client (for external service communication)
31
+ // ===================================================================
32
+ export {
33
+ createXmtpServiceClient,
34
+ XmtpServiceClient
35
+ } from "./service-client"
36
+
37
+ // Service Client Types
38
+ export type {
39
+ GetMessageParams,
40
+ GetRootMessageParams,
41
+ // Function parameter types
42
+ SendMessageParams,
43
+ // Response types
44
+ SendMessageResponse,
45
+ SendReactionParams,
46
+ SendReactionResponse,
47
+ SendReplyParams,
48
+ SendReplyResponse,
49
+ SendTransactionParams,
50
+ SendTransactionResponse,
51
+ TransactionCall,
52
+ TransactionRequest,
53
+ XmtpRootMessageResponse,
54
+ XmtpServiceClientConfig,
55
+ XmtpServiceMessage,
56
+ XmtpServiceResponse
57
+ } from "./types"
58
+
59
+ // ===================================================================
60
+ // XMTP Core SDK Exports
61
+ // ===================================================================
62
+ export {
63
+ Client,
64
+ IdentifierKind,
65
+ // type Conversation,
66
+ type DecodedMessage,
67
+ type Dm,
68
+ // type Group,
69
+ type LogLevel,
70
+ type Signer,
71
+ type XmtpEnv
72
+ } from "@xmtp/node-sdk"
73
+
74
+ // ===================================================================
75
+ // XMTP Content Types
76
+ // ===================================================================
77
+ export {
78
+ ContentTypeTransactionReference,
79
+ type TransactionReference
80
+ } from "@xmtp/content-type-transaction-reference"
81
+
82
+ export { ContentTypeText, type TextParameters } from "@xmtp/content-type-text"
83
+
84
+ export {
85
+ ContentTypeReaction,
86
+ type Reaction
87
+ } from "@xmtp/content-type-reaction"
88
+
89
+ export {
90
+ ContentTypeReply,
91
+ ReplyCodec,
92
+ type Reply
93
+ } from "@xmtp/content-type-reply"
94
+
95
+ export {
96
+ ContentTypeGroupUpdated,
97
+ GroupUpdatedCodec,
98
+ type GroupUpdated
99
+ } from "@xmtp/content-type-group-updated"
100
+
101
+ export {
102
+ ContentTypeWalletSendCalls,
103
+ type WalletSendCallsParams
104
+ } from "@xmtp/content-type-wallet-send-calls"
105
+
106
+ // ===================================================================
107
+ // Local Client Utilities
108
+ // ===================================================================
109
+ export {
110
+ backupDbToPersistentStorage,
111
+ createSigner,
112
+ createUser,
113
+ createXMTPClient,
114
+ diagnoseXMTPIdentityIssue,
115
+ generateEncryptionKeyHex,
116
+ getEncryptionKeyFromHex,
117
+ logAgentDetails,
118
+ startPeriodicBackup,
119
+ validateEnvironment
120
+ } from "./client"
121
+
122
+ // ===================================================================
123
+ // Application Constants
124
+ // ===================================================================
125
+ export {
126
+ DEFAULT_AMOUNT,
127
+ DEFAULT_OPTIONS,
128
+ MAX_USDC_AMOUNT
129
+ } from "./constants"
@@ -0,0 +1,369 @@
1
+ import { EventEmitter } from "node:events"
2
+ import { beforeEach, describe, expect, it, vi } from "vitest"
3
+ import { MessageListener } from "./message-listener"
4
+
5
+ /**
6
+ * Check if content contains any of the supported agent mention patterns
7
+ */
8
+ function hasAgentMention(content: string | undefined): boolean {
9
+ if (!content) return false
10
+
11
+ const lowerContent = content.toLowerCase()
12
+ const mentionPatterns = [
13
+ "@agent",
14
+ "@hybrid",
15
+ "@hybrid.base.eth",
16
+ "@hybrid.eth",
17
+ "@agent.eth",
18
+ "@agent.base.eth"
19
+ ]
20
+
21
+ return mentionPatterns.some((pattern) => lowerContent.includes(pattern))
22
+ }
23
+
24
+ // Mock the XmtpResolver
25
+ vi.mock("./xmtp-resolver", () => ({
26
+ XmtpResolver: vi.fn().mockImplementation(() => ({
27
+ resolveAddress: vi.fn().mockResolvedValue("0x456789abcdef"),
28
+ findRootMessage: vi.fn().mockResolvedValue(null),
29
+ prePopulateCache: vi.fn().mockResolvedValue(undefined)
30
+ }))
31
+ }))
32
+
33
+ // Mock the BasenameResolver
34
+ vi.mock("./basename-resolver", () => ({
35
+ BasenameResolver: vi.fn().mockImplementation(() => ({
36
+ getBasename: vi.fn().mockResolvedValue("testuser.base.eth"),
37
+ getBasenameAddress: vi.fn().mockResolvedValue("0x456789abcdef"),
38
+ resolveBasenameProfile: vi.fn().mockResolvedValue({
39
+ basename: "testuser.base.eth",
40
+ avatar: "https://example.com/avatar.jpg",
41
+ description: "Test user profile",
42
+ twitter: "@testuser",
43
+ github: "testuser",
44
+ url: "https://testuser.com"
45
+ })
46
+ }))
47
+ }))
48
+
49
+ // Mock the ENSResolver
50
+ vi.mock("./ens-resolver", () => ({
51
+ ENSResolver: vi.fn().mockImplementation(() => ({
52
+ resolveAddressToENS: vi.fn().mockResolvedValue(null),
53
+ resolveENSName: vi.fn().mockResolvedValue(null),
54
+ isENSName: vi.fn().mockReturnValue(false)
55
+ }))
56
+ }))
57
+
58
+ // Mock the subjects
59
+ vi.mock("./subjects", () => ({
60
+ extractSubjects: vi.fn().mockResolvedValue({})
61
+ }))
62
+
63
+ // Mock the XMTP client
64
+ const mockClient = {
65
+ inboxId: "test-inbox-id",
66
+ accountIdentifier: { identifier: "0x123" },
67
+ conversations: {
68
+ sync: vi.fn(),
69
+ list: vi.fn().mockResolvedValue([]),
70
+ streamAllMessages: vi.fn(),
71
+ getConversationById: vi.fn()
72
+ },
73
+ preferences: {
74
+ inboxStateFromInboxIds: vi.fn()
75
+ }
76
+ }
77
+
78
+ describe("MessageListener", () => {
79
+ let listener: MessageListener
80
+
81
+ beforeEach(() => {
82
+ vi.clearAllMocks()
83
+ listener = new MessageListener({
84
+ xmtpClient: mockClient as any,
85
+ publicClient: {} as any,
86
+ filter: ({ message }) => {
87
+ const content = message.content as string
88
+ return hasAgentMention(content)
89
+ }
90
+ })
91
+ })
92
+
93
+ it("should be an instance of EventEmitter", () => {
94
+ expect(listener).toBeInstanceOf(EventEmitter)
95
+ })
96
+
97
+ it("should emit message events with enriched sender information", async () => {
98
+ const mockMessage = {
99
+ id: "test-message-id",
100
+ content: "@agent test message",
101
+ senderInboxId: "sender-inbox-id",
102
+ conversationId: "conversation-id",
103
+ sentAt: new Date(),
104
+ contentType: { typeId: "text" }
105
+ }
106
+
107
+ // Mock the stream to emit our test message
108
+ const mockStream = {
109
+ async *[Symbol.asyncIterator]() {
110
+ yield mockMessage
111
+ }
112
+ }
113
+
114
+ mockClient.conversations.streamAllMessages.mockResolvedValue(mockStream)
115
+ mockClient.conversations.getConversationById.mockResolvedValue({
116
+ id: "conversation-id"
117
+ })
118
+
119
+ // Set up message event listener
120
+ const messageHandler = vi.fn()
121
+ listener.on("message", messageHandler)
122
+
123
+ // Start the listener (but don't wait for it to complete since it runs indefinitely)
124
+ const startPromise = listener.start()
125
+
126
+ // Give it a moment to process the message
127
+ await new Promise((resolve) => setTimeout(resolve, 100))
128
+
129
+ expect(messageHandler).toHaveBeenCalledWith(
130
+ expect.objectContaining({
131
+ message: expect.objectContaining({
132
+ id: "test-message-id",
133
+ content: "@agent test message",
134
+ senderInboxId: "sender-inbox-id",
135
+ conversationId: "conversation-id"
136
+ }),
137
+ sender: expect.objectContaining({
138
+ address: "0x456789abcdef",
139
+ inboxId: "sender-inbox-id",
140
+ basename: "testuser.base.eth",
141
+ name: "testuser.base.eth"
142
+ }),
143
+ subjects: expect.any(Object),
144
+ rootMessage: undefined
145
+ })
146
+ )
147
+
148
+ listener.stop()
149
+ })
150
+
151
+ it("should handle messages without basenames gracefully", async () => {
152
+ // Mock resolvers to return no basename
153
+ const listenerWithoutBasename = new MessageListener({
154
+ xmtpClient: mockClient as any,
155
+ publicClient: {} as any,
156
+ filter: ({ message }) => {
157
+ const content = message.content as string
158
+ return hasAgentMention(content)
159
+ }
160
+ })
161
+
162
+ // Mock basename resolver to return null
163
+ const mockBasenameResolver = (listenerWithoutBasename as any)
164
+ .basenameResolver
165
+ mockBasenameResolver.getBasename = vi.fn().mockResolvedValue(null)
166
+
167
+ const mockMessage = {
168
+ id: "test-message-id-2",
169
+ content: "@agent test message 2",
170
+ senderInboxId: "sender-inbox-id-2",
171
+ conversationId: "conversation-id-2",
172
+ sentAt: new Date(),
173
+ contentType: { typeId: "text" }
174
+ }
175
+
176
+ const mockStream = {
177
+ async *[Symbol.asyncIterator]() {
178
+ yield mockMessage
179
+ }
180
+ }
181
+
182
+ mockClient.conversations.streamAllMessages.mockResolvedValue(mockStream)
183
+ mockClient.conversations.getConversationById.mockResolvedValue({
184
+ id: "conversation-id-2"
185
+ })
186
+
187
+ const messageHandler = vi.fn()
188
+ listenerWithoutBasename.on("message", messageHandler)
189
+
190
+ const startPromise = listenerWithoutBasename.start()
191
+ await new Promise((resolve) => setTimeout(resolve, 100))
192
+
193
+ expect(messageHandler).toHaveBeenCalledWith(
194
+ expect.objectContaining({
195
+ sender: expect.objectContaining({
196
+ address: "0x456789abcdef",
197
+ inboxId: "sender-inbox-id-2",
198
+ basename: undefined,
199
+ name: expect.stringContaining("0x4567") // Should use truncated address
200
+ }),
201
+ subjects: expect.any(Object),
202
+ rootMessage: undefined
203
+ })
204
+ )
205
+
206
+ listenerWithoutBasename.stop()
207
+ })
208
+
209
+ it("should handle all supported agent mention patterns", async () => {
210
+ const mentionPatterns = [
211
+ "@agent test message",
212
+ "@hybrid test message",
213
+ "@hybrid.base.eth test message",
214
+ "@hybrid.eth test message",
215
+ "@agent.eth test message",
216
+ "@agent.base.eth test message"
217
+ ]
218
+
219
+ for (const content of mentionPatterns) {
220
+ const testListener = new MessageListener({
221
+ xmtpClient: mockClient as any,
222
+ publicClient: {} as any,
223
+ filter: ({ message }) => {
224
+ const messageContent = message.content as string
225
+ return hasAgentMention(messageContent)
226
+ }
227
+ })
228
+
229
+ const mockMessage = {
230
+ id: `test-message-${content}`,
231
+ content: content,
232
+ senderInboxId: "sender-inbox-id",
233
+ conversationId: "conversation-id",
234
+ sentAt: new Date(),
235
+ contentType: { typeId: "text" }
236
+ }
237
+
238
+ const mockStream = {
239
+ async *[Symbol.asyncIterator]() {
240
+ yield mockMessage
241
+ }
242
+ }
243
+
244
+ mockClient.conversations.streamAllMessages.mockResolvedValue(mockStream)
245
+ mockClient.conversations.getConversationById.mockResolvedValue({
246
+ id: "conversation-id"
247
+ })
248
+
249
+ const messageHandler = vi.fn()
250
+ testListener.on("message", messageHandler)
251
+
252
+ const startPromise = testListener.start()
253
+ await new Promise((resolve) => setTimeout(resolve, 100))
254
+
255
+ expect(messageHandler).toHaveBeenCalledWith(
256
+ expect.objectContaining({
257
+ message: expect.objectContaining({
258
+ content: content
259
+ })
260
+ })
261
+ )
262
+
263
+ testListener.stop()
264
+ }
265
+ })
266
+
267
+ it("should allow replies without mention checking", async () => {
268
+ const testListener = new MessageListener({
269
+ xmtpClient: mockClient as any,
270
+ publicClient: {} as any,
271
+ filter: ({ message }) => {
272
+ const contentTypeId = message.contentType?.typeId
273
+ if (contentTypeId === "reply") {
274
+ return true
275
+ }
276
+ if (contentTypeId === "text") {
277
+ const messageContent = message.content as string
278
+ return hasAgentMention(messageContent)
279
+ }
280
+ return false
281
+ }
282
+ })
283
+
284
+ const mockReplyMessage = {
285
+ id: "test-reply-message",
286
+ content: { content: "yes I'm in" },
287
+ senderInboxId: "sender-inbox-id",
288
+ conversationId: "conversation-id",
289
+ sentAt: new Date(),
290
+ contentType: { typeId: "reply" }
291
+ }
292
+
293
+ const mockStream = {
294
+ async *[Symbol.asyncIterator]() {
295
+ yield mockReplyMessage
296
+ }
297
+ }
298
+
299
+ mockClient.conversations.streamAllMessages.mockResolvedValue(mockStream)
300
+ mockClient.conversations.getConversationById.mockResolvedValue({
301
+ id: "conversation-id"
302
+ })
303
+
304
+ const messageHandler = vi.fn()
305
+ testListener.on("message", messageHandler)
306
+
307
+ const startPromise = testListener.start()
308
+ await new Promise((resolve) => setTimeout(resolve, 100))
309
+
310
+ expect(messageHandler).toHaveBeenCalledWith(
311
+ expect.objectContaining({
312
+ message: expect.objectContaining({
313
+ content: expect.objectContaining({
314
+ content: "yes I'm in"
315
+ })
316
+ })
317
+ })
318
+ )
319
+
320
+ testListener.stop()
321
+ })
322
+
323
+ it("should properly clean up when stopped", () => {
324
+ const removeAllListenersSpy = vi.spyOn(listener, "removeAllListeners")
325
+
326
+ listener.stop()
327
+
328
+ expect(removeAllListenersSpy).toHaveBeenCalled()
329
+ })
330
+
331
+ it("should get stats", () => {
332
+ const stats = listener.getStats()
333
+
334
+ expect(stats).toEqual({
335
+ messageCount: 0,
336
+ conversationCount: 0,
337
+ isActive: false
338
+ })
339
+ })
340
+
341
+ it("should emit started and stopped events", async () => {
342
+ const startedHandler = vi.fn()
343
+ const stoppedHandler = vi.fn()
344
+
345
+ listener.on("started", startedHandler)
346
+ listener.on("stopped", stoppedHandler)
347
+
348
+ // Mock to prevent infinite stream
349
+ mockClient.conversations.streamAllMessages.mockResolvedValue({
350
+ async *[Symbol.asyncIterator]() {
351
+ // Empty iterator that ends immediately
352
+ }
353
+ })
354
+
355
+ await listener.start()
356
+
357
+ // Give a moment for the events to be processed
358
+ await new Promise((resolve) => setTimeout(resolve, 10))
359
+
360
+ expect(startedHandler).toHaveBeenCalled()
361
+
362
+ listener.stop()
363
+
364
+ // Give a moment for the stop event to be processed
365
+ await new Promise((resolve) => setTimeout(resolve, 10))
366
+
367
+ expect(stoppedHandler).toHaveBeenCalled()
368
+ })
369
+ })