@hybrd/xmtp 1.0.0 → 1.0.3
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 -1
- package/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-lint$colon$fix.log +6 -0
- package/dist/scripts/generate-keys.d.ts +1 -0
- package/dist/scripts/generate-keys.js +19 -0
- package/dist/scripts/refresh-identity.d.ts +1 -0
- package/dist/scripts/refresh-identity.js +93 -0
- package/dist/scripts/register-wallet.d.ts +1 -0
- package/dist/scripts/register-wallet.js +75 -0
- package/dist/scripts/revoke-all-installations.d.ts +2 -0
- package/dist/scripts/revoke-all-installations.js +68 -0
- package/dist/scripts/revoke-installations.d.ts +2 -0
- package/dist/scripts/revoke-installations.js +62 -0
- package/dist/src/abi/l2_resolver.d.ts +992 -0
- package/dist/src/abi/l2_resolver.js +699 -0
- package/dist/src/client.d.ts +76 -0
- package/dist/src/client.js +709 -0
- package/dist/src/constants.d.ts +3 -0
- package/dist/src/constants.js +6 -0
- package/dist/src/index.d.ts +22 -0
- package/dist/src/index.js +46 -0
- package/dist/src/lib/message-listener.d.ts +69 -0
- package/dist/src/lib/message-listener.js +235 -0
- package/dist/src/lib/message-listener.test.d.ts +1 -0
- package/dist/src/lib/message-listener.test.js +303 -0
- package/dist/src/lib/subjects.d.ts +24 -0
- package/dist/src/lib/subjects.js +68 -0
- package/dist/src/resolver/address-resolver.d.ts +57 -0
- package/dist/src/resolver/address-resolver.js +168 -0
- package/dist/src/resolver/basename-resolver.d.ts +134 -0
- package/dist/src/resolver/basename-resolver.js +409 -0
- package/dist/src/resolver/ens-resolver.d.ts +95 -0
- package/dist/src/resolver/ens-resolver.js +249 -0
- package/dist/src/resolver/index.d.ts +1 -0
- package/dist/src/resolver/index.js +1 -0
- package/dist/src/resolver/resolver.d.ts +162 -0
- package/dist/src/resolver/resolver.js +238 -0
- package/dist/src/resolver/xmtp-resolver.d.ts +95 -0
- package/dist/src/resolver/xmtp-resolver.js +297 -0
- package/dist/src/service-client.d.ts +77 -0
- package/dist/src/service-client.js +198 -0
- package/dist/src/types.d.ts +123 -0
- package/dist/src/types.js +5 -0
- package/package.json +5 -4
- package/tsconfig.json +3 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { MessageListener } from "./message-listener";
|
|
4
|
+
/**
|
|
5
|
+
* Check if content contains any of the supported agent mention patterns
|
|
6
|
+
*/
|
|
7
|
+
function hasAgentMention(content) {
|
|
8
|
+
if (!content)
|
|
9
|
+
return false;
|
|
10
|
+
const lowerContent = content.toLowerCase();
|
|
11
|
+
const mentionPatterns = [
|
|
12
|
+
"@agent",
|
|
13
|
+
"@hybrid",
|
|
14
|
+
"@hybrid.base.eth",
|
|
15
|
+
"@hybrid.eth",
|
|
16
|
+
"@agent.eth",
|
|
17
|
+
"@agent.base.eth"
|
|
18
|
+
];
|
|
19
|
+
return mentionPatterns.some((pattern) => lowerContent.includes(pattern));
|
|
20
|
+
}
|
|
21
|
+
// Mock the XmtpResolver
|
|
22
|
+
vi.mock("./xmtp-resolver", () => ({
|
|
23
|
+
XmtpResolver: vi.fn().mockImplementation(() => ({
|
|
24
|
+
resolveAddress: vi.fn().mockResolvedValue("0x456789abcdef"),
|
|
25
|
+
findRootMessage: vi.fn().mockResolvedValue(null),
|
|
26
|
+
prePopulateCache: vi.fn().mockResolvedValue(undefined)
|
|
27
|
+
}))
|
|
28
|
+
}));
|
|
29
|
+
// Mock the BasenameResolver
|
|
30
|
+
vi.mock("./basename-resolver", () => ({
|
|
31
|
+
BasenameResolver: vi.fn().mockImplementation(() => ({
|
|
32
|
+
getBasename: vi.fn().mockResolvedValue("testuser.base.eth"),
|
|
33
|
+
getBasenameAddress: vi.fn().mockResolvedValue("0x456789abcdef"),
|
|
34
|
+
resolveBasenameProfile: vi.fn().mockResolvedValue({
|
|
35
|
+
basename: "testuser.base.eth",
|
|
36
|
+
avatar: "https://example.com/avatar.jpg",
|
|
37
|
+
description: "Test user profile",
|
|
38
|
+
twitter: "@testuser",
|
|
39
|
+
github: "testuser",
|
|
40
|
+
url: "https://testuser.com"
|
|
41
|
+
})
|
|
42
|
+
}))
|
|
43
|
+
}));
|
|
44
|
+
// Mock the ENSResolver
|
|
45
|
+
vi.mock("./ens-resolver", () => ({
|
|
46
|
+
ENSResolver: vi.fn().mockImplementation(() => ({
|
|
47
|
+
resolveAddressToENS: vi.fn().mockResolvedValue(null),
|
|
48
|
+
resolveENSName: vi.fn().mockResolvedValue(null),
|
|
49
|
+
isENSName: vi.fn().mockReturnValue(false)
|
|
50
|
+
}))
|
|
51
|
+
}));
|
|
52
|
+
// Mock the subjects
|
|
53
|
+
vi.mock("./subjects", () => ({
|
|
54
|
+
extractSubjects: vi.fn().mockResolvedValue({})
|
|
55
|
+
}));
|
|
56
|
+
// Mock the XMTP client
|
|
57
|
+
const mockClient = {
|
|
58
|
+
inboxId: "test-inbox-id",
|
|
59
|
+
accountIdentifier: { identifier: "0x123" },
|
|
60
|
+
conversations: {
|
|
61
|
+
sync: vi.fn(),
|
|
62
|
+
list: vi.fn().mockResolvedValue([]),
|
|
63
|
+
streamAllMessages: vi.fn(),
|
|
64
|
+
getConversationById: vi.fn()
|
|
65
|
+
},
|
|
66
|
+
preferences: {
|
|
67
|
+
inboxStateFromInboxIds: vi.fn()
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
describe("MessageListener", () => {
|
|
71
|
+
let listener;
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
vi.clearAllMocks();
|
|
74
|
+
listener = new MessageListener({
|
|
75
|
+
xmtpClient: mockClient,
|
|
76
|
+
publicClient: {},
|
|
77
|
+
filter: ({ message }) => {
|
|
78
|
+
const content = message.content;
|
|
79
|
+
return hasAgentMention(content);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
it("should be an instance of EventEmitter", () => {
|
|
84
|
+
expect(listener).toBeInstanceOf(EventEmitter);
|
|
85
|
+
});
|
|
86
|
+
it("should emit message events with enriched sender information", async () => {
|
|
87
|
+
const mockMessage = {
|
|
88
|
+
id: "test-message-id",
|
|
89
|
+
content: "@agent test message",
|
|
90
|
+
senderInboxId: "sender-inbox-id",
|
|
91
|
+
conversationId: "conversation-id",
|
|
92
|
+
sentAt: new Date(),
|
|
93
|
+
contentType: { typeId: "text" }
|
|
94
|
+
};
|
|
95
|
+
// Mock the stream to emit our test message
|
|
96
|
+
const mockStream = {
|
|
97
|
+
async *[Symbol.asyncIterator]() {
|
|
98
|
+
yield mockMessage;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
mockClient.conversations.streamAllMessages.mockResolvedValue(mockStream);
|
|
102
|
+
mockClient.conversations.getConversationById.mockResolvedValue({
|
|
103
|
+
id: "conversation-id"
|
|
104
|
+
});
|
|
105
|
+
// Set up message event listener
|
|
106
|
+
const messageHandler = vi.fn();
|
|
107
|
+
listener.on("message", messageHandler);
|
|
108
|
+
// Start the listener (but don't wait for it to complete since it runs indefinitely)
|
|
109
|
+
const startPromise = listener.start();
|
|
110
|
+
// Give it a moment to process the message
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
112
|
+
expect(messageHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
113
|
+
message: expect.objectContaining({
|
|
114
|
+
id: "test-message-id",
|
|
115
|
+
content: "@agent test message",
|
|
116
|
+
senderInboxId: "sender-inbox-id",
|
|
117
|
+
conversationId: "conversation-id"
|
|
118
|
+
}),
|
|
119
|
+
sender: expect.objectContaining({
|
|
120
|
+
address: "0x456789abcdef",
|
|
121
|
+
inboxId: "sender-inbox-id",
|
|
122
|
+
basename: "testuser.base.eth",
|
|
123
|
+
name: "testuser.base.eth"
|
|
124
|
+
}),
|
|
125
|
+
subjects: expect.any(Object),
|
|
126
|
+
rootMessage: undefined
|
|
127
|
+
}));
|
|
128
|
+
listener.stop();
|
|
129
|
+
});
|
|
130
|
+
it("should handle messages without basenames gracefully", async () => {
|
|
131
|
+
// Mock resolvers to return no basename
|
|
132
|
+
const listenerWithoutBasename = new MessageListener({
|
|
133
|
+
xmtpClient: mockClient,
|
|
134
|
+
publicClient: {},
|
|
135
|
+
filter: ({ message }) => {
|
|
136
|
+
const content = message.content;
|
|
137
|
+
return hasAgentMention(content);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// Mock basename resolver to return null
|
|
141
|
+
const mockBasenameResolver = listenerWithoutBasename
|
|
142
|
+
.basenameResolver;
|
|
143
|
+
mockBasenameResolver.getBasename = vi.fn().mockResolvedValue(null);
|
|
144
|
+
const mockMessage = {
|
|
145
|
+
id: "test-message-id-2",
|
|
146
|
+
content: "@agent test message 2",
|
|
147
|
+
senderInboxId: "sender-inbox-id-2",
|
|
148
|
+
conversationId: "conversation-id-2",
|
|
149
|
+
sentAt: new Date(),
|
|
150
|
+
contentType: { typeId: "text" }
|
|
151
|
+
};
|
|
152
|
+
const mockStream = {
|
|
153
|
+
async *[Symbol.asyncIterator]() {
|
|
154
|
+
yield mockMessage;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
mockClient.conversations.streamAllMessages.mockResolvedValue(mockStream);
|
|
158
|
+
mockClient.conversations.getConversationById.mockResolvedValue({
|
|
159
|
+
id: "conversation-id-2"
|
|
160
|
+
});
|
|
161
|
+
const messageHandler = vi.fn();
|
|
162
|
+
listenerWithoutBasename.on("message", messageHandler);
|
|
163
|
+
const startPromise = listenerWithoutBasename.start();
|
|
164
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
165
|
+
expect(messageHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
166
|
+
sender: expect.objectContaining({
|
|
167
|
+
address: "0x456789abcdef",
|
|
168
|
+
inboxId: "sender-inbox-id-2",
|
|
169
|
+
basename: undefined,
|
|
170
|
+
name: expect.stringContaining("0x4567") // Should use truncated address
|
|
171
|
+
}),
|
|
172
|
+
subjects: expect.any(Object),
|
|
173
|
+
rootMessage: undefined
|
|
174
|
+
}));
|
|
175
|
+
listenerWithoutBasename.stop();
|
|
176
|
+
});
|
|
177
|
+
it("should handle all supported agent mention patterns", async () => {
|
|
178
|
+
const mentionPatterns = [
|
|
179
|
+
"@agent test message",
|
|
180
|
+
"@hybrid test message",
|
|
181
|
+
"@hybrid.base.eth test message",
|
|
182
|
+
"@hybrid.eth test message",
|
|
183
|
+
"@agent.eth test message",
|
|
184
|
+
"@agent.base.eth test message"
|
|
185
|
+
];
|
|
186
|
+
for (const content of mentionPatterns) {
|
|
187
|
+
const testListener = new MessageListener({
|
|
188
|
+
xmtpClient: mockClient,
|
|
189
|
+
publicClient: {},
|
|
190
|
+
filter: ({ message }) => {
|
|
191
|
+
const messageContent = message.content;
|
|
192
|
+
return hasAgentMention(messageContent);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
const mockMessage = {
|
|
196
|
+
id: `test-message-${content}`,
|
|
197
|
+
content: content,
|
|
198
|
+
senderInboxId: "sender-inbox-id",
|
|
199
|
+
conversationId: "conversation-id",
|
|
200
|
+
sentAt: new Date(),
|
|
201
|
+
contentType: { typeId: "text" }
|
|
202
|
+
};
|
|
203
|
+
const mockStream = {
|
|
204
|
+
async *[Symbol.asyncIterator]() {
|
|
205
|
+
yield mockMessage;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
mockClient.conversations.streamAllMessages.mockResolvedValue(mockStream);
|
|
209
|
+
mockClient.conversations.getConversationById.mockResolvedValue({
|
|
210
|
+
id: "conversation-id"
|
|
211
|
+
});
|
|
212
|
+
const messageHandler = vi.fn();
|
|
213
|
+
testListener.on("message", messageHandler);
|
|
214
|
+
const startPromise = testListener.start();
|
|
215
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
216
|
+
expect(messageHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
217
|
+
message: expect.objectContaining({
|
|
218
|
+
content: content
|
|
219
|
+
})
|
|
220
|
+
}));
|
|
221
|
+
testListener.stop();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
it("should allow replies without mention checking", async () => {
|
|
225
|
+
const testListener = new MessageListener({
|
|
226
|
+
xmtpClient: mockClient,
|
|
227
|
+
publicClient: {},
|
|
228
|
+
filter: ({ message }) => {
|
|
229
|
+
const contentTypeId = message.contentType?.typeId;
|
|
230
|
+
if (contentTypeId === "reply") {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
if (contentTypeId === "text") {
|
|
234
|
+
const messageContent = message.content;
|
|
235
|
+
return hasAgentMention(messageContent);
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
const mockReplyMessage = {
|
|
241
|
+
id: "test-reply-message",
|
|
242
|
+
content: { content: "yes I'm in" },
|
|
243
|
+
senderInboxId: "sender-inbox-id",
|
|
244
|
+
conversationId: "conversation-id",
|
|
245
|
+
sentAt: new Date(),
|
|
246
|
+
contentType: { typeId: "reply" }
|
|
247
|
+
};
|
|
248
|
+
const mockStream = {
|
|
249
|
+
async *[Symbol.asyncIterator]() {
|
|
250
|
+
yield mockReplyMessage;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
mockClient.conversations.streamAllMessages.mockResolvedValue(mockStream);
|
|
254
|
+
mockClient.conversations.getConversationById.mockResolvedValue({
|
|
255
|
+
id: "conversation-id"
|
|
256
|
+
});
|
|
257
|
+
const messageHandler = vi.fn();
|
|
258
|
+
testListener.on("message", messageHandler);
|
|
259
|
+
const startPromise = testListener.start();
|
|
260
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
261
|
+
expect(messageHandler).toHaveBeenCalledWith(expect.objectContaining({
|
|
262
|
+
message: expect.objectContaining({
|
|
263
|
+
content: expect.objectContaining({
|
|
264
|
+
content: "yes I'm in"
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
}));
|
|
268
|
+
testListener.stop();
|
|
269
|
+
});
|
|
270
|
+
it("should properly clean up when stopped", () => {
|
|
271
|
+
const removeAllListenersSpy = vi.spyOn(listener, "removeAllListeners");
|
|
272
|
+
listener.stop();
|
|
273
|
+
expect(removeAllListenersSpy).toHaveBeenCalled();
|
|
274
|
+
});
|
|
275
|
+
it("should get stats", () => {
|
|
276
|
+
const stats = listener.getStats();
|
|
277
|
+
expect(stats).toEqual({
|
|
278
|
+
messageCount: 0,
|
|
279
|
+
conversationCount: 0,
|
|
280
|
+
isActive: false
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
it("should emit started and stopped events", async () => {
|
|
284
|
+
const startedHandler = vi.fn();
|
|
285
|
+
const stoppedHandler = vi.fn();
|
|
286
|
+
listener.on("started", startedHandler);
|
|
287
|
+
listener.on("stopped", stoppedHandler);
|
|
288
|
+
// Mock to prevent infinite stream
|
|
289
|
+
mockClient.conversations.streamAllMessages.mockResolvedValue({
|
|
290
|
+
async *[Symbol.asyncIterator]() {
|
|
291
|
+
// Empty iterator that ends immediately
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
await listener.start();
|
|
295
|
+
// Give a moment for the events to be processed
|
|
296
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
297
|
+
expect(startedHandler).toHaveBeenCalled();
|
|
298
|
+
listener.stop();
|
|
299
|
+
// Give a moment for the stop event to be processed
|
|
300
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
301
|
+
expect(stoppedHandler).toHaveBeenCalled();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BasenameResolver } from "../resolver/basename-resolver";
|
|
2
|
+
import type { ENSResolver } from "../resolver/ens-resolver";
|
|
3
|
+
/**
|
|
4
|
+
* Extract basenames/ENS names from message content using @mention pattern
|
|
5
|
+
* @param content The message content to parse
|
|
6
|
+
* @returns Array of unique names found in the message
|
|
7
|
+
*/
|
|
8
|
+
export declare function extractMentionedNames(content: string): string[];
|
|
9
|
+
/**
|
|
10
|
+
* Resolve mentioned names to addresses and return as subjects object
|
|
11
|
+
* @param mentionedNames Array of names to resolve
|
|
12
|
+
* @param basenameResolver Basename resolver instance
|
|
13
|
+
* @param ensResolver ENS resolver instance
|
|
14
|
+
* @returns Promise that resolves to subjects object mapping names to addresses
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveSubjects(mentionedNames: string[], basenameResolver: BasenameResolver, ensResolver: ENSResolver): Promise<Record<string, `0x${string}`>>;
|
|
17
|
+
/**
|
|
18
|
+
* Extract subjects from message content (combines extraction and resolution)
|
|
19
|
+
* @param content The message content to parse
|
|
20
|
+
* @param basenameResolver Basename resolver instance
|
|
21
|
+
* @param ensResolver ENS resolver instance
|
|
22
|
+
* @returns Promise that resolves to subjects object mapping names to addresses
|
|
23
|
+
*/
|
|
24
|
+
export declare function extractSubjects(content: string, basenameResolver: BasenameResolver, ensResolver: ENSResolver): Promise<Record<string, `0x${string}`>>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract basenames/ENS names from message content using @mention pattern
|
|
3
|
+
* @param content The message content to parse
|
|
4
|
+
* @returns Array of unique names found in the message
|
|
5
|
+
*/
|
|
6
|
+
export function extractMentionedNames(content) {
|
|
7
|
+
// Match @basename.eth and @basename.base.eth patterns (case insensitive)
|
|
8
|
+
const nameRegex = /@([a-zA-Z0-9-_]+\.(?:base\.)?eth)\b/gi;
|
|
9
|
+
const matches = content.match(nameRegex);
|
|
10
|
+
if (!matches) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
// Remove @ symbol and deduplicate
|
|
14
|
+
const names = matches.map((match) => match.slice(1).toLowerCase());
|
|
15
|
+
return [...new Set(names)];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve mentioned names to addresses and return as subjects object
|
|
19
|
+
* @param mentionedNames Array of names to resolve
|
|
20
|
+
* @param basenameResolver Basename resolver instance
|
|
21
|
+
* @param ensResolver ENS resolver instance
|
|
22
|
+
* @returns Promise that resolves to subjects object mapping names to addresses
|
|
23
|
+
*/
|
|
24
|
+
export async function resolveSubjects(mentionedNames, basenameResolver, ensResolver) {
|
|
25
|
+
const subjects = {};
|
|
26
|
+
if (mentionedNames.length === 0) {
|
|
27
|
+
return subjects;
|
|
28
|
+
}
|
|
29
|
+
console.log(`🔍 Found ${mentionedNames.length} name mentions:`, mentionedNames);
|
|
30
|
+
for (const mentionedName of mentionedNames) {
|
|
31
|
+
try {
|
|
32
|
+
let resolvedAddress = null;
|
|
33
|
+
// Check if it's an ENS name (.eth but not .base.eth)
|
|
34
|
+
if (ensResolver.isENSName(mentionedName)) {
|
|
35
|
+
console.log(`🔍 Resolving ENS name: ${mentionedName}`);
|
|
36
|
+
resolvedAddress = await ensResolver.resolveENSName(mentionedName);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// It's a basename (.base.eth or other format)
|
|
40
|
+
console.log(`🔍 Resolving basename: ${mentionedName}`);
|
|
41
|
+
resolvedAddress =
|
|
42
|
+
await basenameResolver.getBasenameAddress(mentionedName);
|
|
43
|
+
}
|
|
44
|
+
if (resolvedAddress) {
|
|
45
|
+
subjects[mentionedName] = resolvedAddress;
|
|
46
|
+
console.log(`✅ Resolved ${mentionedName} → ${resolvedAddress}`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.log(`❌ Could not resolve address for: ${mentionedName}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.error(`❌ Error resolving ${mentionedName}:`, error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return subjects;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Extract subjects from message content (combines extraction and resolution)
|
|
60
|
+
* @param content The message content to parse
|
|
61
|
+
* @param basenameResolver Basename resolver instance
|
|
62
|
+
* @param ensResolver ENS resolver instance
|
|
63
|
+
* @returns Promise that resolves to subjects object mapping names to addresses
|
|
64
|
+
*/
|
|
65
|
+
export async function extractSubjects(content, basenameResolver, ensResolver) {
|
|
66
|
+
const mentionedNames = extractMentionedNames(content);
|
|
67
|
+
return await resolveSubjects(mentionedNames, basenameResolver, ensResolver);
|
|
68
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { XmtpClient } from "../types";
|
|
2
|
+
interface AddressResolverOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Maximum number of addresses to cache
|
|
5
|
+
* @default 1000
|
|
6
|
+
*/
|
|
7
|
+
maxCacheSize?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Cache TTL in milliseconds
|
|
10
|
+
* @default 86400000 (24 hours)
|
|
11
|
+
*/
|
|
12
|
+
cacheTtl?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare class AddressResolver {
|
|
15
|
+
private client;
|
|
16
|
+
private cache;
|
|
17
|
+
private readonly maxCacheSize;
|
|
18
|
+
private readonly cacheTtl;
|
|
19
|
+
constructor(client: XmtpClient, options?: AddressResolverOptions);
|
|
20
|
+
/**
|
|
21
|
+
* Resolve user address from inbox ID with caching
|
|
22
|
+
*/
|
|
23
|
+
resolveAddress(inboxId: string, conversationId?: string): Promise<`0x${string}` | null>;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve address from conversation members
|
|
26
|
+
*/
|
|
27
|
+
private resolveFromConversation;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve address from inbox state (network fallback)
|
|
30
|
+
*/
|
|
31
|
+
private resolveFromInboxState;
|
|
32
|
+
/**
|
|
33
|
+
* Get cached address if not expired
|
|
34
|
+
*/
|
|
35
|
+
private getCachedAddress;
|
|
36
|
+
/**
|
|
37
|
+
* Cache address with LRU eviction
|
|
38
|
+
*/
|
|
39
|
+
private setCachedAddress;
|
|
40
|
+
/**
|
|
41
|
+
* Pre-populate cache from existing conversations
|
|
42
|
+
*/
|
|
43
|
+
prePopulateCache(): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Clear the cache
|
|
46
|
+
*/
|
|
47
|
+
clearCache(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Get cache statistics
|
|
50
|
+
*/
|
|
51
|
+
getCacheStats(): {
|
|
52
|
+
size: number;
|
|
53
|
+
maxSize: number;
|
|
54
|
+
hitRate?: number;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
export class AddressResolver {
|
|
2
|
+
constructor(client, options = {}) {
|
|
3
|
+
this.client = client;
|
|
4
|
+
this.cache = new Map();
|
|
5
|
+
this.maxCacheSize = options.maxCacheSize ?? 1000;
|
|
6
|
+
this.cacheTtl = options.cacheTtl ?? 86400000; // 24 hours
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Resolve user address from inbox ID with caching
|
|
10
|
+
*/
|
|
11
|
+
async resolveAddress(inboxId, conversationId) {
|
|
12
|
+
// Check cache first (fastest)
|
|
13
|
+
const cached = this.getCachedAddress(inboxId);
|
|
14
|
+
if (cached) {
|
|
15
|
+
console.log(`✅ Resolved user address from cache: ${cached}`);
|
|
16
|
+
return cached;
|
|
17
|
+
}
|
|
18
|
+
let userAddress = undefined;
|
|
19
|
+
try {
|
|
20
|
+
// Try conversation members lookup first (faster than network call)
|
|
21
|
+
if (conversationId) {
|
|
22
|
+
const conversation = await this.client.conversations.getConversationById(conversationId);
|
|
23
|
+
if (conversation) {
|
|
24
|
+
userAddress = await this.resolveFromConversation(conversation, inboxId);
|
|
25
|
+
if (userAddress) {
|
|
26
|
+
this.setCachedAddress(inboxId, userAddress);
|
|
27
|
+
console.log(`✅ Resolved user address: ${userAddress}`);
|
|
28
|
+
return userAddress;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Fallback to inboxStateFromInboxIds
|
|
33
|
+
userAddress = await this.resolveFromInboxState(inboxId);
|
|
34
|
+
if (userAddress) {
|
|
35
|
+
this.setCachedAddress(inboxId, userAddress);
|
|
36
|
+
console.log(`✅ Resolved user address via fallback: ${userAddress}`);
|
|
37
|
+
return userAddress;
|
|
38
|
+
}
|
|
39
|
+
console.log(`⚠️ No identifiers found for inbox ${inboxId}`);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.error(`❌ Error resolving user address for ${inboxId}:`, error);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve address from conversation members
|
|
49
|
+
*/
|
|
50
|
+
async resolveFromConversation(conversation, inboxId) {
|
|
51
|
+
try {
|
|
52
|
+
const members = await conversation.members();
|
|
53
|
+
const sender = members.find((member) => member.inboxId.toLowerCase() === inboxId.toLowerCase());
|
|
54
|
+
if (sender) {
|
|
55
|
+
const ethIdentifier = sender.accountIdentifiers.find((id) => id.identifierKind === 0 // IdentifierKind.Ethereum
|
|
56
|
+
);
|
|
57
|
+
if (ethIdentifier) {
|
|
58
|
+
return ethIdentifier.identifier;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.log(`⚠️ No Ethereum identifier found for inbox ${inboxId}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.log(`⚠️ Sender not found in conversation members for inbox ${inboxId}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error(`❌ Error resolving from conversation members:`, error);
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve address from inbox state (network fallback)
|
|
75
|
+
*/
|
|
76
|
+
async resolveFromInboxState(inboxId) {
|
|
77
|
+
try {
|
|
78
|
+
const inboxState = await this.client.preferences.inboxStateFromInboxIds([
|
|
79
|
+
inboxId
|
|
80
|
+
]);
|
|
81
|
+
const firstState = inboxState?.[0];
|
|
82
|
+
if (firstState?.identifiers && firstState.identifiers.length > 0) {
|
|
83
|
+
const firstIdentifier = firstState.identifiers[0];
|
|
84
|
+
return firstIdentifier?.identifier;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.error(`❌ Error resolving from inbox state:`, error);
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get cached address if not expired
|
|
94
|
+
*/
|
|
95
|
+
getCachedAddress(inboxId) {
|
|
96
|
+
const entry = this.cache.get(inboxId);
|
|
97
|
+
if (!entry)
|
|
98
|
+
return null;
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
if (now - entry.timestamp > this.cacheTtl) {
|
|
101
|
+
this.cache.delete(inboxId);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return entry.address;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Cache address with LRU eviction
|
|
108
|
+
*/
|
|
109
|
+
setCachedAddress(inboxId, address) {
|
|
110
|
+
// Simple LRU: if cache is full, remove oldest entry
|
|
111
|
+
if (this.cache.size >= this.maxCacheSize) {
|
|
112
|
+
const firstKey = this.cache.keys().next().value;
|
|
113
|
+
if (firstKey) {
|
|
114
|
+
this.cache.delete(firstKey);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
this.cache.set(inboxId, {
|
|
118
|
+
address,
|
|
119
|
+
timestamp: Date.now()
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Pre-populate cache from existing conversations
|
|
124
|
+
*/
|
|
125
|
+
async prePopulateCache() {
|
|
126
|
+
console.log("🔄 Pre-populating address cache...");
|
|
127
|
+
try {
|
|
128
|
+
const conversations = await this.client.conversations.list();
|
|
129
|
+
let cachedCount = 0;
|
|
130
|
+
for (const conversation of conversations) {
|
|
131
|
+
try {
|
|
132
|
+
const members = await conversation.members();
|
|
133
|
+
for (const member of members) {
|
|
134
|
+
const ethIdentifier = member.accountIdentifiers.find((id) => id.identifierKind === 0 // IdentifierKind.Ethereum
|
|
135
|
+
);
|
|
136
|
+
if (ethIdentifier) {
|
|
137
|
+
this.setCachedAddress(member.inboxId, ethIdentifier.identifier);
|
|
138
|
+
cachedCount++;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
console.error("Error pre-caching conversation members:", error);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
console.log(`✅ Pre-cached ${cachedCount} address mappings`);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
console.error("Error pre-populating cache:", error);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Clear the cache
|
|
154
|
+
*/
|
|
155
|
+
clearCache() {
|
|
156
|
+
this.cache.clear();
|
|
157
|
+
console.log("🗑️ Address cache cleared");
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get cache statistics
|
|
161
|
+
*/
|
|
162
|
+
getCacheStats() {
|
|
163
|
+
return {
|
|
164
|
+
size: this.cache.size,
|
|
165
|
+
maxSize: this.maxCacheSize
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|