@agicash/breez-sdk-spark 0.12.2-1

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 (49) hide show
  1. package/README.md +126 -0
  2. package/bundler/breez_sdk_spark_wasm.d.ts +1537 -0
  3. package/bundler/breez_sdk_spark_wasm.js +5 -0
  4. package/bundler/breez_sdk_spark_wasm_bg.js +3028 -0
  5. package/bundler/breez_sdk_spark_wasm_bg.wasm +0 -0
  6. package/bundler/breez_sdk_spark_wasm_bg.wasm.d.ts +134 -0
  7. package/bundler/index.d.ts +3 -0
  8. package/bundler/index.js +33 -0
  9. package/bundler/package.json +29 -0
  10. package/bundler/storage/index.js +2331 -0
  11. package/bundler/storage/package.json +12 -0
  12. package/deno/breez_sdk_spark_wasm.d.ts +1537 -0
  13. package/deno/breez_sdk_spark_wasm.js +2782 -0
  14. package/deno/breez_sdk_spark_wasm_bg.wasm +0 -0
  15. package/deno/breez_sdk_spark_wasm_bg.wasm.d.ts +134 -0
  16. package/nodejs/breez_sdk_spark_wasm.d.ts +1537 -0
  17. package/nodejs/breez_sdk_spark_wasm.js +3042 -0
  18. package/nodejs/breez_sdk_spark_wasm_bg.wasm +0 -0
  19. package/nodejs/breez_sdk_spark_wasm_bg.wasm.d.ts +134 -0
  20. package/nodejs/index.d.ts +1 -0
  21. package/nodejs/index.js +52 -0
  22. package/nodejs/index.mjs +24 -0
  23. package/nodejs/package.json +16 -0
  24. package/nodejs/postgres-storage/errors.cjs +19 -0
  25. package/nodejs/postgres-storage/index.cjs +1390 -0
  26. package/nodejs/postgres-storage/migrations.cjs +265 -0
  27. package/nodejs/postgres-storage/package.json +9 -0
  28. package/nodejs/postgres-token-store/errors.cjs +13 -0
  29. package/nodejs/postgres-token-store/index.cjs +857 -0
  30. package/nodejs/postgres-token-store/migrations.cjs +163 -0
  31. package/nodejs/postgres-token-store/package.json +9 -0
  32. package/nodejs/postgres-tree-store/errors.cjs +13 -0
  33. package/nodejs/postgres-tree-store/index.cjs +808 -0
  34. package/nodejs/postgres-tree-store/migrations.cjs +150 -0
  35. package/nodejs/postgres-tree-store/package.json +9 -0
  36. package/nodejs/storage/errors.cjs +19 -0
  37. package/nodejs/storage/index.cjs +1343 -0
  38. package/nodejs/storage/migrations.cjs +417 -0
  39. package/nodejs/storage/package.json +9 -0
  40. package/package.json +45 -0
  41. package/web/breez_sdk_spark_wasm.d.ts +1695 -0
  42. package/web/breez_sdk_spark_wasm.js +2873 -0
  43. package/web/breez_sdk_spark_wasm_bg.wasm +0 -0
  44. package/web/breez_sdk_spark_wasm_bg.wasm.d.ts +134 -0
  45. package/web/index.d.ts +3 -0
  46. package/web/index.js +33 -0
  47. package/web/package.json +28 -0
  48. package/web/storage/index.js +2331 -0
  49. package/web/storage/package.json +12 -0
