@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.
Files changed (45) hide show
  1. package/.cache/tsbuildinfo.json +1 -1
  2. package/.turbo/turbo-build.log +5 -0
  3. package/.turbo/turbo-lint$colon$fix.log +6 -0
  4. package/dist/scripts/generate-keys.d.ts +1 -0
  5. package/dist/scripts/generate-keys.js +19 -0
  6. package/dist/scripts/refresh-identity.d.ts +1 -0
  7. package/dist/scripts/refresh-identity.js +93 -0
  8. package/dist/scripts/register-wallet.d.ts +1 -0
  9. package/dist/scripts/register-wallet.js +75 -0
  10. package/dist/scripts/revoke-all-installations.d.ts +2 -0
  11. package/dist/scripts/revoke-all-installations.js +68 -0
  12. package/dist/scripts/revoke-installations.d.ts +2 -0
  13. package/dist/scripts/revoke-installations.js +62 -0
  14. package/dist/src/abi/l2_resolver.d.ts +992 -0
  15. package/dist/src/abi/l2_resolver.js +699 -0
  16. package/dist/src/client.d.ts +76 -0
  17. package/dist/src/client.js +709 -0
  18. package/dist/src/constants.d.ts +3 -0
  19. package/dist/src/constants.js +6 -0
  20. package/dist/src/index.d.ts +22 -0
  21. package/dist/src/index.js +46 -0
  22. package/dist/src/lib/message-listener.d.ts +69 -0
  23. package/dist/src/lib/message-listener.js +235 -0
  24. package/dist/src/lib/message-listener.test.d.ts +1 -0
  25. package/dist/src/lib/message-listener.test.js +303 -0
  26. package/dist/src/lib/subjects.d.ts +24 -0
  27. package/dist/src/lib/subjects.js +68 -0
  28. package/dist/src/resolver/address-resolver.d.ts +57 -0
  29. package/dist/src/resolver/address-resolver.js +168 -0
  30. package/dist/src/resolver/basename-resolver.d.ts +134 -0
  31. package/dist/src/resolver/basename-resolver.js +409 -0
  32. package/dist/src/resolver/ens-resolver.d.ts +95 -0
  33. package/dist/src/resolver/ens-resolver.js +249 -0
  34. package/dist/src/resolver/index.d.ts +1 -0
  35. package/dist/src/resolver/index.js +1 -0
  36. package/dist/src/resolver/resolver.d.ts +162 -0
  37. package/dist/src/resolver/resolver.js +238 -0
  38. package/dist/src/resolver/xmtp-resolver.d.ts +95 -0
  39. package/dist/src/resolver/xmtp-resolver.js +297 -0
  40. package/dist/src/service-client.d.ts +77 -0
  41. package/dist/src/service-client.js +198 -0
  42. package/dist/src/types.d.ts +123 -0
  43. package/dist/src/types.js +5 -0
  44. package/package.json +5 -4
  45. package/tsconfig.json +3 -1
