@unicitylabs/sphere-sdk 0.5.0 → 0.5.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 (41) hide show
  1. package/dist/connect/index.cjs +3 -1
  2. package/dist/connect/index.cjs.map +1 -1
  3. package/dist/connect/index.js +3 -1
  4. package/dist/connect/index.js.map +1 -1
  5. package/dist/core/index.cjs +205 -41
  6. package/dist/core/index.cjs.map +1 -1
  7. package/dist/core/index.d.cts +22 -0
  8. package/dist/core/index.d.ts +22 -0
  9. package/dist/core/index.js +205 -41
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/impl/browser/connect/index.cjs +3 -1
  12. package/dist/impl/browser/connect/index.cjs.map +1 -1
  13. package/dist/impl/browser/connect/index.js +3 -1
  14. package/dist/impl/browser/connect/index.js.map +1 -1
  15. package/dist/impl/browser/index.cjs +3 -1
  16. package/dist/impl/browser/index.cjs.map +1 -1
  17. package/dist/impl/browser/index.js +3 -1
  18. package/dist/impl/browser/index.js.map +1 -1
  19. package/dist/impl/browser/ipfs.cjs +3 -1
  20. package/dist/impl/browser/ipfs.cjs.map +1 -1
  21. package/dist/impl/browser/ipfs.js +3 -1
  22. package/dist/impl/browser/ipfs.js.map +1 -1
  23. package/dist/impl/nodejs/connect/index.cjs +3 -1
  24. package/dist/impl/nodejs/connect/index.cjs.map +1 -1
  25. package/dist/impl/nodejs/connect/index.js +3 -1
  26. package/dist/impl/nodejs/connect/index.js.map +1 -1
  27. package/dist/impl/nodejs/index.cjs +3 -1
  28. package/dist/impl/nodejs/index.cjs.map +1 -1
  29. package/dist/impl/nodejs/index.js +3 -1
  30. package/dist/impl/nodejs/index.js.map +1 -1
  31. package/dist/index.cjs +205 -41
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +26 -0
  34. package/dist/index.d.ts +26 -0
  35. package/dist/index.js +205 -41
  36. package/dist/index.js.map +1 -1
  37. package/dist/l1/index.cjs +3 -1
  38. package/dist/l1/index.cjs.map +1 -1
  39. package/dist/l1/index.js +3 -1
  40. package/dist/l1/index.js.map +1 -1
  41. package/package.json +1 -1