@@ -0,0 +1,2331 @@
1
+ /**
2
+ * ES6 module for Web IndexedDB Storage Implementation
3
+ * This provides an ES6 interface to IndexedDB storage for web browsers
4
+ */
5
+
6
+ class MigrationManager {
7
+ constructor(db, StorageError, logger = null) {
8
+ this.db = db;
9
+ this.StorageError = StorageError;
10
+ this.logger = logger;
11
+ this.migrations = this._getMigrations();
12
+ }
13
+
14
+ /**
15
+ * Handle IndexedDB upgrade event - called during database opening
16
+ */
17
+ handleUpgrade(event, oldVersion, newVersion) {
18
+ const db = event.target.result;
19
+ const transaction = event.target.transaction;
20
+
21
+ this._log(
22
+ "info",
23
+ `Upgrading IndexedDB from version ${oldVersion} to ${newVersion}`
24
+ );
25
+
26
+ try {
27
+ for (let i = oldVersion; i < newVersion; i++) {
28
+ const migration = this.migrations[i];
29
+ if (migration) {
30
+ this._log("debug", `Running migration ${i + 1}: ${migration.name}`);
31
+ migration.upgrade(db, transaction);
32
+ }
33
+ }
34
+ this._log("info", `Database migration completed successfully`);
35
+ } catch (error) {
36
+ this._log(
37
+ "error",
38
+ `Migration failed at version ${oldVersion}: ${error.message}`
39
+ );
40
+ throw new this.StorageError(
41
+ `Migration failed at version ${oldVersion}: ${error.message}`,
42
+ error
43
+ );
44
+ }
45
+ }
46
+
47
+ _log(level, message) {
48
+ if (this.logger && typeof this.logger.log === "function") {
49
+ this.logger.log({
50
+ line: message,
51
+ level: level,
52
+ });
53
+ } else if (level === "error") {
54
+ console.error(`[MigrationManager] ${message}`);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Define all database migrations for IndexedDB
60
+ *
61
+ * Each migration is an object with:
62
+ * - name: Description of the migration
63
+ * - upgrade: Function that takes (db, transaction) and creates/modifies object stores
64
+ */
65
+ _getMigrations() {
66
+ return [
67
+ {
68
+ name: "Create initial object stores",
69
+ upgrade: (db) => {
70
+ // Settings store (key-value cache)
71
+ if (!db.objectStoreNames.contains("settings")) {
72
+ db.createObjectStore("settings", { keyPath: "key" });
73
+ }
74
+
75
+ // Payments store
76
+ if (!db.objectStoreNames.contains("payments")) {
77
+ const paymentStore = db.createObjectStore("payments", {
78
+ keyPath: "id",
79
+ });
80
+ paymentStore.createIndex("timestamp", "timestamp", {
81
+ unique: false,
82
+ });
83
+ paymentStore.createIndex("paymentType", "paymentType", {
84
+ unique: false,
85
+ });
86
+ paymentStore.createIndex("status", "status", { unique: false });
87
+ }
88
+
89
+ // Payment metadata store
90
+ if (!db.objectStoreNames.contains("payment_metadata")) {
91
+ db.createObjectStore("payment_metadata", { keyPath: "paymentId" });
92
+ }
93
+
94
+ // Unclaimed deposits store
95
+ if (!db.objectStoreNames.contains("unclaimed_deposits")) {
96
+ const depositStore = db.createObjectStore("unclaimed_deposits", {
97
+ keyPath: ["txid", "vout"],
98
+ });
99
+ depositStore.createIndex("txid", "txid", { unique: false });
100
+ }
101
+ },
102
+ },
103
+ {
104
+ name: "Create invoice index",
105
+ upgrade: (db, transaction) => {
106
+ const paymentStore = transaction.objectStore("payments");
107
+ if (!paymentStore.indexNames.contains("invoice")) {
108
+ paymentStore.createIndex("invoice", "details.invoice", {
109
+ unique: false,
110
+ });
111
+ }
112
+ },
113
+ },
114
+ {
115
+ name: "Convert amount and fees from Number to BigInt for u128 support",
116
+ upgrade: (db, transaction) => {
117
+ const store = transaction.objectStore("payments");
118
+ const getAllRequest = store.getAll();
119
+
120
+ getAllRequest.onsuccess = () => {
121
+ const payments = getAllRequest.result;
122
+ let updated = 0;
123
+
124
+ payments.forEach((payment) => {
125
+ // Convert amount and fees from Number to BigInt if they're numbers
126
+ let needsUpdate = false;
127
+
128
+ if (typeof payment.amount === "number") {
129
+ payment.amount = BigInt(Math.round(payment.amount));
130
+ needsUpdate = true;
131
+ }
132
+
133
+ if (typeof payment.fees === "number") {
134
+ payment.fees = BigInt(Math.round(payment.fees));
135
+ needsUpdate = true;
136
+ }
137
+
138
+ if (needsUpdate) {
139
+ store.put(payment);
140
+ updated++;
141
+ }
142
+ });
143
+
144
+ console.log(`Migrated ${updated} payment records to BigInt format`);
145
+ };
146
+ },
147
+ },
148
+ {
149
+ name: "Add sync tables",
150
+ upgrade: (db, transaction) => {
151
+ // sync_revision: tracks the last committed revision (from server-acknowledged
152
+ // or server-received records). Does NOT include pending outgoing revisions.
153
+ if (!db.objectStoreNames.contains("sync_revision")) {
154
+ const syncRevisionStore = db.createObjectStore("sync_revision", {
155
+ keyPath: "id",
156
+ });
157
+ transaction
158
+ .objectStore("sync_revision")
159
+ .add({ id: 1, revision: "0" });
160
+ }
161
+
162
+ if (!db.objectStoreNames.contains("sync_outgoing")) {
163
+ db.createObjectStore("sync_outgoing", {
164
+ keyPath: ["type", "dataId", "revision"],
165
+ });
166
+ transaction
167
+ .objectStore("sync_outgoing")
168
+ .createIndex("revision", "revision");
169
+ }
170
+
171
+ if (!db.objectStoreNames.contains("sync_incoming")) {
172
+ db.createObjectStore("sync_incoming", {
173
+ keyPath: ["type", "dataId", "revision"],
174
+ });
175
+ transaction
176
+ .objectStore("sync_incoming")
177
+ .createIndex("revision", "revision");
178
+ }
179
+
180
+ if (!db.objectStoreNames.contains("sync_state")) {
181
+ db.createObjectStore("sync_state", { keyPath: ["type", "dataId"] });
182
+ }
183
+ },
184
+ },
185
+ {
186
+ name: "Create lnurl_receive_metadata store",
187
+ upgrade: (db) => {
188
+ if (!db.objectStoreNames.contains("lnurl_receive_metadata")) {
189
+ db.createObjectStore("lnurl_receive_metadata", {
190
+ keyPath: "paymentHash",
191
+ });
192
+ }
193
+ },
194
+ },
195
+ {
196
+ // Delete all unclaimed deposits to clear old claim_error JSON format.
197
+ // Deposits will be recovered on next sync.
198
+ name: "Clear unclaimed deposits for claim_error format change",
199
+ upgrade: (db, transaction) => {
200
+ if (db.objectStoreNames.contains("unclaimed_deposits")) {
201
+ const store = transaction.objectStore("unclaimed_deposits");
202
+ store.clear();
203
+ }
204
+ },
205
+ },
206
+ {
207
+ name: "Clear sync tables for BreezSigner backward compatibility",
208
+ upgrade: (db, transaction) => {
209
+ // Clear all sync tables due to BreezSigner signature change.
210
+ // This forces users to sync from scratch to the sync server.
211
+ // Also delete the sync_initial_complete flag to force re-populating
212
+ // all payment metadata for outgoing sync using the new key.
213
+
214
+ // Clear sync tables (only if they exist)
215
+ if (db.objectStoreNames.contains("sync_outgoing")) {
216
+ const syncOutgoing = transaction.objectStore("sync_outgoing");
217
+ syncOutgoing.clear();
218
+ }
219
+
220
+ if (db.objectStoreNames.contains("sync_incoming")) {
221
+ const syncIncoming = transaction.objectStore("sync_incoming");
222
+ syncIncoming.clear();
223
+ }
224
+
225
+ if (db.objectStoreNames.contains("sync_state")) {
226
+ const syncState = transaction.objectStore("sync_state");
227
+ syncState.clear();
228
+ }
229
+
230
+ // Reset revision to 0 (only if store exists)
231
+ if (db.objectStoreNames.contains("sync_revision")) {
232
+ const syncRevision = transaction.objectStore("sync_revision");
233
+ syncRevision.clear();
234
+ syncRevision.put({ id: 1, revision: "0" });
235
+ }
236
+
237
+ // Delete sync_initial_complete setting (only if store exists)
238
+ if (db.objectStoreNames.contains("settings")) {
239
+ const settings = transaction.objectStore("settings");
240
+ settings.delete("sync_initial_complete");
241
+ }
242
+ }
243
+ },
244
+ {
245
+ name: "Create parentPaymentId index for related payments lookup",
246
+ upgrade: (db, transaction) => {
247
+ if (db.objectStoreNames.contains("payment_metadata")) {
248
+ const metadataStore = transaction.objectStore("payment_metadata");
249
+ if (!metadataStore.indexNames.contains("parentPaymentId")) {
250
+ metadataStore.createIndex("parentPaymentId", "parentPaymentId", { unique: false });
251
+ }
252
+ }
253
+ }
254
+ },
255
+ {
256
+ name: "Add tx_type to token payments and trigger token re-sync",
257
+ upgrade: (db, transaction) => {
258
+ // Update all existing token payments to have a default txType
259
+ if (db.objectStoreNames.contains("payments")) {
260
+ const paymentStore = transaction.objectStore("payments");
261
+ const getAllRequest = paymentStore.getAll();
262
+
263
+ getAllRequest.onsuccess = () => {
264
+ const payments = getAllRequest.result;
265
+
266
+ payments.forEach((payment) => {
267
+ // Parse details if it's a string
268
+ let details = null;
269
+ if (payment.details && typeof payment.details === "string") {
270
+ try {
271
+ details = JSON.parse(payment.details);
272
+ } catch (e) {
273
+ return; // Skip this payment if parsing fails
274
+ }
275
+ } else {
276
+ details = payment.details;
277
+ }
278
+
279
+ // Add default txType to token payments
280
+ if (details && details.type === "token" && !details.txType) {
281
+ details.txType = "transfer";
282
+ payment.details = JSON.stringify(details);
283
+ paymentStore.put(payment);
284
+ }
285
+ });
286
+ };
287
+ }
288
+
289
+ // Reset sync cache to trigger token re-sync
290
+ if (db.objectStoreNames.contains("settings")) {
291
+ const settingsStore = transaction.objectStore("settings");
292
+ const getRequest = settingsStore.get("sync_offset");
293
+
294
+ getRequest.onsuccess = () => {
295
+ const syncCache = getRequest.result;
296
+ if (syncCache && syncCache.value) {
297
+ try {
298
+ const syncInfo = JSON.parse(syncCache.value);
299
+ // Reset only the token sync position, keep the bitcoin offset
300
+ syncInfo.last_synced_final_token_payment_id = null;
301
+ settingsStore.put({
302
+ key: "sync_offset",
303
+ value: JSON.stringify(syncInfo),
304
+ });
305
+ } catch (e) {
306
+ // If parsing fails, just continue
307
+ }
308
+ }
309
+ };
310
+ }
311
+ },
312
+ },
313
+ {
314
+ name: "Clear sync tables to force re-sync",
315
+ upgrade: (db, transaction) => {
316
+ if (db.objectStoreNames.contains("sync_outgoing")) {
317
+ transaction.objectStore("sync_outgoing").clear();
318
+ }
319
+ if (db.objectStoreNames.contains("sync_incoming")) {
320
+ transaction.objectStore("sync_incoming").clear();
321
+ }
322
+ if (db.objectStoreNames.contains("sync_state")) {
323
+ transaction.objectStore("sync_state").clear();
324
+ }
325
+ if (db.objectStoreNames.contains("sync_revision")) {
326
+ const syncRevision = transaction.objectStore("sync_revision");
327
+ syncRevision.clear();
328
+ syncRevision.put({ id: 1, revision: "0" });
329
+ }
330
+ if (db.objectStoreNames.contains("settings")) {
331
+ transaction.objectStore("settings").delete("sync_initial_complete");
332
+ }
333
+ },
334
+ },
335
+ {
336
+ name: "Add preimage to lnurl_receive_metadata for LUD-21 and NIP-57",
337
+ upgrade: (db, transaction) => {
338
+ // IndexedDB doesn't need schema changes for new fields on existing stores.
339
+ // Just clear the lnurl_metadata_updated_after setting to force re-sync.
340
+ if (db.objectStoreNames.contains("settings")) {
341
+ const settings = transaction.objectStore("settings");
342
+ settings.delete("lnurl_metadata_updated_after");
343
+ }
344
+ }
345
+ },
346
+ {
347
+ name: "Backfill htlc_details for existing Lightning payments",
348
+ upgrade: (db, transaction) => {
349
+ if (db.objectStoreNames.contains("payments")) {
350
+ const paymentStore = transaction.objectStore("payments");
351
+ const getAllRequest = paymentStore.getAll();
352
+
353
+ getAllRequest.onsuccess = () => {
354
+ const payments = getAllRequest.result;
355
+ payments.forEach((payment) => {
356
+ let details;
357
+ if (typeof payment.details === "string") {
358
+ try {
359
+ details = JSON.parse(payment.details);
360
+ } catch (e) {
361
+ return;
362
+ }
363
+ } else {
364
+ details = payment.details;
365
+ }
366
+
367
+ if (details && details.type === "lightning" && !details.htlcDetails) {
368
+ let htlcStatus;
369
+ if (payment.status === "completed") {
370
+ htlcStatus = "preimageShared";
371
+ } else if (payment.status === "pending") {
372
+ htlcStatus = "waitingForPreimage";
373
+ } else {
374
+ htlcStatus = "returned";
375
+ }
376
+ details.htlcDetails = {
377
+ paymentHash: details.paymentHash,
378
+ preimage: details.preimage || null,
379
+ expiryTime: 0,
380
+ status: htlcStatus,
381
+ };
382
+ payment.details = JSON.stringify(details);
383
+ paymentStore.put(payment);
384
+ }
385
+ });
386
+ };
387
+ }
388
+
389
+ // Reset sync offset to trigger resync
390
+ if (db.objectStoreNames.contains("settings")) {
391
+ const settingsStore = transaction.objectStore("settings");
392
+ const getRequest = settingsStore.get("sync_offset");
393
+
394
+ getRequest.onsuccess = () => {
395
+ const syncCache = getRequest.result;
396
+ if (syncCache && syncCache.value) {
397
+ try {
398
+ const syncInfo = JSON.parse(syncCache.value);
399
+ syncInfo.offset = 0;
400
+ settingsStore.put({
401
+ key: "sync_offset",
402
+ value: JSON.stringify(syncInfo),
403
+ });
404
+ } catch (e) {
405
+ // If parsing fails, just continue
406
+ }
407
+ }
408
+ };
409
+ }
410
+ },
411
+ },
412
+ {
413
+ name: "Create contacts store",
414
+ upgrade: (db) => {
415
+ if (!db.objectStoreNames.contains("contacts")) {
416
+ const contactsStore = db.createObjectStore("contacts", { keyPath: "id" });
417
+ contactsStore.createIndex("name_identifier", ["name", "paymentIdentifier"], { unique: false });
418
+ contactsStore.createIndex("name", "name", { unique: false });
419
+ }
420
+ }
421
+ },
422
+ {
423
+ name: "Clear cached lightning address for LnurlInfo schema change",
424
+ upgrade: (db, transaction) => {
425
+ if (db.objectStoreNames.contains("settings")) {
426
+ const settings = transaction.objectStore("settings");
427
+ settings.delete("lightning_address");
428
+ }
429
+ }
430
+ },
431
+ {
432
+ name: "Drop preimage from lnurl_receive_metadata records",
433
+ upgrade: (db, transaction) => {
434
+ if (db.objectStoreNames.contains("lnurl_receive_metadata")) {
435
+ const store = transaction.objectStore("lnurl_receive_metadata");
436
+ const request = store.openCursor();
437
+ request.onsuccess = (event) => {
438
+ const cursor = event.target.result;
439
+ if (cursor) {
440
+ const record = cursor.value;
441
+ if ("preimage" in record) {
442
+ delete record.preimage;
443
+ cursor.update(record);
444
+ }
445
+ cursor.continue();
446
+ }
447
+ };
448
+ }
449
+ }
450
+ },
451
+ {
452
+ name: "Clear cached lightning address for CachedLightningAddress format change",
453
+ upgrade: (db, transaction) => {
454
+ if (db.objectStoreNames.contains("settings")) {
455
+ const settings = transaction.objectStore("settings");
456
+ settings.delete("lightning_address");
457
+ }
458
+ }
459
+ },
460
+ ];
461
+ }
462
+ }
463
+
464
+ class StorageError extends Error {
465
+ constructor(message, cause = null) {
466
+ super(message);
467
+ this.name = "StorageError";
468
+ this.cause = cause;
469
+
470
+ // Maintain proper stack trace for where our error was thrown (only available on V8)
471
+ if (Error.captureStackTrace) {
472
+ Error.captureStackTrace(this, StorageError);
473
+ }
474
+ }
475
+ }
476
+
477
+ class IndexedDBStorage {
478
+ constructor(dbName = "BreezSDK", logger = null) {
479
+ this.dbName = dbName;
480
+ this.db = null;
481
+ this.migrationManager = null;
482
+ this.logger = logger;
483
+ this.dbVersion = 15; // Current schema version
484
+ }
485
+
486
+ /**
487
+ * Initialize the storage - must be called before using other methods
488
+ */
489
+ async initialize() {
490
+ if (this.db) {
491
+ return this;
492
+ }
493
+
494
+ if (typeof window === "undefined" || !window.indexedDB) {
495
+ throw new StorageError("IndexedDB is not available in this environment");
496
+ }
497
+
498
+ return new Promise((resolve, reject) => {
499
+ const request = indexedDB.open(this.dbName, this.dbVersion);
500
+
501
+ request.onerror = () => {
502
+ const error = new StorageError(
503
+ `Failed to open IndexedDB: ${request.error?.message || "Unknown error"
504
+ }`,
505
+ request.error
506
+ );
507
+ reject(error);
508
+ };
509
+
510
+ request.onsuccess = () => {
511
+ this.db = request.result;
512
+ this.migrationManager = new MigrationManager(
513
+ this.db,
514
+ StorageError,
515
+ this.logger
516
+ );
517
+
518
+ // Handle unexpected version changes
519
+ this.db.onversionchange = () => {
520
+ this.db.close();
521
+ this.db = null;
522
+ };
523
+
524
+ resolve(this);
525
+ };
526
+
527
+ request.onupgradeneeded = (event) => {
528
+ this.db = event.target.result;
529
+ this.migrationManager = new MigrationManager(
530
+ this.db,
531
+ StorageError,
532
+ this.logger
533
+ );
534
+
535
+ try {
536
+ this.migrationManager.handleUpgrade(
537
+ event,
538
+ event.oldVersion,
539
+ event.newVersion
540
+ );
541
+ } catch (error) {
542
+ reject(error);
543
+ }
544
+ };
545
+ });
546
+ }
547
+
548
+ /**
549
+ * Close the database connection
550
+ */
551
+ close() {
552
+ if (this.db) {
553
+ this.db.close();
554
+ this.db = null;
555
+ }
556
+ }
557
+
558
+ // ===== Cache Operations =====
559
+
560
+ async getCachedItem(key) {
561
+ if (!this.db) {
562
+ throw new StorageError("Database not initialized");
563
+ }
564
+
565
+ return new Promise((resolve, reject) => {
566
+ const transaction = this.db.transaction("settings", "readonly");
567
+ const store = transaction.objectStore("settings");
568
+ const request = store.get(key);
569
+
570
+ request.onsuccess = () => {
571
+ const result = request.result;
572
+ resolve(result ? result.value : null);
573
+ };
574
+
575
+ request.onerror = () => {
576
+ reject(
577
+ new StorageError(
578
+ `Failed to get cached item '${key}': ${request.error?.message || "Unknown error"
579
+ }`,
580
+ request.error
581
+ )
582
+ );
583
+ };
584
+ });
585
+ }
586
+
587
+ async setCachedItem(key, value) {
588
+ if (!this.db) {
589
+ throw new StorageError("Database not initialized");
590
+ }
591
+
592
+ return new Promise((resolve, reject) => {
593
+ const transaction = this.db.transaction("settings", "readwrite");
594
+ const store = transaction.objectStore("settings");
595
+ const request = store.put({ key, value });
596
+
597
+ request.onsuccess = () => resolve();
598
+
599
+ request.onerror = () => {
600
+ reject(
601
+ new StorageError(
602
+ `Failed to set cached item '${key}': ${request.error?.message || "Unknown error"
603
+ }`,
604
+ request.error
605
+ )
606
+ );
607
+ };
608
+ });
609
+ }
610
+
611
+ async deleteCachedItem(key) {
612
+ if (!this.db) {
613
+ throw new StorageError("Database not initialized");
614
+ }
615
+
616
+ return new Promise((resolve, reject) => {
617
+ const transaction = this.db.transaction("settings", "readwrite");
618
+ const store = transaction.objectStore("settings");
619
+ const request = store.delete(key);
620
+
621
+ request.onsuccess = () => resolve();
622
+
623
+ request.onerror = () => {
624
+ reject(
625
+ new StorageError(
626
+ `Failed to delete cached item '${key}': ${request.error?.message || "Unknown error"
627
+ }`,
628
+ request.error
629
+ )
630
+ );
631
+ };
632
+ });
633
+ }
634
+
635
+ // ===== Payment Operations =====
636
+
637
+ /**
638
+ * Gets the set of payment IDs that are related payments (have a parentPaymentId).
639
+ * Uses the parentPaymentId index for efficient lookup.
640
+ * @param {IDBObjectStore} metadataStore - The payment_metadata object store
641
+ * @returns {Promise<Set<string>>} Set of payment IDs that are related payments
642
+ */
643
+ _getRelatedPaymentIds(metadataStore) {
644
+ return new Promise((resolve) => {
645
+ const relatedPaymentIds = new Set();
646
+
647
+ // Check if the parentPaymentId index exists (added in migration)
648
+ if (!metadataStore.indexNames.contains("parentPaymentId")) {
649
+ // Index doesn't exist yet, fall back to scanning all metadata
650
+ const cursorRequest = metadataStore.openCursor();
651
+ cursorRequest.onsuccess = (event) => {
652
+ const cursor = event.target.result;
653
+ if (cursor) {
654
+ if (cursor.value.parentPaymentId) {
655
+ relatedPaymentIds.add(cursor.value.paymentId);
656
+ }
657
+ cursor.continue();
658
+ } else {
659
+ resolve(relatedPaymentIds);
660
+ }
661
+ };
662
+ cursorRequest.onerror = () => resolve(new Set());
663
+ return;
664
+ }
665
+
666
+ // Use the parentPaymentId index to find all metadata entries with a parent
667
+ const index = metadataStore.index("parentPaymentId");
668
+ const cursorRequest = index.openCursor();
669
+
670
+ cursorRequest.onsuccess = (event) => {
671
+ const cursor = event.target.result;
672
+ if (cursor) {
673
+ // Only add if parentPaymentId is truthy (not null/undefined)
674
+ if (cursor.value.parentPaymentId) {
675
+ relatedPaymentIds.add(cursor.value.paymentId);
676
+ }
677
+ cursor.continue();
678
+ } else {
679
+ resolve(relatedPaymentIds);
680
+ }
681
+ };
682
+
683
+ cursorRequest.onerror = () => {
684
+ // If index lookup fails, return empty set and fall back to per-payment lookup
685
+ resolve(new Set());
686
+ };
687
+ });
688
+ }
689
+
690
+ async listPayments(request) {
691
+ if (!this.db) {
692
+ throw new StorageError("Database not initialized");
693
+ }
694
+
695
+ // Handle null values by using default values
696
+ const actualOffset = request.offset !== null ? request.offset : 0;
697
+ const actualLimit = request.limit !== null ? request.limit : 4294967295; // u32::MAX
698
+
699
+ const transaction = this.db.transaction(
700
+ ["payments", "payment_metadata", "lnurl_receive_metadata"],
701
+ "readonly"
702
+ );
703
+ const paymentStore = transaction.objectStore("payments");
704
+ const metadataStore = transaction.objectStore("payment_metadata");
705
+ const lnurlReceiveMetadataStore = transaction.objectStore("lnurl_receive_metadata");
706
+
707
+ // Build set of related payment IDs upfront for O(1) filtering
708
+ const relatedPaymentIds = await this._getRelatedPaymentIds(metadataStore);
709
+
710
+ return new Promise((resolve, reject) => {
711
+ const payments = [];
712
+ let count = 0;
713
+ let skipped = 0;
714
+
715
+ // Determine sort order - "prev" for descending (default), "next" for ascending
716
+ const cursorDirection = request.sortAscending ? "next" : "prev";
717
+
718
+ // Use cursor to iterate through payments ordered by timestamp
719
+ const cursorRequest = paymentStore
720
+ .index("timestamp")
721
+ .openCursor(null, cursorDirection);
722
+
723
+ cursorRequest.onsuccess = (event) => {
724
+ const cursor = event.target.result;
725
+
726
+ if (!cursor || count >= actualLimit) {
727
+ resolve(payments);
728
+ return;
729
+ }
730
+
731
+ const payment = cursor.value;
732
+
733
+ // Skip related payments (those with a parentPaymentId)
734
+ if (relatedPaymentIds.has(payment.id)) {
735
+ cursor.continue();
736
+ return;
737
+ }
738
+
739
+ if (skipped < actualOffset) {
740
+ skipped++;
741
+ cursor.continue();
742
+ return;
743
+ }
744
+
745
+ // Get metadata for this payment (now only for non-related payments)
746
+ const metadataRequest = metadataStore.get(payment.id);
747
+ metadataRequest.onsuccess = () => {
748
+ const metadata = metadataRequest.result;
749
+
750
+ const paymentWithMetadata = this._mergePaymentMetadata(
751
+ payment,
752
+ metadata
753
+ );
754
+
755
+ // Fetch lnurl receive metadata before filtering, so Lightning
756
+ // filters can check lnurlReceiveMetadata fields
757
+ this._fetchLnurlReceiveMetadata(
758
+ paymentWithMetadata,
759
+ lnurlReceiveMetadataStore
760
+ )
761
+ .then((mergedPayment) => {
762
+ // Apply filters after lnurl metadata is populated
763
+ if (!this._matchesFilters(mergedPayment, request)) {
764
+ cursor.continue();
765
+ return;
766
+ }
767
+ payments.push(mergedPayment);
768
+ count++;
769
+ cursor.continue();
770
+ })
771
+ .catch(() => {
772
+ // Apply filters even if lnurl metadata fetch fails
773
+ if (!this._matchesFilters(paymentWithMetadata, request)) {
774
+ cursor.continue();
775
+ return;
776
+ }
777
+ payments.push(paymentWithMetadata);
778
+ count++;
779
+ cursor.continue();
780
+ });
781
+ };
782
+ metadataRequest.onerror = () => {
783
+ // Continue without metadata if it fails
784
+ if (this._matchesFilters(payment, request)) {
785
+ payments.push(payment);
786
+ count++;
787
+ }
788
+
789
+ cursor.continue();
790
+ };
791
+ };
792
+
793
+ cursorRequest.onerror = () => {
794
+ reject(
795
+ new StorageError(
796
+ `Failed to list payments (request: ${JSON.stringify(request)}: ${cursorRequest.error?.message || "Unknown error"
797
+ }`,
798
+ cursorRequest.error
799
+ )
800
+ );
801
+ };
802
+ });
803
+ }
804
+
805
+ async insertPayment(payment) {
806
+ if (!this.db) {
807
+ throw new StorageError("Database not initialized");
808
+ }
809
+
810
+ return new Promise((resolve, reject) => {
811
+ const transaction = this.db.transaction("payments", "readwrite");
812
+ const store = transaction.objectStore("payments");
813
+
814
+ // Ensure details and method are serialized properly
815
+ const paymentToStore = {
816
+ ...payment,
817
+ details: payment.details ? JSON.stringify(payment.details) : null,
818
+ method: payment.method ? JSON.stringify(payment.method) : null,
819
+ };
820
+
821
+ const request = store.put(paymentToStore);
822
+ request.onsuccess = () => resolve();
823
+ request.onerror = () => {
824
+ reject(
825
+ new StorageError(
826
+ `Failed to insert payment '${payment.id}': ${request.error?.message || "Unknown error"
827
+ }`,
828
+ request.error
829
+ )
830
+ );
831
+ };
832
+ });
833
+ }
834
+
835
+ async getPaymentById(id) {
836
+ if (!this.db) {
837
+ throw new StorageError("Database not initialized");
838
+ }
839
+
840
+ return new Promise((resolve, reject) => {
841
+ const transaction = this.db.transaction(
842
+ ["payments", "payment_metadata", "lnurl_receive_metadata"],
843
+ "readonly"
844
+ );
845
+ const paymentStore = transaction.objectStore("payments");
846
+ const metadataStore = transaction.objectStore("payment_metadata");
847
+ const lnurlReceiveMetadataStore = transaction.objectStore(
848
+ "lnurl_receive_metadata"
849
+ );
850
+
851
+ const paymentRequest = paymentStore.get(id);
852
+
853
+ paymentRequest.onsuccess = () => {
854
+ const payment = paymentRequest.result;
855
+ if (!payment) {
856
+ reject(new StorageError(`Payment with id '${id}' not found`));
857
+ return;
858
+ }
859
+
860
+ // Get metadata for this payment
861
+ const metadataRequest = metadataStore.get(id);
862
+ metadataRequest.onsuccess = () => {
863
+ const metadata = metadataRequest.result;
864
+ const paymentWithMetadata = this._mergePaymentMetadata(
865
+ payment,
866
+ metadata
867
+ );
868
+
869
+ // Fetch lnurl receive metadata if it's a lightning payment
870
+ this._fetchLnurlReceiveMetadata(
871
+ paymentWithMetadata,
872
+ lnurlReceiveMetadataStore
873
+ )
874
+ .then(resolve)
875
+ .catch(() => {
876
+ // Continue without lnurl receive metadata if fetch fails
877
+ resolve(paymentWithMetadata);
878
+ });
879
+ };
880
+ metadataRequest.onerror = () => {
881
+ // Return payment without metadata if metadata fetch fails
882
+ resolve(payment);
883
+ };
884
+ };
885
+
886
+ paymentRequest.onerror = () => {
887
+ reject(
888
+ new StorageError(
889
+ `Failed to get payment by id '${id}': ${paymentRequest.error?.message || "Unknown error"
890
+ }`,
891
+ paymentRequest.error
892
+ )
893
+ );
894
+ };
895
+ });
896
+ }
897
+
898
+ async getPaymentByInvoice(invoice) {
899
+ if (!this.db) {
900
+ throw new StorageError("Database not initialized");
901
+ }
902
+
903
+ return new Promise((resolve, reject) => {
904
+ const transaction = this.db.transaction(
905
+ ["payments", "payment_metadata", "lnurl_receive_metadata"],
906
+ "readonly"
907
+ );
908
+ const paymentStore = transaction.objectStore("payments");
909
+ const invoiceIndex = paymentStore.index("invoice");
910
+ const metadataStore = transaction.objectStore("payment_metadata");
911
+ const lnurlReceiveMetadataStore = transaction.objectStore(
912
+ "lnurl_receive_metadata"
913
+ );
914
+
915
+ const paymentRequest = invoiceIndex.get(invoice);
916
+
917
+ paymentRequest.onsuccess = () => {
918
+ const payment = paymentRequest.result;
919
+ if (!payment) {
920
+ resolve(null);
921
+ return;
922
+ }
923
+
924
+ // Get metadata for this payment
925
+ const metadataRequest = metadataStore.get(payment.id);
926
+ metadataRequest.onsuccess = () => {
927
+ const metadata = metadataRequest.result;
928
+ const paymentWithMetadata = this._mergePaymentMetadata(
929
+ payment,
930
+ metadata
931
+ );
932
+
933
+ // Fetch lnurl receive metadata if it's a lightning payment
934
+ this._fetchLnurlReceiveMetadata(
935
+ paymentWithMetadata,
936
+ lnurlReceiveMetadataStore
937
+ )
938
+ .then(resolve)
939
+ .catch(() => {
940
+ // Continue without lnurl receive metadata if fetch fails
941
+ resolve(paymentWithMetadata);
942
+ });
943
+ };
944
+ metadataRequest.onerror = () => {
945
+ // Return payment without metadata if metadata fetch fails
946
+ resolve(payment);
947
+ };
948
+ };
949
+
950
+ paymentRequest.onerror = () => {
951
+ reject(
952
+ new StorageError(
953
+ `Failed to get payment by invoice '${invoice}': ${paymentRequest.error?.message || "Unknown error"
954
+ }`,
955
+ paymentRequest.error
956
+ )
957
+ );
958
+ };
959
+ });
960
+ }
961
+
962
+ /**
963
+ * Checks if any related payments exist (payments with a parentPaymentId).
964
+ * Uses the parentPaymentId index for efficient lookup.
965
+ * @param {IDBObjectStore} metadataStore - The payment_metadata object store
966
+ * @returns {Promise<boolean>} True if any related payments exist
967
+ */
968
+ _hasRelatedPayments(metadataStore) {
969
+ return new Promise((resolve) => {
970
+ // Check if the parentPaymentId index exists (added in migration)
971
+ if (!metadataStore.indexNames.contains("parentPaymentId")) {
972
+ // Index doesn't exist yet, fall back to scanning all metadata
973
+ const cursorRequest = metadataStore.openCursor();
974
+ cursorRequest.onsuccess = (event) => {
975
+ const cursor = event.target.result;
976
+ if (cursor) {
977
+ if (cursor.value.parentPaymentId) {
978
+ resolve(true);
979
+ return;
980
+ }
981
+ cursor.continue();
982
+ } else {
983
+ resolve(false);
984
+ }
985
+ };
986
+ cursorRequest.onerror = () => resolve(true); // Assume there might be related payments on error
987
+ return;
988
+ }
989
+
990
+ const index = metadataStore.index("parentPaymentId");
991
+ const cursorRequest = index.openCursor();
992
+
993
+ cursorRequest.onsuccess = (event) => {
994
+ const cursor = event.target.result;
995
+ if (cursor && cursor.value.parentPaymentId) {
996
+ // Found at least one related payment
997
+ resolve(true);
998
+ } else if (cursor) {
999
+ // Entry with null parentPaymentId, continue searching
1000
+ cursor.continue();
1001
+ } else {
1002
+ // No more entries
1003
+ resolve(false);
1004
+ }
1005
+ };
1006
+
1007
+ cursorRequest.onerror = () => {
1008
+ // If index lookup fails, assume there might be related payments
1009
+ resolve(true);
1010
+ };
1011
+ });
1012
+ }
1013
+
1014
+ /**
1015
+ * Gets payments that have any of the specified parent payment IDs.
1016
+ * @param {string[]} parentPaymentIds - Array of parent payment IDs
1017
+ * @returns {Promise<Object>} Map of parentPaymentId -> array of RelatedPayment objects
1018
+ */
1019
+ async getPaymentsByParentIds(parentPaymentIds) {
1020
+ if (!this.db) {
1021
+ throw new StorageError("Database not initialized");
1022
+ }
1023
+
1024
+ if (!parentPaymentIds || parentPaymentIds.length === 0) {
1025
+ return {};
1026
+ }
1027
+
1028
+ const transaction = this.db.transaction(
1029
+ ["payments", "payment_metadata", "lnurl_receive_metadata"],
1030
+ "readonly"
1031
+ );
1032
+ const metadataStore = transaction.objectStore("payment_metadata");
1033
+
1034
+ // Early exit if no related payments exist
1035
+ const hasRelated = await this._hasRelatedPayments(metadataStore);
1036
+ if (!hasRelated) {
1037
+ return {};
1038
+ }
1039
+
1040
+ const parentIdSet = new Set(parentPaymentIds);
1041
+ const paymentStore = transaction.objectStore("payments");
1042
+ const lnurlReceiveMetadataStore = transaction.objectStore("lnurl_receive_metadata");
1043
+
1044
+ return new Promise((resolve, reject) => {
1045
+ const result = {};
1046
+ const fetchedMetadata = [];
1047
+
1048
+ // Query all metadata records and filter by parentPaymentId
1049
+ const cursorRequest = metadataStore.openCursor();
1050
+
1051
+ cursorRequest.onsuccess = (event) => {
1052
+ const cursor = event.target.result;
1053
+ if (!cursor) {
1054
+ // All metadata processed, now fetch payment details
1055
+ if (fetchedMetadata.length === 0) {
1056
+ resolve(result);
1057
+ return;
1058
+ }
1059
+
1060
+ let processed = 0;
1061
+ for (const metadata of fetchedMetadata) {
1062
+ const parentId = metadata.parentPaymentId;
1063
+ const paymentRequest = paymentStore.get(metadata.paymentId);
1064
+ paymentRequest.onsuccess = () => {
1065
+ const payment = paymentRequest.result;
1066
+ if (payment) {
1067
+ const paymentWithMetadata = this._mergePaymentMetadata(payment, metadata);
1068
+
1069
+ if (!result[parentId]) {
1070
+ result[parentId] = [];
1071
+ }
1072
+
1073
+ // Fetch lnurl receive metadata if applicable
1074
+ this._fetchLnurlReceiveMetadata(paymentWithMetadata, lnurlReceiveMetadataStore)
1075
+ .then((mergedPayment) => {
1076
+ result[parentId].push(mergedPayment);
1077
+ })
1078
+ .catch(() => {
1079
+ result[parentId].push(paymentWithMetadata);
1080
+ })
1081
+ .finally(() => {
1082
+ processed++;
1083
+ if (processed === fetchedMetadata.length) {
1084
+ // Sort each parent's children by timestamp
1085
+ for (const parentId of Object.keys(result)) {
1086
+ result[parentId].sort((a, b) => a.timestamp - b.timestamp);
1087
+ }
1088
+ resolve(result);
1089
+ }
1090
+ });
1091
+ } else {
1092
+ processed++;
1093
+ if (processed === fetchedMetadata.length) {
1094
+ resolve(result);
1095
+ }
1096
+ }
1097
+ };
1098
+ paymentRequest.onerror = () => {
1099
+ processed++;
1100
+ if (processed === fetchedMetadata.length) {
1101
+ resolve(result);
1102
+ }
1103
+ };
1104
+ }
1105
+ return;
1106
+ }
1107
+
1108
+ const metadata = cursor.value;
1109
+ if (metadata.parentPaymentId && parentIdSet.has(metadata.parentPaymentId)) {
1110
+ fetchedMetadata.push(metadata);
1111
+ }
1112
+ cursor.continue();
1113
+ };
1114
+
1115
+ cursorRequest.onerror = () => {
1116
+ reject(
1117
+ new StorageError(
1118
+ `Failed to get payments by parent ids: ${cursorRequest.error?.message || "Unknown error"
1119
+ }`,
1120
+ cursorRequest.error
1121
+ )
1122
+ );
1123
+ };
1124
+ });
1125
+ }
1126
+
1127
+ async insertPaymentMetadata(paymentId, metadata) {
1128
+ if (!this.db) {
1129
+ throw new StorageError("Database not initialized");
1130
+ }
1131
+
1132
+ return new Promise((resolve, reject) => {
1133
+ const transaction = this.db.transaction("payment_metadata", "readwrite");
1134
+ const store = transaction.objectStore("payment_metadata");
1135
+
1136
+ // First get existing record to merge with
1137
+ const getRequest = store.get(paymentId);
1138
+ getRequest.onsuccess = () => {
1139
+ const existing = getRequest.result || {};
1140
+
1141
+ // Use COALESCE-like behavior: new value if non-null, otherwise keep existing
1142
+ const metadataToStore = {
1143
+ paymentId,
1144
+ parentPaymentId: metadata.parentPaymentId ?? existing.parentPaymentId ?? null,
1145
+ lnurlPayInfo: metadata.lnurlPayInfo
1146
+ ? JSON.stringify(metadata.lnurlPayInfo)
1147
+ : existing.lnurlPayInfo ?? null,
1148
+ lnurlWithdrawInfo: metadata.lnurlWithdrawInfo
1149
+ ? JSON.stringify(metadata.lnurlWithdrawInfo)
1150
+ : existing.lnurlWithdrawInfo ?? null,
1151
+ lnurlDescription: metadata.lnurlDescription ?? existing.lnurlDescription ?? null,
1152
+ conversionInfo: metadata.conversionInfo
1153
+ ? JSON.stringify(metadata.conversionInfo)
1154
+ : existing.conversionInfo ?? null,
1155
+ conversionStatus: metadata.conversionStatus ?? existing.conversionStatus ?? null,
1156
+ };
1157
+
1158
+ const putRequest = store.put(metadataToStore);
1159
+ putRequest.onsuccess = () => resolve();
1160
+ putRequest.onerror = () => {
1161
+ reject(
1162
+ new StorageError(
1163
+ `Failed to set payment metadata for '${paymentId}': ${putRequest.error?.message || "Unknown error"
1164
+ }`,
1165
+ putRequest.error
1166
+ )
1167
+ );
1168
+ };
1169
+ };
1170
+ getRequest.onerror = () => {
1171
+ reject(
1172
+ new StorageError(
1173
+ `Failed to get existing payment metadata for '${paymentId}': ${getRequest.error?.message || "Unknown error"
1174
+ }`,
1175
+ getRequest.error
1176
+ )
1177
+ );
1178
+ };
1179
+ });
1180
+ }
1181
+
1182
+ // ===== Deposit Operations =====
1183
+
1184
+ async addDeposit(txid, vout, amountSats, isMature) {
1185
+ if (!this.db) {
1186
+ throw new StorageError("Database not initialized");
1187
+ }
1188
+
1189
+ return new Promise((resolve, reject) => {
1190
+ const transaction = this.db.transaction(
1191
+ "unclaimed_deposits",
1192
+ "readwrite"
1193
+ );
1194
+ const store = transaction.objectStore("unclaimed_deposits");
1195
+
1196
+ // Get existing deposit first to preserve fields on upsert
1197
+ const getRequest = store.get([txid, vout]);
1198
+
1199
+ getRequest.onsuccess = () => {
1200
+ const existing = getRequest.result;
1201
+ const depositToStore = {
1202
+ txid,
1203
+ vout,
1204
+ amountSats,
1205
+ isMature: isMature ?? true,
1206
+ claimError: existing?.claimError ?? null,
1207
+ refundTx: existing?.refundTx ?? null,
1208
+ refundTxId: existing?.refundTxId ?? null,
1209
+ };
1210
+
1211
+ const putRequest = store.put(depositToStore);
1212
+ putRequest.onsuccess = () => resolve();
1213
+ putRequest.onerror = () => {
1214
+ reject(
1215
+ new StorageError(
1216
+ `Failed to add deposit '${txid}:${vout}': ${putRequest.error?.message || "Unknown error"
1217
+ }`,
1218
+ putRequest.error
1219
+ )
1220
+ );
1221
+ };
1222
+ };
1223
+
1224
+ getRequest.onerror = () => {
1225
+ reject(
1226
+ new StorageError(
1227
+ `Failed to add deposit '${txid}:${vout}': ${getRequest.error?.message || "Unknown error"
1228
+ }`,
1229
+ getRequest.error
1230
+ )
1231
+ );
1232
+ };
1233
+ });
1234
+ }
1235
+
1236
+ async deleteDeposit(txid, vout) {
1237
+ if (!this.db) {
1238
+ throw new StorageError("Database not initialized");
1239
+ }
1240
+
1241
+ return new Promise((resolve, reject) => {
1242
+ const transaction = this.db.transaction(
1243
+ "unclaimed_deposits",
1244
+ "readwrite"
1245
+ );
1246
+ const store = transaction.objectStore("unclaimed_deposits");
1247
+ const request = store.delete([txid, vout]);
1248
+
1249
+ request.onsuccess = () => resolve();
1250
+ request.onerror = () => {
1251
+ reject(
1252
+ new StorageError(
1253
+ `Failed to delete deposit '${txid}:${vout}': ${request.error?.message || "Unknown error"
1254
+ }`,
1255
+ request.error
1256
+ )
1257
+ );
1258
+ };
1259
+ });
1260
+ }
1261
+
1262
+ async listDeposits() {
1263
+ if (!this.db) {
1264
+ throw new StorageError("Database not initialized");
1265
+ }
1266
+
1267
+ return new Promise((resolve, reject) => {
1268
+ const transaction = this.db.transaction("unclaimed_deposits", "readonly");
1269
+ const store = transaction.objectStore("unclaimed_deposits");
1270
+ const request = store.getAll();
1271
+
1272
+ request.onsuccess = () => {
1273
+ const deposits = request.result.map((row) => ({
1274
+ txid: row.txid,
1275
+ vout: row.vout,
1276
+ amountSats: row.amountSats,
1277
+ isMature: row.isMature ?? true,
1278
+ claimError: row.claimError ? JSON.parse(row.claimError) : null,
1279
+ refundTx: row.refundTx,
1280
+ refundTxId: row.refundTxId,
1281
+ }));
1282
+ resolve(deposits);
1283
+ };
1284
+
1285
+ request.onerror = () => {
1286
+ reject(
1287
+ new StorageError(
1288
+ `Failed to list deposits: ${request.error?.message || "Unknown error"
1289
+ }`,
1290
+ request.error
1291
+ )
1292
+ );
1293
+ };
1294
+ });
1295
+ }
1296
+
1297
+ async updateDeposit(txid, vout, payload) {
1298
+ if (!this.db) {
1299
+ throw new StorageError("Database not initialized");
1300
+ }
1301
+
1302
+ return new Promise((resolve, reject) => {
1303
+ const transaction = this.db.transaction(
1304
+ "unclaimed_deposits",
1305
+ "readwrite"
1306
+ );
1307
+ const store = transaction.objectStore("unclaimed_deposits");
1308
+
1309
+ // First get the existing deposit
1310
+ const getRequest = store.get([txid, vout]);
1311
+
1312
+ getRequest.onsuccess = () => {
1313
+ const existingDeposit = getRequest.result;
1314
+ if (!existingDeposit) {
1315
+ // Deposit doesn't exist, just resolve (matches SQLite behavior)
1316
+ resolve();
1317
+ return;
1318
+ }
1319
+
1320
+ let updatedDeposit = { ...existingDeposit };
1321
+
1322
+ if (payload.type === "claimError") {
1323
+ updatedDeposit.claimError = JSON.stringify(payload.error);
1324
+ updatedDeposit.refundTx = null;
1325
+ updatedDeposit.refundTxId = null;
1326
+ } else if (payload.type === "refund") {
1327
+ updatedDeposit.refundTx = payload.refundTx;
1328
+ updatedDeposit.refundTxId = payload.refundTxid;
1329
+ updatedDeposit.claimError = null;
1330
+ } else {
1331
+ reject(new StorageError(`Unknown payload type: ${payload.type}`));
1332
+ return;
1333
+ }
1334
+
1335
+ const putRequest = store.put(updatedDeposit);
1336
+ putRequest.onsuccess = () => resolve();
1337
+ putRequest.onerror = () => {
1338
+ reject(
1339
+ new StorageError(
1340
+ `Failed to update deposit '${txid}:${vout}': ${putRequest.error?.message || "Unknown error"
1341
+ }`,
1342
+ putRequest.error
1343
+ )
1344
+ );
1345
+ };
1346
+ };
1347
+
1348
+ getRequest.onerror = () => {
1349
+ reject(
1350
+ new StorageError(
1351
+ `Failed to get deposit '${txid}:${vout}' for update: ${getRequest.error?.message || "Unknown error"
1352
+ }`,
1353
+ getRequest.error
1354
+ )
1355
+ );
1356
+ };
1357
+ });
1358
+ }
1359
+
1360
+ async setLnurlMetadata(metadata) {
1361
+ if (!this.db) {
1362
+ throw new StorageError("Database not initialized");
1363
+ }
1364
+
1365
+ return new Promise((resolve, reject) => {
1366
+ const transaction = this.db.transaction(
1367
+ "lnurl_receive_metadata",
1368
+ "readwrite"
1369
+ );
1370
+ const store = transaction.objectStore("lnurl_receive_metadata");
1371
+
1372
+ let completed = 0;
1373
+ const total = metadata.length;
1374
+
1375
+ if (total === 0) {
1376
+ resolve();
1377
+ return;
1378
+ }
1379
+
1380
+ for (const item of metadata) {
1381
+ const request = store.put({
1382
+ paymentHash: item.paymentHash,
1383
+ nostrZapRequest: item.nostrZapRequest || null,
1384
+ nostrZapReceipt: item.nostrZapReceipt || null,
1385
+ senderComment: item.senderComment || null,
1386
+ });
1387
+
1388
+ request.onsuccess = () => {
1389
+ completed++;
1390
+ if (completed === total) {
1391
+ resolve();
1392
+ }
1393
+ };
1394
+
1395
+ request.onerror = () => {
1396
+ reject(
1397
+ new StorageError(
1398
+ `Failed to add lnurl metadata for payment hash '${item.paymentHash
1399
+ }': ${request.error?.message || "Unknown error"}`,
1400
+ request.error
1401
+ )
1402
+ );
1403
+ };
1404
+ }
1405
+ });
1406
+ }
1407
+
1408
+ async syncAddOutgoingChange(record) {
1409
+ if (!this.db) {
1410
+ throw new StorageError("Database not initialized");
1411
+ }
1412
+
1413
+ return new Promise((resolve, reject) => {
1414
+ const transaction = this.db.transaction(["sync_outgoing"], "readwrite");
1415
+
1416
+ // This revision is a local queue id for pending rows, not a server revision.
1417
+ const outgoingStore = transaction.objectStore("sync_outgoing");
1418
+ const getAllOutgoingRequest = outgoingStore.getAll();
1419
+
1420
+ getAllOutgoingRequest.onsuccess = () => {
1421
+ const records = getAllOutgoingRequest.result;
1422
+ let maxOutgoingRevision = BigInt(0);
1423
+ for (const storeRecord of records) {
1424
+ const rev = BigInt(
1425
+ storeRecord.record.localRevision ?? storeRecord.record.revision
1426
+ );
1427
+ if (rev > maxOutgoingRevision) {
1428
+ maxOutgoingRevision = rev;
1429
+ }
1430
+ }
1431
+ const nextRevision = maxOutgoingRevision + BigInt(1);
1432
+
1433
+ const storeRecord = {
1434
+ type: record.id.type,
1435
+ dataId: record.id.dataId,
1436
+ revision: Number(nextRevision),
1437
+ record: {
1438
+ ...record,
1439
+ localRevision: nextRevision,
1440
+ },
1441
+ };
1442
+
1443
+ const addRequest = outgoingStore.add(storeRecord);
1444
+
1445
+ addRequest.onsuccess = () => {
1446
+ transaction.oncomplete = () => {
1447
+ resolve(nextRevision);
1448
+ };
1449
+ };
1450
+
1451
+ addRequest.onerror = (event) => {
1452
+ reject(
1453
+ new StorageError(
1454
+ `Failed to add outgoing change: ${event.target.error.message}`
1455
+ )
1456
+ );
1457
+ };
1458
+ };
1459
+
1460
+ getAllOutgoingRequest.onerror = (event) => {
1461
+ reject(
1462
+ new StorageError(
1463
+ `Failed to get outgoing records: ${event.target.error.message}`
1464
+ )
1465
+ );
1466
+ };
1467
+
1468
+ transaction.onerror = (event) => {
1469
+ reject(
1470
+ new StorageError(`Transaction failed: ${event.target.error.message}`)
1471
+ );
1472
+ };
1473
+ });
1474
+ }
1475
+
1476
+ async syncCompleteOutgoingSync(record, localRevision) {
1477
+ if (!this.db) {
1478
+ throw new StorageError("Database not initialized");
1479
+ }
1480
+
1481
+ return new Promise((resolve, reject) => {
1482
+ const transaction = this.db.transaction(
1483
+ ["sync_outgoing", "sync_state", "sync_revision"],
1484
+ "readwrite"
1485
+ );
1486
+ const outgoingStore = transaction.objectStore("sync_outgoing");
1487
+ const stateStore = transaction.objectStore("sync_state");
1488
+ const revisionStore = transaction.objectStore("sync_revision");
1489
+
1490
+ const deleteRequest = outgoingStore.delete([
1491
+ record.id.type,
1492
+ record.id.dataId,
1493
+ Number(localRevision),
1494
+ ]);
1495
+
1496
+ deleteRequest.onsuccess = () => {
1497
+ const stateRecord = {
1498
+ type: record.id.type,
1499
+ dataId: record.id.dataId,
1500
+ record: record,
1501
+ };
1502
+ stateStore.put(stateRecord);
1503
+
1504
+ // Update sync_revision to track the highest known revision
1505
+ const getRevisionRequest = revisionStore.get(1);
1506
+ getRevisionRequest.onsuccess = () => {
1507
+ const current = getRevisionRequest.result || { id: 1, revision: "0" };
1508
+ const currentRevision = BigInt(current.revision);
1509
+ const recordRevision = BigInt(record.revision);
1510
+ if (recordRevision > currentRevision) {
1511
+ revisionStore.put({ id: 1, revision: recordRevision.toString() });
1512
+ }
1513
+ resolve();
1514
+ };
1515
+ getRevisionRequest.onerror = (event) => {
1516
+ reject(
1517
+ new StorageError(
1518
+ `Failed to update sync revision: ${event.target.error.message}`
1519
+ )
1520
+ );
1521
+ };
1522
+ };
1523
+
1524
+ deleteRequest.onerror = (event) => {
1525
+ reject(
1526
+ new StorageError(
1527
+ `Failed to complete outgoing sync: ${event.target.error.message}`
1528
+ )
1529
+ );
1530
+ };
1531
+ });
1532
+ }
1533
+
1534
+ async syncGetPendingOutgoingChanges(limit) {
1535
+ if (!this.db) {
1536
+ throw new StorageError("Database not initialized");
1537
+ }
1538
+
1539
+ return new Promise((resolve, reject) => {
1540
+ const transaction = this.db.transaction(
1541
+ ["sync_outgoing", "sync_state"],
1542
+ "readonly"
1543
+ );
1544
+ const outgoingStore = transaction.objectStore("sync_outgoing");
1545
+ const stateStore = transaction.objectStore("sync_state");
1546
+
1547
+ // Get pending outgoing changes (all records in this store are pending)
1548
+ // Use revision index to order by revision ascending
1549
+ const revisionIndex = outgoingStore.index("revision");
1550
+ const request = revisionIndex.openCursor(null, "next");
1551
+ const changes = [];
1552
+ let count = 0;
1553
+
1554
+ request.onsuccess = (event) => {
1555
+ const cursor = event.target.result;
1556
+ if (cursor && count < limit) {
1557
+ const storeRecord = cursor.value;
1558
+ const change = {
1559
+ ...storeRecord.record,
1560
+ localRevision:
1561
+ storeRecord.record.localRevision ?? storeRecord.record.revision,
1562
+ };
1563
+
1564
+ // Look up parent record if it exists
1565
+ const stateRequest = stateStore.get([
1566
+ storeRecord.type,
1567
+ storeRecord.dataId,
1568
+ ]);
1569
+ stateRequest.onsuccess = () => {
1570
+ const stateRecord = stateRequest.result;
1571
+ const parent = stateRecord ? stateRecord.record : null;
1572
+
1573
+ changes.push({
1574
+ change: change,
1575
+ parent: parent,
1576
+ });
1577
+
1578
+ count++;
1579
+ cursor.continue();
1580
+ };
1581
+
1582
+ stateRequest.onerror = () => {
1583
+ changes.push({
1584
+ change: change,
1585
+ parent: null,
1586
+ });
1587
+
1588
+ count++;
1589
+ cursor.continue();
1590
+ };
1591
+ } else {
1592
+ resolve(changes);
1593
+ }
1594
+ };
1595
+
1596
+ request.onerror = (event) => {
1597
+ reject(
1598
+ new StorageError(
1599
+ `Failed to get pending outgoing changes: ${event.target.error.message}`
1600
+ )
1601
+ );
1602
+ };
1603
+ });
1604
+ }
1605
+
1606
+ async syncGetLastRevision() {
1607
+ if (!this.db) {
1608
+ throw new StorageError("Database not initialized");
1609
+ }
1610
+
1611
+ return new Promise((resolve, reject) => {
1612
+ const transaction = this.db.transaction("sync_revision", "readonly");
1613
+ const store = transaction.objectStore("sync_revision");
1614
+ const request = store.get(1);
1615
+
1616
+ request.onsuccess = () => {
1617
+ const result = request.result || { id: 1, revision: "0" };
1618
+ resolve(BigInt(result.revision));
1619
+ };
1620
+
1621
+ request.onerror = (event) => {
1622
+ reject(
1623
+ new StorageError(
1624
+ `Failed to get last revision: ${event.target.error.message}`
1625
+ )
1626
+ );
1627
+ };
1628
+ });
1629
+ }
1630
+
1631
+ async syncInsertIncomingRecords(records) {
1632
+ if (!this.db) {
1633
+ throw new StorageError("Database not initialized");
1634
+ }
1635
+
1636
+ return new Promise((resolve, reject) => {
1637
+ const transaction = this.db.transaction(["sync_incoming"], "readwrite");
1638
+ const store = transaction.objectStore("sync_incoming");
1639
+
1640
+ // Add each record to the incoming store
1641
+ let recordsProcessed = 0;
1642
+
1643
+ for (const record of records) {
1644
+ const storeRecord = {
1645
+ type: record.id.type,
1646
+ dataId: record.id.dataId,
1647
+ revision: Number(record.revision),
1648
+ record: record,
1649
+ };
1650
+
1651
+ const request = store.put(storeRecord);
1652
+
1653
+ request.onsuccess = () => {
1654
+ recordsProcessed++;
1655
+ if (recordsProcessed === records.length) {
1656
+ resolve();
1657
+ }
1658
+ };
1659
+
1660
+ request.onerror = (event) => {
1661
+ reject(
1662
+ new StorageError(
1663
+ `Failed to insert incoming record: ${event.target.error.message}`
1664
+ )
1665
+ );
1666
+ };
1667
+ }
1668
+
1669
+ // If no records were provided
1670
+ if (records.length === 0) {
1671
+ resolve();
1672
+ }
1673
+ });
1674
+ }
1675
+
1676
+ async syncDeleteIncomingRecord(record) {
1677
+ if (!this.db) {
1678
+ throw new StorageError("Database not initialized");
1679
+ }
1680
+
1681
+ return new Promise((resolve, reject) => {
1682
+ const transaction = this.db.transaction(["sync_incoming"], "readwrite");
1683
+ const store = transaction.objectStore("sync_incoming");
1684
+
1685
+ const key = [record.id.type, record.id.dataId, Number(record.revision)];
1686
+ const request = store.delete(key);
1687
+
1688
+ request.onsuccess = () => {
1689
+ resolve();
1690
+ };
1691
+
1692
+ request.onerror = (event) => {
1693
+ reject(
1694
+ new StorageError(
1695
+ `Failed to delete incoming record: ${event.target.error.message}`
1696
+ )
1697
+ );
1698
+ };
1699
+ });
1700
+ }
1701
+
1702
+ async syncGetIncomingRecords(limit) {
1703
+ if (!this.db) {
1704
+ throw new StorageError("Database not initialized");
1705
+ }
1706
+
1707
+ return new Promise((resolve, reject) => {
1708
+ const transaction = this.db.transaction(
1709
+ ["sync_incoming", "sync_state"],
1710
+ "readonly"
1711
+ );
1712
+ const incomingStore = transaction.objectStore("sync_incoming");
1713
+ const stateStore = transaction.objectStore("sync_state");
1714
+
1715
+ // Get records up to the limit, ordered by revision
1716
+ const revisionIndex = incomingStore.index("revision");
1717
+ const request = revisionIndex.openCursor(null, "next");
1718
+ const records = [];
1719
+ let count = 0;
1720
+
1721
+ request.onsuccess = (event) => {
1722
+ const cursor = event.target.result;
1723
+ if (cursor && count < limit) {
1724
+ const storeRecord = cursor.value;
1725
+ const newState = storeRecord.record;
1726
+
1727
+ // Look for parent record
1728
+ const stateRequest = stateStore.get([
1729
+ storeRecord.type,
1730
+ storeRecord.dataId,
1731
+ ]);
1732
+
1733
+ stateRequest.onsuccess = () => {
1734
+ const stateRecord = stateRequest.result;
1735
+ const oldState = stateRecord ? stateRecord.record : null;
1736
+
1737
+ records.push({
1738
+ newState: newState,
1739
+ oldState: oldState,
1740
+ });
1741
+
1742
+ count++;
1743
+ cursor.continue();
1744
+ };
1745
+
1746
+ stateRequest.onerror = () => {
1747
+ records.push({
1748
+ newState: newState,
1749
+ oldState: null,
1750
+ });
1751
+
1752
+ count++;
1753
+ cursor.continue();
1754
+ };
1755
+ } else {
1756
+ resolve(records);
1757
+ }
1758
+ };
1759
+
1760
+ request.onerror = (event) => {
1761
+ reject(
1762
+ new StorageError(
1763
+ `Failed to get incoming records: ${event.target.error.message}`
1764
+ )
1765
+ );
1766
+ };
1767
+ });
1768
+ }
1769
+
1770
+ async syncGetLatestOutgoingChange() {
1771
+ if (!this.db) {
1772
+ throw new StorageError("Database not initialized");
1773
+ }
1774
+
1775
+ return new Promise((resolve, reject) => {
1776
+ const transaction = this.db.transaction(
1777
+ ["sync_outgoing", "sync_state"],
1778
+ "readonly"
1779
+ );
1780
+ const outgoingStore = transaction.objectStore("sync_outgoing");
1781
+ const stateStore = transaction.objectStore("sync_state");
1782
+
1783
+ // Get the highest revision record
1784
+ const index = outgoingStore.index("revision");
1785
+ const request = index.openCursor(null, "prev");
1786
+
1787
+ request.onsuccess = (event) => {
1788
+ const cursor = event.target.result;
1789
+ if (cursor) {
1790
+ const storeRecord = cursor.value;
1791
+ const change = {
1792
+ ...storeRecord.record,
1793
+ localRevision:
1794
+ storeRecord.record.localRevision ?? storeRecord.record.revision,
1795
+ };
1796
+
1797
+ // Get the parent record
1798
+ const stateRequest = stateStore.get([
1799
+ storeRecord.type,
1800
+ storeRecord.dataId,
1801
+ ]);
1802
+
1803
+ stateRequest.onsuccess = () => {
1804
+ const stateRecord = stateRequest.result;
1805
+ const parent = stateRecord ? stateRecord.record : null;
1806
+
1807
+ resolve({
1808
+ change: change,
1809
+ parent: parent,
1810
+ });
1811
+ };
1812
+
1813
+ stateRequest.onerror = () => {
1814
+ resolve({
1815
+ change: change,
1816
+ parent: null,
1817
+ });
1818
+ };
1819
+ } else {
1820
+ // No records found
1821
+ resolve(null);
1822
+ }
1823
+ };
1824
+
1825
+ request.onerror = (event) => {
1826
+ reject(
1827
+ new StorageError(
1828
+ `Failed to get latest outgoing change: ${event.target.error.message}`
1829
+ )
1830
+ );
1831
+ };
1832
+ });
1833
+ }
1834
+
1835
+ async syncUpdateRecordFromIncoming(record) {
1836
+ if (!this.db) {
1837
+ throw new StorageError("Database not initialized");
1838
+ }
1839
+
1840
+ return new Promise((resolve, reject) => {
1841
+ const transaction = this.db.transaction(["sync_state", "sync_revision"], "readwrite");
1842
+ const stateStore = transaction.objectStore("sync_state");
1843
+ const revisionStore = transaction.objectStore("sync_revision");
1844
+
1845
+ const storeRecord = {
1846
+ type: record.id.type,
1847
+ dataId: record.id.dataId,
1848
+ record: record,
1849
+ };
1850
+
1851
+ const request = stateStore.put(storeRecord);
1852
+
1853
+ request.onsuccess = () => {
1854
+ // Update sync_revision to track the highest known revision
1855
+ const getRevisionRequest = revisionStore.get(1);
1856
+ getRevisionRequest.onsuccess = () => {
1857
+ const current = getRevisionRequest.result || { id: 1, revision: "0" };
1858
+ const currentRevision = BigInt(current.revision);
1859
+ const incomingRevision = BigInt(record.revision);
1860
+ if (incomingRevision > currentRevision) {
1861
+ revisionStore.put({ id: 1, revision: incomingRevision.toString() });
1862
+ }
1863
+ resolve();
1864
+ };
1865
+ getRevisionRequest.onerror = (event) => {
1866
+ reject(
1867
+ new StorageError(
1868
+ `Failed to update sync revision: ${event.target.error.message}`
1869
+ )
1870
+ );
1871
+ };
1872
+ };
1873
+
1874
+ request.onerror = (event) => {
1875
+ reject(
1876
+ new StorageError(
1877
+ `Failed to update record from incoming: ${event.target.error.message}`
1878
+ )
1879
+ );
1880
+ };
1881
+ });
1882
+ }
1883
+
1884
+ // ===== Contact Operations =====
1885
+
1886
+ async listContacts(request) {
1887
+ if (!this.db) {
1888
+ throw new StorageError("Database not initialized");
1889
+ }
1890
+
1891
+ const actualOffset = request.offset !== null && request.offset !== undefined ? request.offset : 0;
1892
+ const actualLimit = request.limit !== null && request.limit !== undefined ? request.limit : 4294967295;
1893
+
1894
+ return new Promise((resolve, reject) => {
1895
+ const transaction = this.db.transaction("contacts", "readonly");
1896
+ const store = transaction.objectStore("contacts");
1897
+ const nameIndex = store.index("name");
1898
+
1899
+ const contacts = [];
1900
+ let count = 0;
1901
+ let skipped = 0;
1902
+
1903
+ const cursorRequest = nameIndex.openCursor();
1904
+
1905
+ cursorRequest.onsuccess = (event) => {
1906
+ const cursor = event.target.result;
1907
+
1908
+ if (!cursor || count >= actualLimit) {
1909
+ resolve(contacts);
1910
+ return;
1911
+ }
1912
+
1913
+ if (skipped < actualOffset) {
1914
+ skipped++;
1915
+ cursor.continue();
1916
+ return;
1917
+ }
1918
+
1919
+ contacts.push(cursor.value);
1920
+ count++;
1921
+ cursor.continue();
1922
+ };
1923
+
1924
+ cursorRequest.onerror = () => {
1925
+ reject(
1926
+ new StorageError(
1927
+ `Failed to list contacts: ${cursorRequest.error?.message || "Unknown error"}`,
1928
+ cursorRequest.error
1929
+ )
1930
+ );
1931
+ };
1932
+ });
1933
+ }
1934
+
1935
+ async getContact(id) {
1936
+ if (!this.db) {
1937
+ throw new StorageError("Database not initialized");
1938
+ }
1939
+
1940
+ return new Promise((resolve, reject) => {
1941
+ const transaction = this.db.transaction("contacts", "readonly");
1942
+ const store = transaction.objectStore("contacts");
1943
+ const request = store.get(id);
1944
+
1945
+ request.onsuccess = () => {
1946
+ resolve(request.result || null);
1947
+ };
1948
+
1949
+ request.onerror = () => {
1950
+ reject(
1951
+ new StorageError(
1952
+ `Failed to get contact '${id}': ${request.error?.message || "Unknown error"}`,
1953
+ request.error
1954
+ )
1955
+ );
1956
+ };
1957
+ });
1958
+ }
1959
+
1960
+ async insertContact(contact) {
1961
+ if (!this.db) {
1962
+ throw new StorageError("Database not initialized");
1963
+ }
1964
+
1965
+ return new Promise((resolve, reject) => {
1966
+ const transaction = this.db.transaction("contacts", "readwrite");
1967
+ const store = transaction.objectStore("contacts");
1968
+
1969
+ // Preserve created_at from existing record on update
1970
+ const getRequest = store.get(contact.id);
1971
+ getRequest.onsuccess = () => {
1972
+ const existingById = getRequest.result;
1973
+ const toStore = existingById
1974
+ ? { ...contact, createdAt: existingById.createdAt }
1975
+ : contact;
1976
+
1977
+ const putRequest = store.put(toStore);
1978
+ putRequest.onsuccess = () => resolve();
1979
+ putRequest.onerror = () => {
1980
+ reject(
1981
+ new StorageError(
1982
+ `Inserting contact failed: ${putRequest.error?.name || "Unknown error"} - ${putRequest.error?.message || ""}`,
1983
+ putRequest.error
1984
+ )
1985
+ );
1986
+ };
1987
+ };
1988
+ getRequest.onerror = () => {
1989
+ reject(
1990
+ new StorageError(
1991
+ `Failed to check existing contact: ${getRequest.error?.message || "Unknown error"}`,
1992
+ getRequest.error
1993
+ )
1994
+ );
1995
+ };
1996
+ });
1997
+ }
1998
+
1999
+ async deleteContact(id) {
2000
+ if (!this.db) {
2001
+ throw new StorageError("Database not initialized");
2002
+ }
2003
+
2004
+ return new Promise((resolve, reject) => {
2005
+ const transaction = this.db.transaction("contacts", "readwrite");
2006
+ const store = transaction.objectStore("contacts");
2007
+ const request = store.delete(id);
2008
+
2009
+ request.onsuccess = () => resolve();
2010
+
2011
+ request.onerror = () => {
2012
+ reject(
2013
+ new StorageError(
2014
+ `Failed to delete contact '${id}': ${request.error?.message || "Unknown error"}`,
2015
+ request.error
2016
+ )
2017
+ );
2018
+ };
2019
+ });
2020
+ }
2021
+
2022
+ // ===== Private Helper Methods =====
2023
+
2024
+ _matchesFilters(payment, request) {
2025
+ // Filter by payment type
2026
+ if (request.typeFilter && request.typeFilter.length > 0) {
2027
+ if (!request.typeFilter.includes(payment.paymentType)) {
2028
+ return false;
2029
+ }
2030
+ }
2031
+
2032
+ // Filter by status
2033
+ if (request.statusFilter && request.statusFilter.length > 0) {
2034
+ if (!request.statusFilter.includes(payment.status)) {
2035
+ return false;
2036
+ }
2037
+ }
2038
+
2039
+ // Filter by timestamp range
2040
+ if (request.fromTimestamp !== null && request.fromTimestamp !== undefined) {
2041
+ if (payment.timestamp < request.fromTimestamp) {
2042
+ return false;
2043
+ }
2044
+ }
2045
+
2046
+ if (request.toTimestamp !== null && request.toTimestamp !== undefined) {
2047
+ if (payment.timestamp >= request.toTimestamp) {
2048
+ return false;
2049
+ }
2050
+ }
2051
+
2052
+ // Filter by payment details
2053
+ if (
2054
+ request.paymentDetailsFilter &&
2055
+ request.paymentDetailsFilter.length > 0
2056
+ ) {
2057
+ let details = null;
2058
+
2059
+ // Parse details if it's a string (stored in IndexedDB)
2060
+ if (payment.details && typeof payment.details === "string") {
2061
+ try {
2062
+ details = JSON.parse(payment.details);
2063
+ } catch (e) {
2064
+ // If parsing fails, treat as no details
2065
+ details = null;
2066
+ }
2067
+ } else {
2068
+ details = payment.details;
2069
+ }
2070
+
2071
+ if (!details) {
2072
+ return false;
2073
+ }
2074
+
2075
+ // Filter by payment details. If any filter matches, we include the payment
2076
+ let paymentDetailsFilterMatches = false;
2077
+ for (const paymentDetailsFilter of request.paymentDetailsFilter) {
2078
+ // Filter by HTLC status (Spark or Lightning)
2079
+ if (
2080
+ (paymentDetailsFilter.type === "spark" ||
2081
+ paymentDetailsFilter.type === "lightning") &&
2082
+ paymentDetailsFilter.htlcStatus != null &&
2083
+ paymentDetailsFilter.htlcStatus.length > 0
2084
+ ) {
2085
+ if (
2086
+ details.type !== paymentDetailsFilter.type ||
2087
+ !details.htlcDetails ||
2088
+ !paymentDetailsFilter.htlcStatus.includes(
2089
+ details.htlcDetails.status
2090
+ )
2091
+ ) {
2092
+ continue;
2093
+ }
2094
+ }
2095
+ // Filter by token conversion info presence
2096
+ if (
2097
+ (paymentDetailsFilter.type === "spark" ||
2098
+ paymentDetailsFilter.type === "token") &&
2099
+ paymentDetailsFilter.conversionRefundNeeded != null
2100
+ ) {
2101
+ if (
2102
+ details.type !== paymentDetailsFilter.type ||
2103
+ !details.conversionInfo
2104
+ ) {
2105
+ continue;
2106
+ }
2107
+
2108
+ if (
2109
+ paymentDetailsFilter.conversionRefundNeeded ===
2110
+ (details.conversionInfo.status !== "refundNeeded")
2111
+ ) {
2112
+ continue;
2113
+ }
2114
+ }
2115
+ // Filter by token transaction hash
2116
+ if (
2117
+ paymentDetailsFilter.type === "token" &&
2118
+ paymentDetailsFilter.txHash != null
2119
+ ) {
2120
+ if (
2121
+ details.type !== "token" ||
2122
+ details.txHash !== paymentDetailsFilter.txHash
2123
+ ) {
2124
+ continue;
2125
+ }
2126
+ }
2127
+ // Filter by token transaction type
2128
+ if (
2129
+ paymentDetailsFilter.type === "token" &&
2130
+ paymentDetailsFilter.txType != null
2131
+ ) {
2132
+ if (
2133
+ details.type !== "token" ||
2134
+ details.txType !== paymentDetailsFilter.txType
2135
+ ) {
2136
+ continue;
2137
+ }
2138
+ }
2139
+
2140
+
2141
+ paymentDetailsFilterMatches = true;
2142
+ break;
2143
+ }
2144
+
2145
+ if (!paymentDetailsFilterMatches) {
2146
+ return false;
2147
+ }
2148
+ }
2149
+
2150
+ // Filter by payment details/method
2151
+ if (request.assetFilter) {
2152
+ const assetFilter = request.assetFilter;
2153
+ let details = null;
2154
+
2155
+ // Parse details if it's a string (stored in IndexedDB)
2156
+ if (payment.details && typeof payment.details === "string") {
2157
+ try {
2158
+ details = JSON.parse(payment.details);
2159
+ } catch (e) {
2160
+ // If parsing fails, treat as no details
2161
+ details = null;
2162
+ }
2163
+ } else {
2164
+ details = payment.details;
2165
+ }
2166
+
2167
+ if (!details) {
2168
+ return false;
2169
+ }
2170
+
2171
+ if (assetFilter.type === "bitcoin" && details.type === "token") {
2172
+ return false;
2173
+ }
2174
+
2175
+ if (assetFilter.type === "token") {
2176
+ if (details.type !== "token") {
2177
+ return false;
2178
+ }
2179
+
2180
+ // Check token identifier if specified
2181
+ if (assetFilter.tokenIdentifier) {
2182
+ if (
2183
+ !details.metadata ||
2184
+ details.metadata.identifier !== assetFilter.tokenIdentifier
2185
+ ) {
2186
+ return false;
2187
+ }
2188
+ }
2189
+ }
2190
+ }
2191
+
2192
+ return true;
2193
+ }
2194
+
2195
+ _mergePaymentMetadata(payment, metadata) {
2196
+ let details = null;
2197
+ if (payment.details) {
2198
+ try {
2199
+ details = JSON.parse(payment.details);
2200
+ } catch (e) {
2201
+ throw new StorageError(
2202
+ `Failed to parse payment details JSON for payment ${payment.id}: ${e.message}`,
2203
+ e
2204
+ );
2205
+ }
2206
+ }
2207
+
2208
+ let method = null;
2209
+ if (payment.method) {
2210
+ try {
2211
+ method = JSON.parse(payment.method);
2212
+ } catch (e) {
2213
+ throw new StorageError(
2214
+ `Failed to parse payment method JSON for payment ${payment.id}: ${e.message}`,
2215
+ e
2216
+ );
2217
+ }
2218
+ }
2219
+
2220
+ if (details && details.type === "lightning" && !details.htlcDetails) {
2221
+ throw new StorageError(
2222
+ `htlc_details is required for Lightning payment ${payment.id}`
2223
+ );
2224
+ }
2225
+
2226
+ if (metadata && details) {
2227
+ if (details.type == "lightning") {
2228
+ if (metadata.lnurlDescription && !details.description) {
2229
+ details.description = metadata.lnurlDescription;
2230
+ }
2231
+ // If lnurlPayInfo exists, parse and add to details
2232
+ if (metadata.lnurlPayInfo) {
2233
+ try {
2234
+ details.lnurlPayInfo = JSON.parse(metadata.lnurlPayInfo);
2235
+ } catch (e) {
2236
+ throw new StorageError(
2237
+ `Failed to parse lnurlPayInfo JSON for payment ${payment.id}: ${e.message}`,
2238
+ e
2239
+ );
2240
+ }
2241
+ }
2242
+ // If lnurlWithdrawInfo exists, parse and add to details
2243
+ if (metadata.lnurlWithdrawInfo) {
2244
+ try {
2245
+ details.lnurlWithdrawInfo = JSON.parse(metadata.lnurlWithdrawInfo);
2246
+ } catch (e) {
2247
+ throw new StorageError(
2248
+ `Failed to parse lnurlWithdrawInfo JSON for payment ${payment.id}: ${e.message}`,
2249
+ e
2250
+ );
2251
+ }
2252
+ }
2253
+ } else if (details.type == "spark" || details.type == "token") {
2254
+ // If conversionInfo exists, parse and add to details
2255
+ if (metadata.conversionInfo) {
2256
+ try {
2257
+ details.conversionInfo = JSON.parse(metadata.conversionInfo);
2258
+ } catch (e) {
2259
+ throw new StorageError(
2260
+ `Failed to parse conversionInfo JSON for payment ${payment.id}: ${e.message}`,
2261
+ e
2262
+ );
2263
+ }
2264
+ }
2265
+ }
2266
+ }
2267
+
2268
+ return {
2269
+ id: payment.id,
2270
+ paymentType: payment.paymentType,
2271
+ status: payment.status,
2272
+ amount: payment.amount,
2273
+ fees: payment.fees,
2274
+ timestamp: payment.timestamp,
2275
+ method,
2276
+ details,
2277
+ conversionDetails: metadata?.conversionStatus
2278
+ ? { status: metadata.conversionStatus, from: null, to: null }
2279
+ : null,
2280
+ };
2281
+ }
2282
+
2283
+ _fetchLnurlReceiveMetadata(payment, lnurlReceiveMetadataStore) {
2284
+ // Only fetch for lightning payments with a payment hash
2285
+ if (
2286
+ !payment.details ||
2287
+ payment.details.type !== "lightning" ||
2288
+ !payment.details.htlcDetails?.paymentHash
2289
+ ) {
2290
+ return Promise.resolve(payment);
2291
+ }
2292
+
2293
+ if (!lnurlReceiveMetadataStore) {
2294
+ return Promise.resolve(payment);
2295
+ }
2296
+
2297
+ return new Promise((resolve, reject) => {
2298
+ const lnurlReceiveRequest = lnurlReceiveMetadataStore.get(
2299
+ payment.details.htlcDetails.paymentHash
2300
+ );
2301
+
2302
+ lnurlReceiveRequest.onsuccess = () => {
2303
+ const lnurlReceiveMetadata = lnurlReceiveRequest.result;
2304
+ if (lnurlReceiveMetadata) {
2305
+ payment.details.lnurlReceiveMetadata = {
2306
+ nostrZapRequest: lnurlReceiveMetadata.nostrZapRequest || null,
2307
+ nostrZapReceipt: lnurlReceiveMetadata.nostrZapReceipt || null,
2308
+ senderComment: lnurlReceiveMetadata.senderComment || null,
2309
+ };
2310
+ }
2311
+ resolve(payment);
2312
+ };
2313
+
2314
+ lnurlReceiveRequest.onerror = () => {
2315
+ // Continue without lnurlReceiveMetadata if fetch fails
2316
+ reject(new Error("Failed to fetch lnurl receive metadata"));
2317
+ };
2318
+ });
2319
+ }
2320
+ }
2321
+
2322
+ export async function createDefaultStorage(
2323
+ dbName = "BreezSdkSpark",
2324
+ logger = null
2325
+ ) {
2326
+ const storage = new IndexedDBStorage(dbName, logger);
2327
+ await storage.initialize();
2328
+ return storage;
2329
+ }
2330
+
2331
+ export { IndexedDBStorage, MigrationManager, StorageError };