@@ -0,0 +1,709 @@
1
+ import { ReactionCodec } from "@xmtp/content-type-reaction";
2
+ import { ReplyCodec } from "@xmtp/content-type-reply";
3
+ import { TransactionReferenceCodec } from "@xmtp/content-type-transaction-reference";
4
+ import { WalletSendCallsCodec } from "@xmtp/content-type-wallet-send-calls";
5
+ import { Client } from "@xmtp/node-sdk";
6
+ import { getRandomValues } from "node:crypto";
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { fromString, toString as uint8arraysToString } from "uint8arrays";
11
+ import { createWalletClient, http, toBytes } from "viem";
12
+ import { privateKeyToAccount } from "viem/accounts";
13
+ import { sepolia } from "viem/chains";
14
+ import { revokeOldInstallations } from "../scripts/revoke-installations";
15
+ // ===================================================================
16
+ // Module Setup
17
+ // ===================================================================
18
+ // ES module equivalent of __dirname
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = path.dirname(__filename);
21
+ // ===================================================================
22
+ // User and Signer Creation
23
+ // ===================================================================
24
+ export const createUser = (key) => {
25
+ const account = privateKeyToAccount(key);
26
+ return {
27
+ key: key,
28
+ account,
29
+ wallet: createWalletClient({
30
+ account,
31
+ chain: sepolia,
32
+ transport: http()
33
+ })
34
+ };
35
+ };
36
+ export const createSigner = (key) => {
37
+ if (!key || typeof key !== "string") {
38
+ throw new Error("XMTP wallet key must be a non-empty string");
39
+ }
40
+ const sanitizedKey = key.startsWith("0x") ? key : `0x${key}`;
41
+ const user = createUser(sanitizedKey);
42
+ return {
43
+ type: "EOA",
44
+ getIdentifier: () => ({
45
+ identifierKind: 0, // Use numeric value to avoid ambient const enum issue
46
+ identifier: user.account.address.toLowerCase()
47
+ }),
48
+ signMessage: async (message) => {
49
+ const signature = await user.wallet.signMessage({
50
+ message,
51
+ account: user.account
52
+ });
53
+ return toBytes(signature);
54
+ }
55
+ };
56
+ };
57
+ // XMTP XmtpClient setup
58
+ // const xmtpClient: XmtpClient | null = null
59
+ // Function to clear XMTP database when hitting installation limits
60
+ async function clearXMTPDatabase(address, env) {
61
+ console.log("๐Ÿงน Clearing XMTP database to resolve installation limit...");
62
+ // Get the storage directory using the same logic as getDbPath
63
+ const getStorageDirectory = () => {
64
+ const customStoragePath = process.env.XMTP_STORAGE_PATH;
65
+ if (customStoragePath) {
66
+ return path.isAbsolute(customStoragePath)
67
+ ? customStoragePath
68
+ : path.resolve(process.cwd(), customStoragePath);
69
+ }
70
+ // Use existing logic as fallback
71
+ const projectRoot = process.env.PROJECT_ROOT || path.resolve(__dirname, "../../..");
72
+ return path.join(projectRoot, ".data/xmtp"); // Local development
73
+ };
74
+ // Clear local database files
75
+ const dbPattern = `${env}-${address}.db3`;
76
+ const storageDir = getStorageDirectory();
77
+ // Primary storage directory
78
+ const possiblePaths = [
79
+ storageDir,
80
+ // Legacy fallback paths for backward compatibility
81
+ path.join(process.cwd(), ".data", "xmtp"),
82
+ path.join(process.cwd(), "..", ".data", "xmtp"),
83
+ path.join(process.cwd(), "..", "..", ".data", "xmtp")
84
+ ];
85
+ for (const dir of possiblePaths) {
86
+ try {
87
+ if (fs.existsSync(dir)) {
88
+ const files = fs.readdirSync(dir);
89
+ const matchingFiles = files.filter((file) => file.includes(dbPattern) ||
90
+ file.includes(address) ||
91
+ file.includes(`xmtp-${env}-${address}`));
92
+ for (const file of matchingFiles) {
93
+ const fullPath = path.join(dir, file);
94
+ try {
95
+ fs.unlinkSync(fullPath);
96
+ console.log(`โœ… Removed: ${fullPath}`);
97
+ }
98
+ catch (err) {
99
+ console.log(`โš ๏ธ Could not remove ${fullPath}:`, err);
100
+ }
101
+ }
102
+ }
103
+ }
104
+ catch (err) {
105
+ // Ignore errors when checking directories
106
+ }
107
+ }
108
+ }
109
+ export async function createXMTPClient(
110
+ // signer: Signer,
111
+ privateKey, opts) {
112
+ const { persist = true, maxRetries = 3, storagePath } = opts ?? {};
113
+ let attempt = 0;
114
+ // Extract common variables for error handling
115
+ // const actualSigner = signer
116
+ const signer = createSigner(privateKey);
117
+ if (!signer) {
118
+ throw new Error("No signer provided and XMTP_WALLET_KEY environment variable is not set");
119
+ }
120
+ const { XMTP_ENCRYPTION_KEY, XMTP_ENV } = process.env;
121
+ // Get the wallet address to use the correct database
122
+ const identifier = await signer.getIdentifier();
123
+ const address = identifier.identifier;
124
+ while (attempt < maxRetries) {
125
+ try {
126
+ console.log(`๐Ÿ”„ Attempt ${attempt + 1}/${maxRetries} to create XMTP client...`);
127
+ // Always require encryption key and persistence - no stateless mode
128
+ if (!persist) {
129
+ throw new Error("Stateless mode is not supported. XMTP client must run in persistent mode " +
130
+ "to properly receive and process messages. Set persist: true or remove the persist option " +
131
+ "to use the default persistent mode.");
132
+ }
133
+ if (!XMTP_ENCRYPTION_KEY) {
134
+ throw new Error("XMTP_ENCRYPTION_KEY must be set for persistent mode");
135
+ }
136
+ const dbEncryptionKey = getEncryptionKeyFromHex(XMTP_ENCRYPTION_KEY);
137
+ const dbPath = await getDbPath(`${XMTP_ENV || "dev"}-${address}`, storagePath);
138
+ console.log(`๐Ÿ“ Using database path: ${dbPath}`);
139
+ // Always create a fresh client and sync it
140
+ const client = await Client.create(signer, {
141
+ dbEncryptionKey,
142
+ env: XMTP_ENV,
143
+ dbPath,
144
+ codecs: [
145
+ new ReplyCodec(),
146
+ new ReactionCodec(),
147
+ new WalletSendCallsCodec(),
148
+ new TransactionReferenceCodec()
149
+ ]
150
+ });
151
+ // Force sync conversations to ensure we have the latest data
152
+ console.log("๐Ÿ“ก Syncing conversations to ensure latest state...");
153
+ await client.conversations.sync();
154
+ await backupDbToPersistentStorage(dbPath, `${XMTP_ENV || "dev"}-${address}`);
155
+ console.log("โœ… XMTP XmtpClient created");
156
+ console.log(`๐Ÿ”‘ Wallet address: ${address}`);
157
+ console.log(`๐ŸŒ Environment: ${XMTP_ENV || "dev"}`);
158
+ console.log(`๐Ÿ’พ Storage mode: persistent`);
159
+ return client;
160
+ }
161
+ catch (error) {
162
+ attempt++;
163
+ if (error instanceof Error &&
164
+ error.message.includes("5/5 installations")) {
165
+ console.log(`๐Ÿ’ฅ Installation limit reached (attempt ${attempt}/${maxRetries})`);
166
+ if (attempt < maxRetries) {
167
+ // Get wallet address for database clearing
168
+ const identifier = await signer.getIdentifier();
169
+ const address = identifier.identifier;
170
+ // Extract inboxId from the error message
171
+ const inboxIdMatch = error.message.match(/InboxID ([a-f0-9]+)/);
172
+ const inboxId = inboxIdMatch ? inboxIdMatch[1] : undefined;
173
+ // First try to revoke old installations
174
+ const revocationSuccess = await revokeOldInstallations(signer, inboxId);
175
+ if (revocationSuccess) {
176
+ console.log("๐ŸŽฏ Installations revoked, retrying connection...");
177
+ }
178
+ else {
179
+ console.log("โš ๏ธ Installation revocation failed or not needed, clearing database...");
180
+ // Clear database as fallback
181
+ await clearXMTPDatabase(address, process.env.XMTP_ENV || "dev");
182
+ }
183
+ // Wait a bit before retrying
184
+ const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
185
+ console.log(`โณ Waiting ${delay}ms before retry...`);
186
+ await new Promise((resolve) => setTimeout(resolve, delay));
187
+ }
188
+ else {
189
+ console.error("โŒ Failed to resolve installation limit after all retries");
190
+ console.error("๐Ÿ’ก Possible solutions:");
191
+ console.error(" 1. Use a different wallet (generate new keys)");
192
+ console.error(" 2. Switch XMTP environments (dev <-> production)");
193
+ console.error(" 3. Wait and try again later");
194
+ console.error(" 4. Contact XMTP support for manual intervention");
195
+ throw error;
196
+ }
197
+ }
198
+ else if (error instanceof Error &&
199
+ error.message.includes("Association error: Missing identity update")) {
200
+ console.log(`๐Ÿ”„ Identity association error detected (attempt ${attempt}/${maxRetries})`);
201
+ if (attempt < maxRetries) {
202
+ console.log("๐Ÿ”ง Attempting automatic identity refresh...");
203
+ // Try to refresh identity by creating a persistent client first
204
+ try {
205
+ console.log("๐Ÿ“ Creating persistent client to refresh identity...");
206
+ const tempEncryptionKey = XMTP_ENCRYPTION_KEY
207
+ ? getEncryptionKeyFromHex(XMTP_ENCRYPTION_KEY)
208
+ : getEncryptionKeyFromHex(generateEncryptionKeyHex());
209
+ const tempClient = await Client.create(signer, {
210
+ dbEncryptionKey: tempEncryptionKey,
211
+ env: XMTP_ENV,
212
+ dbPath: await getDbPath(`${XMTP_ENV || "dev"}-${address}`, storagePath),
213
+ codecs: [
214
+ new ReplyCodec(),
215
+ new ReactionCodec(),
216
+ new WalletSendCallsCodec(),
217
+ new TransactionReferenceCodec()
218
+ ]
219
+ });
220
+ console.log("๐Ÿ“ก Syncing identity and conversations...");
221
+ await tempClient.conversations.sync();
222
+ console.log("โœ… Identity refresh successful, retrying original request...");
223
+ // Wait a bit before retrying
224
+ const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
225
+ console.log(`โณ Waiting ${delay}ms before retry...`);
226
+ await new Promise((resolve) => setTimeout(resolve, delay));
227
+ }
228
+ catch (refreshError) {
229
+ console.log(`โŒ Identity refresh failed:`, refreshError);
230
+ // Continue to the retry logic
231
+ }
232
+ }
233
+ else {
234
+ console.error("โŒ Failed to resolve identity association error after all retries");
235
+ console.error("๐Ÿ’ก Try running: pnpm with-env pnpm --filter @hybrd/xmtp refresh:identity");
236
+ throw error;
237
+ }
238
+ }
239
+ else {
240
+ // For other errors, don't retry
241
+ throw error;
242
+ }
243
+ }
244
+ }
245
+ throw new Error("Max retries exceeded");
246
+ }
247
+ // ===================================================================
248
+ // Encryption Key Management
249
+ // ===================================================================
250
+ /**
251
+ * Generate a random encryption key
252
+ * @returns The encryption key as a hex string
253
+ */
254
+ export const generateEncryptionKeyHex = () => {
255
+ const uint8Array = getRandomValues(new Uint8Array(32));
256
+ return uint8arraysToString(uint8Array, "hex");
257
+ };
258
+ /**
259
+ * Get the encryption key from a hex string
260
+ * @param hex - The hex string
261
+ * @returns The encryption key as Uint8Array
262
+ */
263
+ export const getEncryptionKeyFromHex = (hex) => {
264
+ return fromString(hex, "hex");
265
+ };
266
+ // ===================================================================
267
+ // Database Path Management
268
+ // ===================================================================
269
+ export const getDbPath = async (description = "xmtp", storagePath) => {
270
+ // Allow custom storage path via environment variable
271
+ const customStoragePath = process.env.XMTP_STORAGE_PATH;
272
+ let volumePath;
273
+ if (customStoragePath) {
274
+ // Use custom storage path if provided
275
+ volumePath = path.isAbsolute(customStoragePath)
276
+ ? customStoragePath
277
+ : path.resolve(process.cwd(), customStoragePath);
278
+ }
279
+ else if (storagePath) {
280
+ volumePath = path.isAbsolute(storagePath)
281
+ ? storagePath
282
+ : path.resolve(process.cwd(), storagePath);
283
+ }
284
+ else {
285
+ // Use existing logic as fallback
286
+ const projectRoot = process.env.PROJECT_ROOT || path.resolve(__dirname, "../../..");
287
+ // Default storage path for local development
288
+ volumePath = path.join(projectRoot, ".data/xmtp");
289
+ }
290
+ const dbPath = `${volumePath}/${description}.db3`;
291
+ if (typeof globalThis !== "undefined" && "XMTP_STORAGE" in globalThis) {
292
+ try {
293
+ console.log(`๐Ÿ“ฆ Using Cloudflare R2 storage for: ${dbPath}`);
294
+ const r2Bucket = globalThis.XMTP_STORAGE;
295
+ const remotePath = `xmtp-databases/${description}.db3`;
296
+ try {
297
+ const existingObject = await r2Bucket.head(remotePath);
298
+ if (existingObject) {
299
+ console.log(`๐Ÿ“ฅ Downloading existing database from R2 storage...`);
300
+ if (!fs.existsSync(volumePath)) {
301
+ fs.mkdirSync(volumePath, { recursive: true });
302
+ }
303
+ const object = await r2Bucket.get(remotePath);
304
+ if (object) {
305
+ const fileData = await object.arrayBuffer();
306
+ fs.writeFileSync(dbPath, new Uint8Array(fileData));
307
+ console.log(`โœ… Database downloaded from R2 storage`);
308
+ }
309
+ }
310
+ else {
311
+ console.log(`๐Ÿ“ No existing database found in R2 storage`);
312
+ }
313
+ }
314
+ catch (error) {
315
+ console.log(`โš ๏ธ Failed to download database from R2 storage:`, error);
316
+ }
317
+ }
318
+ catch (error) {
319
+ console.log(`โš ๏ธ R2 storage not available:`, error);
320
+ }
321
+ }
322
+ if (!fs.existsSync(volumePath)) {
323
+ fs.mkdirSync(volumePath, { recursive: true });
324
+ }
325
+ return dbPath;
326
+ };
327
+ export const backupDbToPersistentStorage = async (dbPath, description) => {
328
+ if (typeof globalThis !== "undefined" &&
329
+ "XMTP_STORAGE" in globalThis &&
330
+ fs.existsSync(dbPath)) {
331
+ try {
332
+ console.log(`๐Ÿ“ฆ Backing up database to R2 storage: ${dbPath}`);
333
+ const r2Bucket = globalThis.XMTP_STORAGE;
334
+ const remotePath = `xmtp-databases/${description}.db3`;
335
+ const fileData = fs.readFileSync(dbPath);
336
+ await r2Bucket.put(remotePath, fileData);
337
+ console.log(`โœ… Database backed up to R2 storage: ${remotePath}`);
338
+ }
339
+ catch (error) {
340
+ console.log(`โš ๏ธ Failed to backup database to R2 storage:`, error);
341
+ }
342
+ }
343
+ };
344
+ // ===================================================================
345
+ // Logging and Debugging
346
+ // ===================================================================
347
+ export const logAgentDetails = async (clients) => {
348
+ const clientsByAddress = Array.isArray(clients)
349
+ ? clients.reduce((acc, XmtpClient) => {
350
+ const address = XmtpClient.accountIdentifier?.identifier ?? "";
351
+ acc[address] = acc[address] ?? [];
352
+ acc[address].push(XmtpClient);
353
+ return acc;
354
+ }, {})
355
+ : {
356
+ [clients.accountIdentifier?.identifier ?? ""]: [clients]
357
+ };
358
+ for (const [address, clientGroup] of Object.entries(clientsByAddress)) {
359
+ const firstClient = clientGroup[0];
360
+ const inboxId = firstClient?.inboxId;
361
+ const environments = clientGroup
362
+ .map((c) => c.options?.env ?? "dev")
363
+ .join(", ");
364
+ console.log(`\x1b[38;2;252;76;52m
365
+ โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—
366
+ โ•šโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ•šโ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—
367
+ โ•šโ–ˆโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•
368
+ โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•”โ•โ•โ•โ•
369
+ โ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ•šโ•โ• โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘
370
+ โ•šโ•โ• โ•šโ•โ•โ•šโ•โ• โ•šโ•โ• โ•šโ•โ• โ•šโ•โ•
371
+ \x1b[0m`);
372
+ const urls = [`http://xmtp.chat/dm/${address}`];
373
+ const conversations = await firstClient?.conversations.list();
374
+ console.log(`
375
+ โœ“ XMTP XmtpClient:
376
+ โ€ข Address: ${address}
377
+ โ€ข Conversations: ${conversations?.length}
378
+ โ€ข InboxId: ${inboxId}
379
+ โ€ข Networks: ${environments}
380
+ ${urls.map((url) => `โ€ข URL: ${url}`).join("\n")}`);
381
+ }
382
+ };
383
+ // ===================================================================
384
+ // Environment Validation
385
+ // ===================================================================
386
+ export function validateEnvironment(vars) {
387
+ const missing = vars.filter((v) => !process.env[v]);
388
+ if (missing.length) {
389
+ try {
390
+ const envPath = path.resolve(process.cwd(), ".env");
391
+ if (fs.existsSync(envPath)) {
392
+ const envVars = fs
393
+ .readFileSync(envPath, "utf-8")
394
+ .split("\n")
395
+ .filter((line) => line.trim() && !line.startsWith("#"))
396
+ .reduce((acc, line) => {
397
+ const [key, ...val] = line.split("=");
398
+ if (key && val.length)
399
+ acc[key.trim()] = val.join("=").trim();
400
+ return acc;
401
+ }, {});
402
+ missing.forEach((v) => {
403
+ if (envVars[v])
404
+ process.env[v] = envVars[v];
405
+ });
406
+ }
407
+ }
408
+ catch (e) {
409
+ console.error(e);
410
+ /* ignore errors */
411
+ }
412
+ const stillMissing = vars.filter((v) => !process.env[v]);
413
+ if (stillMissing.length) {
414
+ console.error("Missing env vars:", stillMissing.join(", "));
415
+ process.exit(1);
416
+ }
417
+ }
418
+ return vars.reduce((acc, key) => {
419
+ acc[key] = process.env[key];
420
+ return acc;
421
+ }, {});
422
+ }
423
+ /**
424
+ * Diagnose XMTP environment and identity issues
425
+ */
426
+ export async function diagnoseXMTPIdentityIssue(client, inboxId, environment) {
427
+ const suggestions = [];
428
+ const details = {
429
+ environment,
430
+ inboxId,
431
+ timestamp: new Date().toISOString()
432
+ };
433
+ try {
434
+ // Try to resolve the inbox state
435
+ const inboxState = await client.preferences.inboxStateFromInboxIds([
436
+ inboxId
437
+ ]);
438
+ if (inboxState.length === 0) {
439
+ suggestions.push(`Inbox ID ${inboxId} not found in ${environment} environment`);
440
+ suggestions.push("Try switching XMTP_ENV to 'dev' if currently 'production' or vice versa");
441
+ suggestions.push("Verify the user has created an identity on this XMTP network");
442
+ details.inboxStateFound = false;
443
+ return { canResolve: false, suggestions, details };
444
+ }
445
+ const inbox = inboxState[0];
446
+ if (!inbox) {
447
+ suggestions.push("Inbox state returned empty data");
448
+ details.inboxStateFound = false;
449
+ return { canResolve: false, suggestions, details };
450
+ }
451
+ details.inboxStateFound = true;
452
+ details.identifierCount = inbox.identifiers?.length || 0;
453
+ if (!inbox.identifiers || inbox.identifiers.length === 0) {
454
+ suggestions.push("Inbox found but has no identifiers");
455
+ suggestions.push("This indicates incomplete identity registration");
456
+ suggestions.push("User may need to re-register their identity on XMTP");
457
+ details.hasIdentifiers = false;
458
+ return { canResolve: false, suggestions, details };
459
+ }
460
+ // Successfully resolved
461
+ details.hasIdentifiers = true;
462
+ details.resolvedAddress = inbox.identifiers[0]?.identifier;
463
+ return {
464
+ canResolve: true,
465
+ suggestions: ["Identity resolved successfully"],
466
+ details
467
+ };
468
+ }
469
+ catch (error) {
470
+ const errorMessage = error instanceof Error ? error.message : String(error);
471
+ details.error = errorMessage;
472
+ if (errorMessage.includes("Association error")) {
473
+ suggestions.push("XMTP identity association error detected");
474
+ suggestions.push("Check if user exists on the correct XMTP environment (dev vs production)");
475
+ suggestions.push("Identity may need to be recreated on the current environment");
476
+ }
477
+ if (errorMessage.includes("Missing identity update")) {
478
+ suggestions.push("Missing identity updates in XMTP network");
479
+ suggestions.push("This can indicate network sync issues");
480
+ suggestions.push("Wait a few minutes and retry, or recreate identity");
481
+ }
482
+ if (errorMessage.includes("database") || errorMessage.includes("storage")) {
483
+ suggestions.push("XMTP local database/storage issue");
484
+ suggestions.push("Try clearing XMTP database and resyncing");
485
+ suggestions.push("Check .data/xmtp directory permissions");
486
+ }
487
+ suggestions.push("Consider testing with a fresh XMTP identity");
488
+ return { canResolve: false, suggestions, details };
489
+ }
490
+ }
491
+ export class XMTPConnectionManager {
492
+ constructor(privateKey, config = {}) {
493
+ this.client = null;
494
+ this.healthCheckTimer = null;
495
+ this.isReconnecting = false;
496
+ this.privateKey = privateKey;
497
+ this.config = {
498
+ maxRetries: config.maxRetries ?? 5,
499
+ retryDelayMs: config.retryDelayMs ?? 1000,
500
+ healthCheckIntervalMs: config.healthCheckIntervalMs ?? 30000,
501
+ connectionTimeoutMs: config.connectionTimeoutMs ?? 10000,
502
+ reconnectOnFailure: config.reconnectOnFailure ?? true
503
+ };
504
+ this.health = {
505
+ isConnected: false,
506
+ lastHealthCheck: new Date(),
507
+ consecutiveFailures: 0,
508
+ totalReconnects: 0,
509
+ avgResponseTime: 0
510
+ };
511
+ }
512
+ async connect(persist = false) {
513
+ if (this.client && this.health.isConnected) {
514
+ return this.client;
515
+ }
516
+ let attempt = 0;
517
+ while (attempt < this.config.maxRetries) {
518
+ try {
519
+ console.log(`๐Ÿ”„ XMTP connection attempt ${attempt + 1}/${this.config.maxRetries}`);
520
+ this.client = await createXMTPClient(this.privateKey, { persist });
521
+ this.health.isConnected = true;
522
+ this.health.consecutiveFailures = 0;
523
+ // Start health monitoring
524
+ this.startHealthMonitoring();
525
+ console.log("โœ… XMTP client connected successfully");
526
+ return this.client;
527
+ }
528
+ catch (error) {
529
+ attempt++;
530
+ this.health.consecutiveFailures++;
531
+ console.error(`โŒ XMTP connection attempt ${attempt} failed:`, error);
532
+ if (attempt < this.config.maxRetries) {
533
+ const delay = this.config.retryDelayMs * Math.pow(2, attempt - 1);
534
+ console.log(`โณ Retrying in ${delay}ms...`);
535
+ await this.sleep(delay);
536
+ }
537
+ }
538
+ }
539
+ throw new Error(`Failed to connect to XMTP after ${this.config.maxRetries} attempts`);
540
+ }
541
+ // private async createClientWithTimeout(persist: boolean): Promise<XmtpClient> {
542
+ // const timeoutPromise = new Promise<never>((_, reject) => {
543
+ // setTimeout(
544
+ // () => reject(new Error("Connection timeout")),
545
+ // this.config.connectionTimeoutMs
546
+ // )
547
+ // })
548
+ // const clientPromise = createXMTPClient(this.signer, { persist })
549
+ // return Promise.race([clientPromise, timeoutPromise])
550
+ // }
551
+ startHealthMonitoring() {
552
+ if (this.healthCheckTimer) {
553
+ clearInterval(this.healthCheckTimer);
554
+ }
555
+ this.healthCheckTimer = setInterval(() => {
556
+ this.performHealthCheck();
557
+ }, this.config.healthCheckIntervalMs);
558
+ }
559
+ async performHealthCheck() {
560
+ if (!this.client)
561
+ return;
562
+ const startTime = Date.now();
563
+ try {
564
+ // Simple health check: try to list conversations
565
+ await this.client.conversations.list();
566
+ const responseTime = Date.now() - startTime;
567
+ this.health.avgResponseTime =
568
+ (this.health.avgResponseTime + responseTime) / 2;
569
+ this.health.lastHealthCheck = new Date();
570
+ this.health.consecutiveFailures = 0;
571
+ this.health.isConnected = true;
572
+ console.log(`๐Ÿ’“ XMTP health check passed (${responseTime}ms)`);
573
+ }
574
+ catch (error) {
575
+ this.health.consecutiveFailures++;
576
+ this.health.isConnected = false;
577
+ console.error(`๐Ÿ’” XMTP health check failed:`, error);
578
+ // Trigger reconnection if enabled
579
+ if (this.config.reconnectOnFailure && !this.isReconnecting) {
580
+ this.handleConnectionFailure();
581
+ }
582
+ }
583
+ }
584
+ async handleConnectionFailure() {
585
+ if (this.isReconnecting)
586
+ return;
587
+ this.isReconnecting = true;
588
+ this.health.totalReconnects++;
589
+ console.log("๐Ÿ”„ XMTP connection lost, attempting to reconnect...");
590
+ try {
591
+ this.client = null;
592
+ await this.connect();
593
+ console.log("โœ… XMTP reconnection successful");
594
+ }
595
+ catch (error) {
596
+ console.error("โŒ XMTP reconnection failed:", error);
597
+ }
598
+ finally {
599
+ this.isReconnecting = false;
600
+ }
601
+ }
602
+ sleep(ms) {
603
+ return new Promise((resolve) => setTimeout(resolve, ms));
604
+ }
605
+ getHealth() {
606
+ return { ...this.health };
607
+ }
608
+ getClient() {
609
+ return this.client;
610
+ }
611
+ async disconnect() {
612
+ if (this.healthCheckTimer) {
613
+ clearInterval(this.healthCheckTimer);
614
+ this.healthCheckTimer = null;
615
+ }
616
+ this.client = null;
617
+ this.health.isConnected = false;
618
+ console.log("๐Ÿ”Œ XMTP client disconnected");
619
+ }
620
+ }
621
+ // Enhanced client creation with connection management
622
+ export async function createXMTPConnectionManager(privateKey, config) {
623
+ const manager = new XMTPConnectionManager(privateKey, config);
624
+ await manager.connect();
625
+ return manager;
626
+ }
627
+ // ===================================================================
628
+ // User Address Resolution with Auto-Refresh
629
+ // ===================================================================
630
+ /**
631
+ * Resolve user address from inbox ID with automatic identity refresh on association errors
632
+ */
633
+ export async function resolveUserAddress(client, senderInboxId, maxRetries = 2) {
634
+ let attempt = 0;
635
+ while (attempt < maxRetries) {
636
+ try {
637
+ console.log(`๐Ÿ” Resolving user address (attempt ${attempt + 1}/${maxRetries})...`);
638
+ const inboxState = await client.preferences.inboxStateFromInboxIds([
639
+ senderInboxId
640
+ ]);
641
+ const firstInbox = inboxState[0];
642
+ if (inboxState.length > 0 &&
643
+ firstInbox?.identifiers &&
644
+ firstInbox.identifiers.length > 0) {
645
+ const userAddress = firstInbox.identifiers[0]?.identifier;
646
+ if (userAddress) {
647
+ console.log("โœ… Resolved user address:", userAddress);
648
+ return userAddress;
649
+ }
650
+ }
651
+ console.log("โš ๏ธ No identifiers found in inbox state");
652
+ return "unknown";
653
+ }
654
+ catch (error) {
655
+ attempt++;
656
+ if (error instanceof Error &&
657
+ error.message.includes("Association error: Missing identity update")) {
658
+ console.log(`๐Ÿ”„ Identity association error during address resolution (attempt ${attempt}/${maxRetries})`);
659
+ if (attempt < maxRetries) {
660
+ console.log("๐Ÿ”ง Attempting automatic identity refresh for address resolution...");
661
+ try {
662
+ // Force a conversation sync to refresh identity state
663
+ console.log("๐Ÿ“ก Syncing conversations to refresh identity...");
664
+ await client.conversations.sync();
665
+ // Small delay before retry
666
+ console.log("โณ Waiting 2s before retry...");
667
+ await new Promise((resolve) => setTimeout(resolve, 2000));
668
+ console.log("โœ… Identity sync completed, retrying address resolution...");
669
+ }
670
+ catch (refreshError) {
671
+ console.log(`โŒ Identity refresh failed:`, refreshError);
672
+ }
673
+ }
674
+ else {
675
+ console.error("โŒ Failed to resolve user address after all retries");
676
+ console.error("๐Ÿ’ก Identity association issue persists");
677
+ // Run diagnostic
678
+ try {
679
+ const diagnosis = await diagnoseXMTPIdentityIssue(client, senderInboxId, process.env.XMTP_ENV || "dev");
680
+ console.log("๐Ÿ” XMTP Identity Diagnosis:");
681
+ diagnosis.suggestions.forEach((suggestion) => {
682
+ console.error(`๐Ÿ’ก ${suggestion}`);
683
+ });
684
+ }
685
+ catch (diagError) {
686
+ console.warn("โš ๏ธ Could not run XMTP identity diagnosis:", diagError);
687
+ }
688
+ return "unknown";
689
+ }
690
+ }
691
+ else {
692
+ // For other errors, don't retry
693
+ console.error("โŒ Error resolving user address:", error);
694
+ return "unknown";
695
+ }
696
+ }
697
+ }
698
+ return "unknown";
699
+ }
700
+ export const startPeriodicBackup = (dbPath, description, intervalMs = 300000) => {
701
+ return setInterval(async () => {
702
+ try {
703
+ await backupDbToPersistentStorage(dbPath, description);
704
+ }
705
+ catch (error) {
706
+ console.log(`โš ๏ธ Periodic backup failed:`, error);
707
+ }
708
+ }, intervalMs);
709
+ };