@@ -1928,6 +1928,11 @@ declare class PaymentsModule {
1928
1928
  private proofPollingInterval;
1929
1929
  private static readonly PROOF_POLLING_INTERVAL_MS;
1930
1930
  private static readonly PROOF_POLLING_MAX_ATTEMPTS;
1931
+ private resolveUnconfirmedTimer;
1932
+ private static readonly RESOLVE_UNCONFIRMED_INTERVAL_MS;
1933
+ private loadedPromise;
1934
+ private loaded;
1935
+ private processedSplitGroupIds;
1931
1936
  private storageEventUnsubscribers;
1932
1937
  private syncDebounceTimer;
1933
1938
  private static readonly SYNC_DEBOUNCE_MS;
@@ -2225,6 +2230,13 @@ declare class PaymentsModule {
2225
2230
  * @returns Summary with counts of resolved, still-pending, and failed tokens plus per-token details.
2226
2231
  */
2227
2232
  resolveUnconfirmed(): Promise<UnconfirmedResolutionResult>;
2233
+ /**
2234
+ * Start a periodic interval that retries resolveUnconfirmed() until all
2235
+ * tokens are confirmed or failed. Stops automatically when nothing is
2236
+ * pending and is cleaned up by destroy().
2237
+ */
2238
+ private scheduleResolveUnconfirmed;
2239
+ private stopResolveUnconfirmedPolling;
2228
2240
  /**
2229
2241
  * Process a single V5 token through its finalization stages with quick-timeout proof checks.
2230
2242
  */
@@ -2258,6 +2270,16 @@ declare class PaymentsModule {
2258
2270
  * Called during load() to restore tokens that TXF format can't represent.
2259
2271
  */
2260
2272
  private loadPendingV5Tokens;
2273
+ /**
2274
+ * Persist the set of processed splitGroupIds to KV storage.
2275
+ * This ensures Nostr re-deliveries are ignored across page reloads,
2276
+ * even when the confirmed token's in-memory ID differs from v5split_{id}.
2277
+ */
2278
+ private saveProcessedSplitGroupIds;
2279
+ /**
2280
+ * Load processed splitGroupIds from KV storage.
2281
+ */
2282
+ private loadProcessedSplitGroupIds;
2261
2283
  /**
2262
2284
  * Add a token to the wallet.
2263
2285
  *
@@ -1928,6 +1928,11 @@ declare class PaymentsModule {
1928
1928
  private proofPollingInterval;
1929
1929
  private static readonly PROOF_POLLING_INTERVAL_MS;
1930
1930
  private static readonly PROOF_POLLING_MAX_ATTEMPTS;
1931
+ private resolveUnconfirmedTimer;
1932
+ private static readonly RESOLVE_UNCONFIRMED_INTERVAL_MS;
1933
+ private loadedPromise;
1934
+ private loaded;
1935
+ private processedSplitGroupIds;
1931
1936
  private storageEventUnsubscribers;
1932
1937
  private syncDebounceTimer;
1933
1938
  private static readonly SYNC_DEBOUNCE_MS;
@@ -2225,6 +2230,13 @@ declare class PaymentsModule {
2225
2230
  * @returns Summary with counts of resolved, still-pending, and failed tokens plus per-token details.
2226
2231
  */
2227
2232
  resolveUnconfirmed(): Promise<UnconfirmedResolutionResult>;
2233
+ /**
2234
+ * Start a periodic interval that retries resolveUnconfirmed() until all
2235
+ * tokens are confirmed or failed. Stops automatically when nothing is
2236
+ * pending and is cleaned up by destroy().
2237
+ */
2238
+ private scheduleResolveUnconfirmed;
2239
+ private stopResolveUnconfirmedPolling;
2228
2240
  /**
2229
2241
  * Process a single V5 token through its finalization stages with quick-timeout proof checks.
2230
2242
  */
@@ -2258,6 +2270,16 @@ declare class PaymentsModule {
2258
2270
  * Called during load() to restore tokens that TXF format can't represent.
2259
2271
  */
2260
2272
  private loadPendingV5Tokens;
2273
+ /**
2274
+ * Persist the set of processed splitGroupIds to KV storage.
2275
+ * This ensures Nostr re-deliveries are ignored across page reloads,
2276
+ * even when the confirmed token's in-memory ID differs from v5split_{id}.
2277
+ */
2278
+ private saveProcessedSplitGroupIds;
2279
+ /**
2280
+ * Load processed splitGroupIds from KV storage.
2281
+ */
2282
+ private loadProcessedSplitGroupIds;
2261
2283
  /**
2262
2284
  * Add a token to the wallet.
2263
2285
  *
@@ -87,7 +87,9 @@ var init_constants = __esm({
87
87
  /** Group chat: members for this address */
88
88
  GROUP_CHAT_MEMBERS: "group_chat_members",
89
89
  /** Group chat: processed event IDs for deduplication */
90
- GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events"
90
+ GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
91
+ /** Processed V5 split group IDs for Nostr re-delivery dedup */
92
+ PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids"
91
93
  };
92
94
  STORAGE_KEYS = {
93
95
  ...STORAGE_KEYS_GLOBAL,
@@ -4300,6 +4302,17 @@ var PaymentsModule = class _PaymentsModule {
4300
4302
  // Poll every 2s
4301
4303
  static PROOF_POLLING_MAX_ATTEMPTS = 30;
4302
4304
  // Max 30 attempts (~60s)
4305
+ // Periodic retry for resolveUnconfirmed (V5 lazy finalization)
4306
+ resolveUnconfirmedTimer = null;
4307
+ static RESOLVE_UNCONFIRMED_INTERVAL_MS = 1e4;
4308
+ // Retry every 10s
4309
+ // Guard: ensure load() completes before processing incoming bundles
4310
+ loadedPromise = null;
4311
+ loaded = false;
4312
+ // Persistent dedup: tracks splitGroupIds that have been fully processed.
4313
+ // Survives page reloads via KV storage so Nostr re-deliveries are ignored
4314
+ // even when the confirmed token's in-memory ID differs from v5split_{id}.
4315
+ processedSplitGroupIds = /* @__PURE__ */ new Set();
4303
4316
  // Storage event subscriptions (push-based sync)
4304
4317
  storageEventUnsubscribers = [];
4305
4318
  syncDebounceTimer = null;
@@ -4385,31 +4398,40 @@ var PaymentsModule = class _PaymentsModule {
4385
4398
  */
4386
4399
  async load() {
4387
4400
  this.ensureInitialized();
4388
- await TokenRegistry.waitForReady();
4389
- const providers = this.getTokenStorageProviders();
4390
- for (const [id, provider] of providers) {
4391
- try {
4392
- const result = await provider.load();
4393
- if (result.success && result.data) {
4394
- this.loadFromStorageData(result.data);
4395
- this.log(`Loaded metadata from provider ${id}`);
4396
- break;
4401
+ const doLoad = async () => {
4402
+ await TokenRegistry.waitForReady();
4403
+ const providers = this.getTokenStorageProviders();
4404
+ for (const [id, provider] of providers) {
4405
+ try {
4406
+ const result = await provider.load();
4407
+ if (result.success && result.data) {
4408
+ this.loadFromStorageData(result.data);
4409
+ this.log(`Loaded metadata from provider ${id}`);
4410
+ break;
4411
+ }
4412
+ } catch (err) {
4413
+ console.error(`[Payments] Failed to load from provider ${id}:`, err);
4397
4414
  }
4398
- } catch (err) {
4399
- console.error(`[Payments] Failed to load from provider ${id}:`, err);
4400
4415
  }
4401
- }
4402
- await this.loadPendingV5Tokens();
4403
- await this.loadHistory();
4404
- const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
4405
- if (pending2) {
4406
- const transfers = JSON.parse(pending2);
4407
- for (const transfer of transfers) {
4408
- this.pendingTransfers.set(transfer.id, transfer);
4416
+ const loadedTokens = Array.from(this.tokens.values()).map((t) => `${t.id.slice(0, 12)}(${t.status})`);
4417
+ console.log(`[Payments][DEBUG] load(): from TXF providers: ${this.tokens.size} tokens [${loadedTokens.join(", ")}]`);
4418
+ await this.loadPendingV5Tokens();
4419
+ await this.loadProcessedSplitGroupIds();
4420
+ await this.loadHistory();
4421
+ const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
4422
+ if (pending2) {
4423
+ const transfers = JSON.parse(pending2);
4424
+ for (const transfer of transfers) {
4425
+ this.pendingTransfers.set(transfer.id, transfer);
4426
+ }
4409
4427
  }
4410
- }
4428
+ this.loaded = true;
4429
+ };
4430
+ this.loadedPromise = doLoad();
4431
+ await this.loadedPromise;
4411
4432
  this.resolveUnconfirmed().catch(() => {
4412
4433
  });
4434
+ this.scheduleResolveUnconfirmed();
4413
4435
  }
4414
4436
  /**
4415
4437
  * Cleanup all subscriptions, polling jobs, and pending resolvers.
@@ -4428,6 +4450,7 @@ var PaymentsModule = class _PaymentsModule {
4428
4450
  this.paymentRequestResponseHandlers.clear();
4429
4451
  this.stopProofPolling();
4430
4452
  this.proofPollingJobs.clear();
4453
+ this.stopResolveUnconfirmedPolling();
4431
4454
  for (const [, resolver] of this.pendingResponseResolvers) {
4432
4455
  clearTimeout(resolver.timeout);
4433
4456
  resolver.reject(new Error("Module destroyed"));
@@ -4834,13 +4857,16 @@ var PaymentsModule = class _PaymentsModule {
4834
4857
  */
4835
4858
  async processInstantSplitBundle(bundle, senderPubkey, memo) {
4836
4859
  this.ensureInitialized();
4860
+ if (!this.loaded && this.loadedPromise) {
4861
+ await this.loadedPromise;
4862
+ }
4837
4863
  if (!isInstantSplitBundleV5(bundle)) {
4838
4864
  return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
4839
4865
  }
4840
4866
  try {
4841
4867
  const deterministicId = `v5split_${bundle.splitGroupId}`;
4842
- if (this.tokens.has(deterministicId)) {
4843
- this.log(`V5 bundle ${deterministicId.slice(0, 16)}... already exists, skipping duplicate`);
4868
+ if (this.tokens.has(deterministicId) || this.processedSplitGroupIds.has(bundle.splitGroupId)) {
4869
+ console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
4844
4870
  return { success: true, durationMs: 0 };
4845
4871
  }
4846
4872
  const registry = TokenRegistry.getInstance();
@@ -4866,7 +4892,8 @@ var PaymentsModule = class _PaymentsModule {
4866
4892
  sdkData: JSON.stringify({ _pendingFinalization: pendingData })
4867
4893
  };
4868
4894
  await this.addToken(uiToken);
4869
- this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
4895
+ this.processedSplitGroupIds.add(bundle.splitGroupId);
4896
+ await this.saveProcessedSplitGroupIds();
4870
4897
  const senderInfo = await this.resolveSenderInfo(senderPubkey);
4871
4898
  await this.addToHistory({
4872
4899
  type: "RECEIVED",
@@ -4890,6 +4917,7 @@ var PaymentsModule = class _PaymentsModule {
4890
4917
  await this.save();
4891
4918
  this.resolveUnconfirmed().catch(() => {
4892
4919
  });
4920
+ this.scheduleResolveUnconfirmed();
4893
4921
  return { success: true, durationMs: 0 };
4894
4922
  } catch (error) {
4895
4923
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -5636,28 +5664,70 @@ var PaymentsModule = class _PaymentsModule {
5636
5664
  };
5637
5665
  const stClient = this.deps.oracle.getStateTransitionClient?.();
5638
5666
  const trustBase = this.deps.oracle.getTrustBase?.();
5639
- if (!stClient || !trustBase) return result;
5667
+ if (!stClient || !trustBase) {
5668
+ console.log(`[V5-RESOLVE] resolveUnconfirmed: EARLY EXIT \u2014 stClient=${!!stClient} trustBase=${!!trustBase}`);
5669
+ return result;
5670
+ }
5640
5671
  const signingService = await this.createSigningService();
5672
+ const submittedCount = Array.from(this.tokens.values()).filter((t) => t.status === "submitted").length;
5673
+ console.log(`[V5-RESOLVE] resolveUnconfirmed: ${submittedCount} submitted token(s) to process`);
5641
5674
  for (const [tokenId, token] of this.tokens) {
5642
5675
  if (token.status !== "submitted") continue;
5643
5676
  const pending2 = this.parsePendingFinalization(token.sdkData);
5644
5677
  if (!pending2) {
5678
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 16)}: no pending finalization metadata, skipping`);
5645
5679
  result.stillPending++;
5646
5680
  continue;
5647
5681
  }
5648
5682
  if (pending2.type === "v5_bundle") {
5683
+ console.log(`[V5-RESOLVE] Processing ${tokenId.slice(0, 16)}... stage=${pending2.stage} attempt=${pending2.attemptCount}`);
5649
5684
  const progress = await this.resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService);
5685
+ console.log(`[V5-RESOLVE] Result for ${tokenId.slice(0, 16)}...: ${progress} (stage now: ${pending2.stage})`);
5650
5686
  result.details.push({ tokenId, stage: pending2.stage, status: progress });
5651
5687
  if (progress === "resolved") result.resolved++;
5652
5688
  else if (progress === "failed") result.failed++;
5653
5689
  else result.stillPending++;
5654
5690
  }
5655
5691
  }
5656
- if (result.resolved > 0 || result.failed > 0) {
5692
+ if (result.resolved > 0 || result.failed > 0 || result.stillPending > 0) {
5693
+ console.log(`[V5-RESOLVE] Saving: resolved=${result.resolved} failed=${result.failed} stillPending=${result.stillPending}`);
5657
5694
  await this.save();
5658
5695
  }
5659
5696
  return result;
5660
5697
  }
5698
+ /**
5699
+ * Start a periodic interval that retries resolveUnconfirmed() until all
5700
+ * tokens are confirmed or failed. Stops automatically when nothing is
5701
+ * pending and is cleaned up by destroy().
5702
+ */
5703
+ scheduleResolveUnconfirmed() {
5704
+ if (this.resolveUnconfirmedTimer) return;
5705
+ const hasUnconfirmed = Array.from(this.tokens.values()).some(
5706
+ (t) => t.status === "submitted"
5707
+ );
5708
+ if (!hasUnconfirmed) {
5709
+ console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: no submitted tokens, not starting timer`);
5710
+ return;
5711
+ }
5712
+ console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: starting periodic retry (every ${_PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS}ms)`);
5713
+ this.resolveUnconfirmedTimer = setInterval(async () => {
5714
+ try {
5715
+ const result = await this.resolveUnconfirmed();
5716
+ if (result.stillPending === 0) {
5717
+ console.log(`[V5-RESOLVE] All tokens resolved, stopping periodic retry`);
5718
+ this.stopResolveUnconfirmedPolling();
5719
+ }
5720
+ } catch (err) {
5721
+ console.log(`[V5-RESOLVE] Periodic retry error:`, err);
5722
+ }
5723
+ }, _PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS);
5724
+ }
5725
+ stopResolveUnconfirmedPolling() {
5726
+ if (this.resolveUnconfirmedTimer) {
5727
+ clearInterval(this.resolveUnconfirmedTimer);
5728
+ this.resolveUnconfirmedTimer = null;
5729
+ }
5730
+ }
5661
5731
  // ===========================================================================
5662
5732
  // Private - V5 Lazy Resolution Helpers
5663
5733
  // ===========================================================================
@@ -5670,10 +5740,12 @@ var PaymentsModule = class _PaymentsModule {
5670
5740
  pending2.lastAttemptAt = Date.now();
5671
5741
  try {
5672
5742
  if (pending2.stage === "RECEIVED") {
5743
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: RECEIVED \u2192 submitting mint commitment...`);
5673
5744
  const mintDataJson = JSON.parse(bundle.recipientMintData);
5674
5745
  const mintData = await MintTransactionData3.fromJSON(mintDataJson);
5675
5746
  const mintCommitment = await MintCommitment3.create(mintData);
5676
5747
  const mintResponse = await stClient.submitMintCommitment(mintCommitment);
5748
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint response status=${mintResponse.status}`);
5677
5749
  if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
5678
5750
  throw new Error(`Mint submission failed: ${mintResponse.status}`);
5679
5751
  }
@@ -5681,22 +5753,27 @@ var PaymentsModule = class _PaymentsModule {
5681
5753
  this.updatePendingFinalization(token, pending2);
5682
5754
  }
5683
5755
  if (pending2.stage === "MINT_SUBMITTED") {
5756
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_SUBMITTED \u2192 checking mint proof...`);
5684
5757
  const mintDataJson = JSON.parse(bundle.recipientMintData);
5685
5758
  const mintData = await MintTransactionData3.fromJSON(mintDataJson);
5686
5759
  const mintCommitment = await MintCommitment3.create(mintData);
5687
5760
  const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
5688
5761
  if (!proof) {
5762
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof not yet available, staying MINT_SUBMITTED`);
5689
5763
  this.updatePendingFinalization(token, pending2);
5690
5764
  return "pending";
5691
5765
  }
5766
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof obtained!`);
5692
5767
  pending2.mintProofJson = JSON.stringify(proof);
5693
5768
  pending2.stage = "MINT_PROVEN";
5694
5769
  this.updatePendingFinalization(token, pending2);
5695
5770
  }
5696
5771
  if (pending2.stage === "MINT_PROVEN") {
5772
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_PROVEN \u2192 submitting transfer commitment...`);
5697
5773
  const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
5698
5774
  const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
5699
5775
  const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
5776
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer response status=${transferResponse.status}`);
5700
5777
  if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
5701
5778
  throw new Error(`Transfer submission failed: ${transferResponse.status}`);
5702
5779
  }
@@ -5704,13 +5781,16 @@ var PaymentsModule = class _PaymentsModule {
5704
5781
  this.updatePendingFinalization(token, pending2);
5705
5782
  }
5706
5783
  if (pending2.stage === "TRANSFER_SUBMITTED") {
5784
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: TRANSFER_SUBMITTED \u2192 checking transfer proof...`);
5707
5785
  const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
5708
5786
  const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
5709
5787
  const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
5710
5788
  if (!proof) {
5789
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof not yet available, staying TRANSFER_SUBMITTED`);
5711
5790
  this.updatePendingFinalization(token, pending2);
5712
5791
  return "pending";
5713
5792
  }
5793
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof obtained! Finalizing...`);
5714
5794
  const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
5715
5795
  const confirmedToken = {
5716
5796
  id: token.id,
@@ -5726,6 +5806,12 @@ var PaymentsModule = class _PaymentsModule {
5726
5806
  sdkData: JSON.stringify(finalizedToken.toJSON())
5727
5807
  };
5728
5808
  this.tokens.set(tokenId, confirmedToken);
5809
+ this.deps.emitEvent("transfer:confirmed", {
5810
+ id: crypto.randomUUID(),
5811
+ status: "completed",
5812
+ tokens: [confirmedToken],
5813
+ tokenTransfers: []
5814
+ });
5729
5815
  this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
5730
5816
  return "resolved";
5731
5817
  }
@@ -5867,11 +5953,20 @@ var PaymentsModule = class _PaymentsModule {
5867
5953
  }
5868
5954
  }
5869
5955
  if (pendingTokens.length > 0) {
5956
+ const json = JSON.stringify(pendingTokens);
5957
+ this.log(`[V5-PERSIST] Saving ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")} (${json.length} bytes)`);
5870
5958
  await this.deps.storage.set(
5871
5959
  STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
5872
- JSON.stringify(pendingTokens)
5960
+ json
5873
5961
  );
5962
+ const verify = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
5963
+ if (!verify) {
5964
+ console.error("[Payments][V5-PERSIST] CRITICAL: KV write succeeded but read-back is empty!");
5965
+ } else {
5966
+ this.log(`[V5-PERSIST] Verified: read-back ${verify.length} bytes`);
5967
+ }
5874
5968
  } else {
5969
+ this.log(`[V5-PERSIST] No pending V5 tokens to save (total tokens: ${this.tokens.size}), clearing KV`);
5875
5970
  await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
5876
5971
  }
5877
5972
  }
@@ -5881,16 +5976,47 @@ var PaymentsModule = class _PaymentsModule {
5881
5976
  */
5882
5977
  async loadPendingV5Tokens() {
5883
5978
  const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
5979
+ this.log(`[V5-PERSIST] loadPendingV5Tokens: KV data = ${data ? `${data.length} bytes` : "null/empty"}`);
5884
5980
  if (!data) return;
5885
5981
  try {
5886
5982
  const pendingTokens = JSON.parse(data);
5983
+ this.log(`[V5-PERSIST] Parsed ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")}`);
5887
5984
  for (const token of pendingTokens) {
5888
5985
  if (!this.tokens.has(token.id)) {
5889
5986
  this.tokens.set(token.id, token);
5987
+ this.log(`[V5-PERSIST] Restored token ${token.id.slice(0, 16)} (status=${token.status})`);
5988
+ } else {
5989
+ this.log(`[V5-PERSIST] Token ${token.id.slice(0, 16)} already in map, skipping`);
5890
5990
  }
5891
5991
  }
5892
- if (pendingTokens.length > 0) {
5893
- this.log(`Restored ${pendingTokens.length} pending V5 token(s)`);
5992
+ } catch (err) {
5993
+ console.error("[Payments][V5-PERSIST] Failed to parse pending V5 tokens:", err);
5994
+ }
5995
+ }
5996
+ /**
5997
+ * Persist the set of processed splitGroupIds to KV storage.
5998
+ * This ensures Nostr re-deliveries are ignored across page reloads,
5999
+ * even when the confirmed token's in-memory ID differs from v5split_{id}.
6000
+ */
6001
+ async saveProcessedSplitGroupIds() {
6002
+ const ids = Array.from(this.processedSplitGroupIds);
6003
+ if (ids.length > 0) {
6004
+ await this.deps.storage.set(
6005
+ STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS,
6006
+ JSON.stringify(ids)
6007
+ );
6008
+ }
6009
+ }
6010
+ /**
6011
+ * Load processed splitGroupIds from KV storage.
6012
+ */
6013
+ async loadProcessedSplitGroupIds() {
6014
+ const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS);
6015
+ if (!data) return;
6016
+ try {
6017
+ const ids = JSON.parse(data);
6018
+ for (const id of ids) {
6019
+ this.processedSplitGroupIds.add(id);
5894
6020
  }
5895
6021
  } catch {
5896
6022
  }
@@ -6545,7 +6671,32 @@ var PaymentsModule = class _PaymentsModule {
6545
6671
  try {
6546
6672
  const result = await provider.sync(localData);
6547
6673
  if (result.success && result.merged) {
6674
+ const savedTokens = new Map(this.tokens);
6548
6675
  this.loadFromStorageData(result.merged);
6676
+ let restoredCount = 0;
6677
+ for (const [tokenId, token] of savedTokens) {
6678
+ if (this.tokens.has(tokenId)) continue;
6679
+ const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
6680
+ const stateHash = extractStateHashFromSdkData(token.sdkData);
6681
+ if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
6682
+ continue;
6683
+ }
6684
+ if (sdkTokenId) {
6685
+ let hasEquivalent = false;
6686
+ for (const existing of this.tokens.values()) {
6687
+ if (extractTokenIdFromSdkData(existing.sdkData) === sdkTokenId) {
6688
+ hasEquivalent = true;
6689
+ break;
6690
+ }
6691
+ }
6692
+ if (hasEquivalent) continue;
6693
+ }
6694
+ this.tokens.set(tokenId, token);
6695
+ restoredCount++;
6696
+ }
6697
+ if (restoredCount > 0) {
6698
+ console.log(`[Payments] Sync: restored ${restoredCount} token(s) lost by loadFromStorageData`);
6699
+ }
6549
6700
  if (this.nametags.length === 0 && savedNametags.length > 0) {
6550
6701
  this.nametags = savedNametags;
6551
6702
  }
@@ -6879,8 +7030,9 @@ var PaymentsModule = class _PaymentsModule {
6879
7030
  return;
6880
7031
  }
6881
7032
  this.tokens.set(token.id, token);
7033
+ console.log(`[Payments][DEBUG] NOSTR-FIRST: saving token id=${token.id.slice(0, 16)} status=${token.status} sdkData.length=${token.sdkData?.length}`);
6882
7034
  await this.save();
6883
- this.log(`NOSTR-FIRST: Token ${token.id.slice(0, 8)}... added as submitted (unconfirmed)`);
7035
+ console.log(`[Payments][DEBUG] NOSTR-FIRST: save() completed, tokens.size=${this.tokens.size}`);
6884
7036
  const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
6885
7037
  const incomingTransfer = {
6886
7038
  id: transfer.id,
@@ -7037,8 +7189,12 @@ var PaymentsModule = class _PaymentsModule {
7037
7189
  }
7038
7190
  }
7039
7191
  async handleIncomingTransfer(transfer) {
7192
+ if (!this.loaded && this.loadedPromise) {
7193
+ await this.loadedPromise;
7194
+ }
7040
7195
  try {
7041
7196
  const payload = transfer.payload;
7197
+ console.log("[Payments][DEBUG] handleIncomingTransfer: keys=", Object.keys(payload).join(","));
7042
7198
  let instantBundle = null;
7043
7199
  if (isInstantSplitBundle(payload)) {
7044
7200
  instantBundle = payload;
@@ -7070,7 +7226,7 @@ var PaymentsModule = class _PaymentsModule {
7070
7226
  return;
7071
7227
  }
7072
7228
  if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
7073
- this.log("Processing NOSTR-FIRST commitment-only transfer...");
7229
+ console.log("[Payments][DEBUG] >>> NOSTR-FIRST commitment-only transfer detected");
7074
7230
  await this.handleCommitmentOnlyTransfer(transfer, payload);
7075
7231
  return;
7076
7232
  }
@@ -7233,17 +7389,24 @@ var PaymentsModule = class _PaymentsModule {
7233
7389
  // ===========================================================================
7234
7390
  async save() {
7235
7391
  const providers = this.getTokenStorageProviders();
7236
- if (providers.size === 0) {
7237
- this.log("No token storage providers - tokens not persisted");
7238
- return;
7239
- }
7240
- const data = await this.createStorageData();
7241
- for (const [id, provider] of providers) {
7242
- try {
7243
- await provider.save(data);
7244
- } catch (err) {
7245
- console.error(`[Payments] Failed to save to provider ${id}:`, err);
7392
+ const tokenStats = Array.from(this.tokens.values()).map((t) => {
7393
+ const txf = tokenToTxf(t);
7394
+ return `${t.id.slice(0, 12)}(${t.status},txf=${!!txf})`;
7395
+ });
7396
+ console.log(`[Payments][DEBUG] save(): providers=${providers.size}, tokens=[${tokenStats.join(", ")}]`);
7397
+ if (providers.size > 0) {
7398
+ const data = await this.createStorageData();
7399
+ const dataKeys = Object.keys(data).filter((k) => k.startsWith("token-"));
7400
+ console.log(`[Payments][DEBUG] save(): TXF keys=${dataKeys.length} (${dataKeys.join(", ")})`);
7401
+ for (const [id, provider] of providers) {
7402
+ try {
7403
+ await provider.save(data);
7404
+ } catch (err) {
7405
+ console.error(`[Payments] Failed to save to provider ${id}:`, err);
7406
+ }
7246
7407
  }
7408
+ } else {
7409
+ console.log("[Payments][DEBUG] save(): No token storage providers - TXF not persisted");
7247
7410
  }
7248
7411
  await this.savePendingV5Tokens();
7249
7412
  }
@@ -7279,6 +7442,7 @@ var PaymentsModule = class _PaymentsModule {
7279
7442
  }
7280
7443
  loadFromStorageData(data) {
7281
7444
  const parsed = parseTxfStorageData(data);
7445
+ console.log(`[Payments][DEBUG] loadFromStorageData: parsed ${parsed.tokens.length} tokens, ${parsed.tombstones.length} tombstones, errors=[${parsed.validationErrors.join("; ")}]`);
7282
7446
  this.tombstones = parsed.tombstones;
7283
7447
  this.tokens.clear();
7284
7448
  for (const token of parsed.tokens) {