@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
package/dist/index.d.cts CHANGED
@@ -2572,6 +2572,11 @@ declare class PaymentsModule {
2572
2572
  private proofPollingInterval;
2573
2573
  private static readonly PROOF_POLLING_INTERVAL_MS;
2574
2574
  private static readonly PROOF_POLLING_MAX_ATTEMPTS;
2575
+ private resolveUnconfirmedTimer;
2576
+ private static readonly RESOLVE_UNCONFIRMED_INTERVAL_MS;
2577
+ private loadedPromise;
2578
+ private loaded;
2579
+ private processedSplitGroupIds;
2575
2580
  private storageEventUnsubscribers;
2576
2581
  private syncDebounceTimer;
2577
2582
  private static readonly SYNC_DEBOUNCE_MS;
@@ -2869,6 +2874,13 @@ declare class PaymentsModule {
2869
2874
  * @returns Summary with counts of resolved, still-pending, and failed tokens plus per-token details.
2870
2875
  */
2871
2876
  resolveUnconfirmed(): Promise<UnconfirmedResolutionResult>;
2877
+ /**
2878
+ * Start a periodic interval that retries resolveUnconfirmed() until all
2879
+ * tokens are confirmed or failed. Stops automatically when nothing is
2880
+ * pending and is cleaned up by destroy().
2881
+ */
2882
+ private scheduleResolveUnconfirmed;
2883
+ private stopResolveUnconfirmedPolling;
2872
2884
  /**
2873
2885
  * Process a single V5 token through its finalization stages with quick-timeout proof checks.
2874
2886
  */
@@ -2902,6 +2914,16 @@ declare class PaymentsModule {
2902
2914
  * Called during load() to restore tokens that TXF format can't represent.
2903
2915
  */
2904
2916
  private loadPendingV5Tokens;
2917
+ /**
2918
+ * Persist the set of processed splitGroupIds to KV storage.
2919
+ * This ensures Nostr re-deliveries are ignored across page reloads,
2920
+ * even when the confirmed token's in-memory ID differs from v5split_{id}.
2921
+ */
2922
+ private saveProcessedSplitGroupIds;
2923
+ /**
2924
+ * Load processed splitGroupIds from KV storage.
2925
+ */
2926
+ private loadProcessedSplitGroupIds;
2905
2927
  /**
2906
2928
  * Add a token to the wallet.
2907
2929
  *
@@ -3755,6 +3777,8 @@ declare const STORAGE_KEYS_ADDRESS: {
3755
3777
  readonly GROUP_CHAT_MEMBERS: "group_chat_members";
3756
3778
  /** Group chat: processed event IDs for deduplication */
3757
3779
  readonly GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events";
3780
+ /** Processed V5 split group IDs for Nostr re-delivery dedup */
3781
+ readonly PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids";
3758
3782
  };
3759
3783
  /** @deprecated Use STORAGE_KEYS_GLOBAL and STORAGE_KEYS_ADDRESS instead */
3760
3784
  declare const STORAGE_KEYS: {
@@ -3778,6 +3802,8 @@ declare const STORAGE_KEYS: {
3778
3802
  readonly GROUP_CHAT_MEMBERS: "group_chat_members";
3779
3803
  /** Group chat: processed event IDs for deduplication */
3780
3804
  readonly GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events";
3805
+ /** Processed V5 split group IDs for Nostr re-delivery dedup */
3806
+ readonly PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids";
3781
3807
  /** Encrypted BIP39 mnemonic */
3782
3808
  readonly MNEMONIC: "mnemonic";
3783
3809
  /** Encrypted master private key */
package/dist/index.d.ts CHANGED
@@ -2572,6 +2572,11 @@ declare class PaymentsModule {
2572
2572
  private proofPollingInterval;
2573
2573
  private static readonly PROOF_POLLING_INTERVAL_MS;
2574
2574
  private static readonly PROOF_POLLING_MAX_ATTEMPTS;
2575
+ private resolveUnconfirmedTimer;
2576
+ private static readonly RESOLVE_UNCONFIRMED_INTERVAL_MS;
2577
+ private loadedPromise;
2578
+ private loaded;
2579
+ private processedSplitGroupIds;
2575
2580
  private storageEventUnsubscribers;
2576
2581
  private syncDebounceTimer;
2577
2582
  private static readonly SYNC_DEBOUNCE_MS;
@@ -2869,6 +2874,13 @@ declare class PaymentsModule {
2869
2874
  * @returns Summary with counts of resolved, still-pending, and failed tokens plus per-token details.
2870
2875
  */
2871
2876
  resolveUnconfirmed(): Promise<UnconfirmedResolutionResult>;
2877
+ /**
2878
+ * Start a periodic interval that retries resolveUnconfirmed() until all
2879
+ * tokens are confirmed or failed. Stops automatically when nothing is
2880
+ * pending and is cleaned up by destroy().
2881
+ */
2882
+ private scheduleResolveUnconfirmed;
2883
+ private stopResolveUnconfirmedPolling;
2872
2884
  /**
2873
2885
  * Process a single V5 token through its finalization stages with quick-timeout proof checks.
2874
2886
  */
@@ -2902,6 +2914,16 @@ declare class PaymentsModule {
2902
2914
  * Called during load() to restore tokens that TXF format can't represent.
2903
2915
  */
2904
2916
  private loadPendingV5Tokens;
2917
+ /**
2918
+ * Persist the set of processed splitGroupIds to KV storage.
2919
+ * This ensures Nostr re-deliveries are ignored across page reloads,
2920
+ * even when the confirmed token's in-memory ID differs from v5split_{id}.
2921
+ */
2922
+ private saveProcessedSplitGroupIds;
2923
+ /**
2924
+ * Load processed splitGroupIds from KV storage.
2925
+ */
2926
+ private loadProcessedSplitGroupIds;
2905
2927
  /**
2906
2928
  * Add a token to the wallet.
2907
2929
  *
@@ -3755,6 +3777,8 @@ declare const STORAGE_KEYS_ADDRESS: {
3755
3777
  readonly GROUP_CHAT_MEMBERS: "group_chat_members";
3756
3778
  /** Group chat: processed event IDs for deduplication */
3757
3779
  readonly GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events";
3780
+ /** Processed V5 split group IDs for Nostr re-delivery dedup */
3781
+ readonly PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids";
3758
3782
  };
3759
3783
  /** @deprecated Use STORAGE_KEYS_GLOBAL and STORAGE_KEYS_ADDRESS instead */
3760
3784
  declare const STORAGE_KEYS: {
@@ -3778,6 +3802,8 @@ declare const STORAGE_KEYS: {
3778
3802
  readonly GROUP_CHAT_MEMBERS: "group_chat_members";
3779
3803
  /** Group chat: processed event IDs for deduplication */
3780
3804
  readonly GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events";
3805
+ /** Processed V5 split group IDs for Nostr re-delivery dedup */
3806
+ readonly PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids";
3781
3807
  /** Encrypted BIP39 mnemonic */
3782
3808
  readonly MNEMONIC: "mnemonic";
3783
3809
  /** Encrypted master private key */
package/dist/index.js CHANGED
@@ -91,7 +91,9 @@ var init_constants = __esm({
91
91
  /** Group chat: members for this address */
92
92
  GROUP_CHAT_MEMBERS: "group_chat_members",
93
93
  /** Group chat: processed event IDs for deduplication */
94
- GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events"
94
+ GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
95
+ /** Processed V5 split group IDs for Nostr re-delivery dedup */
96
+ PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids"
95
97
  };
96
98
  STORAGE_KEYS = {
97
99
  ...STORAGE_KEYS_GLOBAL,
@@ -4595,6 +4597,17 @@ var PaymentsModule = class _PaymentsModule {
4595
4597
  // Poll every 2s
4596
4598
  static PROOF_POLLING_MAX_ATTEMPTS = 30;
4597
4599
  // Max 30 attempts (~60s)
4600
+ // Periodic retry for resolveUnconfirmed (V5 lazy finalization)
4601
+ resolveUnconfirmedTimer = null;
4602
+ static RESOLVE_UNCONFIRMED_INTERVAL_MS = 1e4;
4603
+ // Retry every 10s
4604
+ // Guard: ensure load() completes before processing incoming bundles
4605
+ loadedPromise = null;
4606
+ loaded = false;
4607
+ // Persistent dedup: tracks splitGroupIds that have been fully processed.
4608
+ // Survives page reloads via KV storage so Nostr re-deliveries are ignored
4609
+ // even when the confirmed token's in-memory ID differs from v5split_{id}.
4610
+ processedSplitGroupIds = /* @__PURE__ */ new Set();
4598
4611
  // Storage event subscriptions (push-based sync)
4599
4612
  storageEventUnsubscribers = [];
4600
4613
  syncDebounceTimer = null;
@@ -4680,31 +4693,40 @@ var PaymentsModule = class _PaymentsModule {
4680
4693
  */
4681
4694
  async load() {
4682
4695
  this.ensureInitialized();
4683
- await TokenRegistry.waitForReady();
4684
- const providers = this.getTokenStorageProviders();
4685
- for (const [id, provider] of providers) {
4686
- try {
4687
- const result = await provider.load();
4688
- if (result.success && result.data) {
4689
- this.loadFromStorageData(result.data);
4690
- this.log(`Loaded metadata from provider ${id}`);
4691
- break;
4696
+ const doLoad = async () => {
4697
+ await TokenRegistry.waitForReady();
4698
+ const providers = this.getTokenStorageProviders();
4699
+ for (const [id, provider] of providers) {
4700
+ try {
4701
+ const result = await provider.load();
4702
+ if (result.success && result.data) {
4703
+ this.loadFromStorageData(result.data);
4704
+ this.log(`Loaded metadata from provider ${id}`);
4705
+ break;
4706
+ }
4707
+ } catch (err) {
4708
+ console.error(`[Payments] Failed to load from provider ${id}:`, err);
4692
4709
  }
4693
- } catch (err) {
4694
- console.error(`[Payments] Failed to load from provider ${id}:`, err);
4695
4710
  }
4696
- }
4697
- await this.loadPendingV5Tokens();
4698
- await this.loadHistory();
4699
- const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
4700
- if (pending2) {
4701
- const transfers = JSON.parse(pending2);
4702
- for (const transfer of transfers) {
4703
- this.pendingTransfers.set(transfer.id, transfer);
4711
+ const loadedTokens = Array.from(this.tokens.values()).map((t) => `${t.id.slice(0, 12)}(${t.status})`);
4712
+ console.log(`[Payments][DEBUG] load(): from TXF providers: ${this.tokens.size} tokens [${loadedTokens.join(", ")}]`);
4713
+ await this.loadPendingV5Tokens();
4714
+ await this.loadProcessedSplitGroupIds();
4715
+ await this.loadHistory();
4716
+ const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
4717
+ if (pending2) {
4718
+ const transfers = JSON.parse(pending2);
4719
+ for (const transfer of transfers) {
4720
+ this.pendingTransfers.set(transfer.id, transfer);
4721
+ }
4704
4722
  }
4705
- }
4723
+ this.loaded = true;
4724
+ };
4725
+ this.loadedPromise = doLoad();
4726
+ await this.loadedPromise;
4706
4727
  this.resolveUnconfirmed().catch(() => {
4707
4728
  });
4729
+ this.scheduleResolveUnconfirmed();
4708
4730
  }
4709
4731
  /**
4710
4732
  * Cleanup all subscriptions, polling jobs, and pending resolvers.
@@ -4723,6 +4745,7 @@ var PaymentsModule = class _PaymentsModule {
4723
4745
  this.paymentRequestResponseHandlers.clear();
4724
4746
  this.stopProofPolling();
4725
4747
  this.proofPollingJobs.clear();
4748
+ this.stopResolveUnconfirmedPolling();
4726
4749
  for (const [, resolver] of this.pendingResponseResolvers) {
4727
4750
  clearTimeout(resolver.timeout);
4728
4751
  resolver.reject(new Error("Module destroyed"));
@@ -5129,13 +5152,16 @@ var PaymentsModule = class _PaymentsModule {
5129
5152
  */
5130
5153
  async processInstantSplitBundle(bundle, senderPubkey, memo) {
5131
5154
  this.ensureInitialized();
5155
+ if (!this.loaded && this.loadedPromise) {
5156
+ await this.loadedPromise;
5157
+ }
5132
5158
  if (!isInstantSplitBundleV5(bundle)) {
5133
5159
  return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
5134
5160
  }
5135
5161
  try {
5136
5162
  const deterministicId = `v5split_${bundle.splitGroupId}`;
5137
- if (this.tokens.has(deterministicId)) {
5138
- this.log(`V5 bundle ${deterministicId.slice(0, 16)}... already exists, skipping duplicate`);
5163
+ if (this.tokens.has(deterministicId) || this.processedSplitGroupIds.has(bundle.splitGroupId)) {
5164
+ console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
5139
5165
  return { success: true, durationMs: 0 };
5140
5166
  }
5141
5167
  const registry = TokenRegistry.getInstance();
@@ -5161,7 +5187,8 @@ var PaymentsModule = class _PaymentsModule {
5161
5187
  sdkData: JSON.stringify({ _pendingFinalization: pendingData })
5162
5188
  };
5163
5189
  await this.addToken(uiToken);
5164
- this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
5190
+ this.processedSplitGroupIds.add(bundle.splitGroupId);
5191
+ await this.saveProcessedSplitGroupIds();
5165
5192
  const senderInfo = await this.resolveSenderInfo(senderPubkey);
5166
5193
  await this.addToHistory({
5167
5194
  type: "RECEIVED",
@@ -5185,6 +5212,7 @@ var PaymentsModule = class _PaymentsModule {
5185
5212
  await this.save();
5186
5213
  this.resolveUnconfirmed().catch(() => {
5187
5214
  });
5215
+ this.scheduleResolveUnconfirmed();
5188
5216
  return { success: true, durationMs: 0 };
5189
5217
  } catch (error) {
5190
5218
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -5931,28 +5959,70 @@ var PaymentsModule = class _PaymentsModule {
5931
5959
  };
5932
5960
  const stClient = this.deps.oracle.getStateTransitionClient?.();
5933
5961
  const trustBase = this.deps.oracle.getTrustBase?.();
5934
- if (!stClient || !trustBase) return result;
5962
+ if (!stClient || !trustBase) {
5963
+ console.log(`[V5-RESOLVE] resolveUnconfirmed: EARLY EXIT \u2014 stClient=${!!stClient} trustBase=${!!trustBase}`);
5964
+ return result;
5965
+ }
5935
5966
  const signingService = await this.createSigningService();
5967
+ const submittedCount = Array.from(this.tokens.values()).filter((t) => t.status === "submitted").length;
5968
+ console.log(`[V5-RESOLVE] resolveUnconfirmed: ${submittedCount} submitted token(s) to process`);
5936
5969
  for (const [tokenId, token] of this.tokens) {
5937
5970
  if (token.status !== "submitted") continue;
5938
5971
  const pending2 = this.parsePendingFinalization(token.sdkData);
5939
5972
  if (!pending2) {
5973
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 16)}: no pending finalization metadata, skipping`);
5940
5974
  result.stillPending++;
5941
5975
  continue;
5942
5976
  }
5943
5977
  if (pending2.type === "v5_bundle") {
5978
+ console.log(`[V5-RESOLVE] Processing ${tokenId.slice(0, 16)}... stage=${pending2.stage} attempt=${pending2.attemptCount}`);
5944
5979
  const progress = await this.resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService);
5980
+ console.log(`[V5-RESOLVE] Result for ${tokenId.slice(0, 16)}...: ${progress} (stage now: ${pending2.stage})`);
5945
5981
  result.details.push({ tokenId, stage: pending2.stage, status: progress });
5946
5982
  if (progress === "resolved") result.resolved++;
5947
5983
  else if (progress === "failed") result.failed++;
5948
5984
  else result.stillPending++;
5949
5985
  }
5950
5986
  }
5951
- if (result.resolved > 0 || result.failed > 0) {
5987
+ if (result.resolved > 0 || result.failed > 0 || result.stillPending > 0) {
5988
+ console.log(`[V5-RESOLVE] Saving: resolved=${result.resolved} failed=${result.failed} stillPending=${result.stillPending}`);
5952
5989
  await this.save();
5953
5990
  }
5954
5991
  return result;
5955
5992
  }
5993
+ /**
5994
+ * Start a periodic interval that retries resolveUnconfirmed() until all
5995
+ * tokens are confirmed or failed. Stops automatically when nothing is
5996
+ * pending and is cleaned up by destroy().
5997
+ */
5998
+ scheduleResolveUnconfirmed() {
5999
+ if (this.resolveUnconfirmedTimer) return;
6000
+ const hasUnconfirmed = Array.from(this.tokens.values()).some(
6001
+ (t) => t.status === "submitted"
6002
+ );
6003
+ if (!hasUnconfirmed) {
6004
+ console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: no submitted tokens, not starting timer`);
6005
+ return;
6006
+ }
6007
+ console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: starting periodic retry (every ${_PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS}ms)`);
6008
+ this.resolveUnconfirmedTimer = setInterval(async () => {
6009
+ try {
6010
+ const result = await this.resolveUnconfirmed();
6011
+ if (result.stillPending === 0) {
6012
+ console.log(`[V5-RESOLVE] All tokens resolved, stopping periodic retry`);
6013
+ this.stopResolveUnconfirmedPolling();
6014
+ }
6015
+ } catch (err) {
6016
+ console.log(`[V5-RESOLVE] Periodic retry error:`, err);
6017
+ }
6018
+ }, _PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS);
6019
+ }
6020
+ stopResolveUnconfirmedPolling() {
6021
+ if (this.resolveUnconfirmedTimer) {
6022
+ clearInterval(this.resolveUnconfirmedTimer);
6023
+ this.resolveUnconfirmedTimer = null;
6024
+ }
6025
+ }
5956
6026
  // ===========================================================================
5957
6027
  // Private - V5 Lazy Resolution Helpers
5958
6028
  // ===========================================================================
@@ -5965,10 +6035,12 @@ var PaymentsModule = class _PaymentsModule {
5965
6035
  pending2.lastAttemptAt = Date.now();
5966
6036
  try {
5967
6037
  if (pending2.stage === "RECEIVED") {
6038
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: RECEIVED \u2192 submitting mint commitment...`);
5968
6039
  const mintDataJson = JSON.parse(bundle.recipientMintData);
5969
6040
  const mintData = await MintTransactionData3.fromJSON(mintDataJson);
5970
6041
  const mintCommitment = await MintCommitment3.create(mintData);
5971
6042
  const mintResponse = await stClient.submitMintCommitment(mintCommitment);
6043
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint response status=${mintResponse.status}`);
5972
6044
  if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
5973
6045
  throw new Error(`Mint submission failed: ${mintResponse.status}`);
5974
6046
  }
@@ -5976,22 +6048,27 @@ var PaymentsModule = class _PaymentsModule {
5976
6048
  this.updatePendingFinalization(token, pending2);
5977
6049
  }
5978
6050
  if (pending2.stage === "MINT_SUBMITTED") {
6051
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_SUBMITTED \u2192 checking mint proof...`);
5979
6052
  const mintDataJson = JSON.parse(bundle.recipientMintData);
5980
6053
  const mintData = await MintTransactionData3.fromJSON(mintDataJson);
5981
6054
  const mintCommitment = await MintCommitment3.create(mintData);
5982
6055
  const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
5983
6056
  if (!proof) {
6057
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof not yet available, staying MINT_SUBMITTED`);
5984
6058
  this.updatePendingFinalization(token, pending2);
5985
6059
  return "pending";
5986
6060
  }
6061
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof obtained!`);
5987
6062
  pending2.mintProofJson = JSON.stringify(proof);
5988
6063
  pending2.stage = "MINT_PROVEN";
5989
6064
  this.updatePendingFinalization(token, pending2);
5990
6065
  }
5991
6066
  if (pending2.stage === "MINT_PROVEN") {
6067
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_PROVEN \u2192 submitting transfer commitment...`);
5992
6068
  const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
5993
6069
  const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
5994
6070
  const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
6071
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer response status=${transferResponse.status}`);
5995
6072
  if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
5996
6073
  throw new Error(`Transfer submission failed: ${transferResponse.status}`);
5997
6074
  }
@@ -5999,13 +6076,16 @@ var PaymentsModule = class _PaymentsModule {
5999
6076
  this.updatePendingFinalization(token, pending2);
6000
6077
  }
6001
6078
  if (pending2.stage === "TRANSFER_SUBMITTED") {
6079
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: TRANSFER_SUBMITTED \u2192 checking transfer proof...`);
6002
6080
  const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
6003
6081
  const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
6004
6082
  const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
6005
6083
  if (!proof) {
6084
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof not yet available, staying TRANSFER_SUBMITTED`);
6006
6085
  this.updatePendingFinalization(token, pending2);
6007
6086
  return "pending";
6008
6087
  }
6088
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof obtained! Finalizing...`);
6009
6089
  const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
6010
6090
  const confirmedToken = {
6011
6091
  id: token.id,
@@ -6021,6 +6101,12 @@ var PaymentsModule = class _PaymentsModule {
6021
6101
  sdkData: JSON.stringify(finalizedToken.toJSON())
6022
6102
  };
6023
6103
  this.tokens.set(tokenId, confirmedToken);
6104
+ this.deps.emitEvent("transfer:confirmed", {
6105
+ id: crypto.randomUUID(),
6106
+ status: "completed",
6107
+ tokens: [confirmedToken],
6108
+ tokenTransfers: []
6109
+ });
6024
6110
  this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
6025
6111
  return "resolved";
6026
6112
  }
@@ -6162,11 +6248,20 @@ var PaymentsModule = class _PaymentsModule {
6162
6248
  }
6163
6249
  }
6164
6250
  if (pendingTokens.length > 0) {
6251
+ const json = JSON.stringify(pendingTokens);
6252
+ this.log(`[V5-PERSIST] Saving ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")} (${json.length} bytes)`);
6165
6253
  await this.deps.storage.set(
6166
6254
  STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
6167
- JSON.stringify(pendingTokens)
6255
+ json
6168
6256
  );
6257
+ const verify = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
6258
+ if (!verify) {
6259
+ console.error("[Payments][V5-PERSIST] CRITICAL: KV write succeeded but read-back is empty!");
6260
+ } else {
6261
+ this.log(`[V5-PERSIST] Verified: read-back ${verify.length} bytes`);
6262
+ }
6169
6263
  } else {
6264
+ this.log(`[V5-PERSIST] No pending V5 tokens to save (total tokens: ${this.tokens.size}), clearing KV`);
6170
6265
  await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
6171
6266
  }
6172
6267
  }
@@ -6176,16 +6271,47 @@ var PaymentsModule = class _PaymentsModule {
6176
6271
  */
6177
6272
  async loadPendingV5Tokens() {
6178
6273
  const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
6274
+ this.log(`[V5-PERSIST] loadPendingV5Tokens: KV data = ${data ? `${data.length} bytes` : "null/empty"}`);
6179
6275
  if (!data) return;
6180
6276
  try {
6181
6277
  const pendingTokens = JSON.parse(data);
6278
+ this.log(`[V5-PERSIST] Parsed ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")}`);
6182
6279
  for (const token of pendingTokens) {
6183
6280
  if (!this.tokens.has(token.id)) {
6184
6281
  this.tokens.set(token.id, token);
6282
+ this.log(`[V5-PERSIST] Restored token ${token.id.slice(0, 16)} (status=${token.status})`);
6283
+ } else {
6284
+ this.log(`[V5-PERSIST] Token ${token.id.slice(0, 16)} already in map, skipping`);
6185
6285
  }
6186
6286
  }
6187
- if (pendingTokens.length > 0) {
6188
- this.log(`Restored ${pendingTokens.length} pending V5 token(s)`);
6287
+ } catch (err) {
6288
+ console.error("[Payments][V5-PERSIST] Failed to parse pending V5 tokens:", err);
6289
+ }
6290
+ }
6291
+ /**
6292
+ * Persist the set of processed splitGroupIds to KV storage.
6293
+ * This ensures Nostr re-deliveries are ignored across page reloads,
6294
+ * even when the confirmed token's in-memory ID differs from v5split_{id}.
6295
+ */
6296
+ async saveProcessedSplitGroupIds() {
6297
+ const ids = Array.from(this.processedSplitGroupIds);
6298
+ if (ids.length > 0) {
6299
+ await this.deps.storage.set(
6300
+ STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS,
6301
+ JSON.stringify(ids)
6302
+ );
6303
+ }
6304
+ }
6305
+ /**
6306
+ * Load processed splitGroupIds from KV storage.
6307
+ */
6308
+ async loadProcessedSplitGroupIds() {
6309
+ const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS);
6310
+ if (!data) return;
6311
+ try {
6312
+ const ids = JSON.parse(data);
6313
+ for (const id of ids) {
6314
+ this.processedSplitGroupIds.add(id);
6189
6315
  }
6190
6316
  } catch {
6191
6317
  }
@@ -6840,7 +6966,32 @@ var PaymentsModule = class _PaymentsModule {
6840
6966
  try {
6841
6967
  const result = await provider.sync(localData);
6842
6968
  if (result.success && result.merged) {
6969
+ const savedTokens = new Map(this.tokens);
6843
6970
  this.loadFromStorageData(result.merged);
6971
+ let restoredCount = 0;
6972
+ for (const [tokenId, token] of savedTokens) {
6973
+ if (this.tokens.has(tokenId)) continue;
6974
+ const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
6975
+ const stateHash = extractStateHashFromSdkData(token.sdkData);
6976
+ if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
6977
+ continue;
6978
+ }
6979
+ if (sdkTokenId) {
6980
+ let hasEquivalent = false;
6981
+ for (const existing of this.tokens.values()) {
6982
+ if (extractTokenIdFromSdkData(existing.sdkData) === sdkTokenId) {
6983
+ hasEquivalent = true;
6984
+ break;
6985
+ }
6986
+ }
6987
+ if (hasEquivalent) continue;
6988
+ }
6989
+ this.tokens.set(tokenId, token);
6990
+ restoredCount++;
6991
+ }
6992
+ if (restoredCount > 0) {
6993
+ console.log(`[Payments] Sync: restored ${restoredCount} token(s) lost by loadFromStorageData`);
6994
+ }
6844
6995
  if (this.nametags.length === 0 && savedNametags.length > 0) {
6845
6996
  this.nametags = savedNametags;
6846
6997
  }
@@ -7174,8 +7325,9 @@ var PaymentsModule = class _PaymentsModule {
7174
7325
  return;
7175
7326
  }
7176
7327
  this.tokens.set(token.id, token);
7328
+ console.log(`[Payments][DEBUG] NOSTR-FIRST: saving token id=${token.id.slice(0, 16)} status=${token.status} sdkData.length=${token.sdkData?.length}`);
7177
7329
  await this.save();
7178
- this.log(`NOSTR-FIRST: Token ${token.id.slice(0, 8)}... added as submitted (unconfirmed)`);
7330
+ console.log(`[Payments][DEBUG] NOSTR-FIRST: save() completed, tokens.size=${this.tokens.size}`);
7179
7331
  const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
7180
7332
  const incomingTransfer = {
7181
7333
  id: transfer.id,
@@ -7332,8 +7484,12 @@ var PaymentsModule = class _PaymentsModule {
7332
7484
  }
7333
7485
  }
7334
7486
  async handleIncomingTransfer(transfer) {
7487
+ if (!this.loaded && this.loadedPromise) {
7488
+ await this.loadedPromise;
7489
+ }
7335
7490
  try {
7336
7491
  const payload = transfer.payload;
7492
+ console.log("[Payments][DEBUG] handleIncomingTransfer: keys=", Object.keys(payload).join(","));
7337
7493
  let instantBundle = null;
7338
7494
  if (isInstantSplitBundle(payload)) {
7339
7495
  instantBundle = payload;
@@ -7365,7 +7521,7 @@ var PaymentsModule = class _PaymentsModule {
7365
7521
  return;
7366
7522
  }
7367
7523
  if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
7368
- this.log("Processing NOSTR-FIRST commitment-only transfer...");
7524
+ console.log("[Payments][DEBUG] >>> NOSTR-FIRST commitment-only transfer detected");
7369
7525
  await this.handleCommitmentOnlyTransfer(transfer, payload);
7370
7526
  return;
7371
7527
  }
@@ -7528,17 +7684,24 @@ var PaymentsModule = class _PaymentsModule {
7528
7684
  // ===========================================================================
7529
7685
  async save() {
7530
7686
  const providers = this.getTokenStorageProviders();
7531
- if (providers.size === 0) {
7532
- this.log("No token storage providers - tokens not persisted");
7533
- return;
7534
- }
7535
- const data = await this.createStorageData();
7536
- for (const [id, provider] of providers) {
7537
- try {
7538
- await provider.save(data);
7539
- } catch (err) {
7540
- console.error(`[Payments] Failed to save to provider ${id}:`, err);
7687
+ const tokenStats = Array.from(this.tokens.values()).map((t) => {
7688
+ const txf = tokenToTxf(t);
7689
+ return `${t.id.slice(0, 12)}(${t.status},txf=${!!txf})`;
7690
+ });
7691
+ console.log(`[Payments][DEBUG] save(): providers=${providers.size}, tokens=[${tokenStats.join(", ")}]`);
7692
+ if (providers.size > 0) {
7693
+ const data = await this.createStorageData();
7694
+ const dataKeys = Object.keys(data).filter((k) => k.startsWith("token-"));
7695
+ console.log(`[Payments][DEBUG] save(): TXF keys=${dataKeys.length} (${dataKeys.join(", ")})`);
7696
+ for (const [id, provider] of providers) {
7697
+ try {
7698
+ await provider.save(data);
7699
+ } catch (err) {
7700
+ console.error(`[Payments] Failed to save to provider ${id}:`, err);
7701
+ }
7541
7702
  }
7703
+ } else {
7704
+ console.log("[Payments][DEBUG] save(): No token storage providers - TXF not persisted");
7542
7705
  }
7543
7706
  await this.savePendingV5Tokens();
7544
7707
  }
@@ -7574,6 +7737,7 @@ var PaymentsModule = class _PaymentsModule {
7574
7737
  }
7575
7738
  loadFromStorageData(data) {
7576
7739
  const parsed = parseTxfStorageData(data);
7740
+ console.log(`[Payments][DEBUG] loadFromStorageData: parsed ${parsed.tokens.length} tokens, ${parsed.tombstones.length} tombstones, errors=[${parsed.validationErrors.join("; ")}]`);
7577
7741
  this.tombstones = parsed.tombstones;
7578
7742
  this.tokens.clear();
7579
7743
  for (const token of parsed.tokens) {