@unicitylabs/sphere-sdk 0.4.9 → 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 (43) 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 +384 -119
  6. package/dist/core/index.cjs.map +1 -1
  7. package/dist/core/index.d.cts +244 -194
  8. package/dist/core/index.d.ts +244 -194
  9. package/dist/core/index.js +384 -119
  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 +67 -3
  16. package/dist/impl/browser/index.cjs.map +1 -1
  17. package/dist/impl/browser/index.js +67 -3
  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 +64 -5
  28. package/dist/impl/nodejs/index.cjs.map +1 -1
  29. package/dist/impl/nodejs/index.d.cts +668 -628
  30. package/dist/impl/nodejs/index.d.ts +668 -628
  31. package/dist/impl/nodejs/index.js +64 -5
  32. package/dist/impl/nodejs/index.js.map +1 -1
  33. package/dist/index.cjs +384 -119
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +248 -194
  36. package/dist/index.d.ts +248 -194
  37. package/dist/index.js +384 -119
  38. package/dist/index.js.map +1 -1
  39. package/dist/l1/index.cjs +3 -1
  40. package/dist/l1/index.cjs.map +1 -1
  41. package/dist/l1/index.js +3 -1
  42. package/dist/l1/index.js.map +1 -1
  43. package/package.json +1 -1
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,
@@ -3713,7 +3715,7 @@ var InstantSplitExecutor = class {
3713
3715
  token: JSON.stringify(bundle),
3714
3716
  proof: null,
3715
3717
  // Proof is included in the bundle
3716
- memo: "INSTANT_SPLIT_V5",
3718
+ memo: options?.memo,
3717
3719
  sender: {
3718
3720
  transportPubkey: senderPubkey
3719
3721
  }
@@ -4272,6 +4274,11 @@ import { MintCommitment as MintCommitment3 } from "@unicitylabs/state-transition
4272
4274
  import { MintTransactionData as MintTransactionData3 } from "@unicitylabs/state-transition-sdk/lib/transaction/MintTransactionData";
4273
4275
  import { waitInclusionProof as waitInclusionProof5 } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils";
4274
4276
  import { InclusionProof } from "@unicitylabs/state-transition-sdk/lib/transaction/InclusionProof";
4277
+ function computeHistoryDedupKey(type, tokenId, transferId) {
4278
+ if (type === "SENT" && transferId) return `${type}_transfer_${transferId}`;
4279
+ if (tokenId) return `${type}_${tokenId}`;
4280
+ return `${type}_${crypto.randomUUID()}`;
4281
+ }
4275
4282
  function enrichWithRegistry(info) {
4276
4283
  const registry = TokenRegistry.getInstance();
4277
4284
  const def = registry.getDefinition(info.coinId);
@@ -4570,7 +4577,7 @@ var PaymentsModule = class _PaymentsModule {
4570
4577
  tombstones = [];
4571
4578
  archivedTokens = /* @__PURE__ */ new Map();
4572
4579
  forkedTokens = /* @__PURE__ */ new Map();
4573
- transactionHistory = [];
4580
+ _historyCache = [];
4574
4581
  nametags = [];
4575
4582
  // Payment Requests State (Incoming)
4576
4583
  paymentRequests = [];
@@ -4590,6 +4597,17 @@ var PaymentsModule = class _PaymentsModule {
4590
4597
  // Poll every 2s
4591
4598
  static PROOF_POLLING_MAX_ATTEMPTS = 30;
4592
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();
4593
4611
  // Storage event subscriptions (push-based sync)
4594
4612
  storageEventUnsubscribers = [];
4595
4613
  syncDebounceTimer = null;
@@ -4639,7 +4657,7 @@ var PaymentsModule = class _PaymentsModule {
4639
4657
  this.tombstones = [];
4640
4658
  this.archivedTokens.clear();
4641
4659
  this.forkedTokens.clear();
4642
- this.transactionHistory = [];
4660
+ this._historyCache = [];
4643
4661
  this.nametags = [];
4644
4662
  this.deps = deps;
4645
4663
  this.priceProvider = deps.price ?? null;
@@ -4675,38 +4693,40 @@ var PaymentsModule = class _PaymentsModule {
4675
4693
  */
4676
4694
  async load() {
4677
4695
  this.ensureInitialized();
4678
- await TokenRegistry.waitForReady();
4679
- const providers = this.getTokenStorageProviders();
4680
- for (const [id, provider] of providers) {
4681
- try {
4682
- const result = await provider.load();
4683
- if (result.success && result.data) {
4684
- this.loadFromStorageData(result.data);
4685
- this.log(`Loaded metadata from provider ${id}`);
4686
- 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);
4687
4709
  }
4688
- } catch (err) {
4689
- console.error(`[Payments] Failed to load from provider ${id}:`, err);
4690
- }
4691
- }
4692
- await this.loadPendingV5Tokens();
4693
- const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
4694
- if (historyData) {
4695
- try {
4696
- this.transactionHistory = JSON.parse(historyData);
4697
- } catch {
4698
- this.transactionHistory = [];
4699
4710
  }
4700
- }
4701
- const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
4702
- if (pending2) {
4703
- const transfers = JSON.parse(pending2);
4704
- for (const transfer of transfers) {
4705
- 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
+ }
4706
4722
  }
4707
- }
4723
+ this.loaded = true;
4724
+ };
4725
+ this.loadedPromise = doLoad();
4726
+ await this.loadedPromise;
4708
4727
  this.resolveUnconfirmed().catch(() => {
4709
4728
  });
4729
+ this.scheduleResolveUnconfirmed();
4710
4730
  }
4711
4731
  /**
4712
4732
  * Cleanup all subscriptions, polling jobs, and pending resolvers.
@@ -4725,6 +4745,7 @@ var PaymentsModule = class _PaymentsModule {
4725
4745
  this.paymentRequestResponseHandlers.clear();
4726
4746
  this.stopProofPolling();
4727
4747
  this.proofPollingJobs.clear();
4748
+ this.stopResolveUnconfirmedPolling();
4728
4749
  for (const [, resolver] of this.pendingResponseResolvers) {
4729
4750
  clearTimeout(resolver.timeout);
4730
4751
  resolver.reject(new Error("Module destroyed"));
@@ -4784,7 +4805,7 @@ var PaymentsModule = class _PaymentsModule {
4784
4805
  }
4785
4806
  await this.saveToOutbox(result, recipientPubkey);
4786
4807
  result.status = "submitted";
4787
- const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
4808
+ const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
4788
4809
  const transferMode = request.transferMode ?? "instant";
4789
4810
  if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
4790
4811
  if (transferMode === "conservative") {
@@ -4815,7 +4836,7 @@ var PaymentsModule = class _PaymentsModule {
4815
4836
  updatedAt: Date.now(),
4816
4837
  sdkData: JSON.stringify(changeTokenData)
4817
4838
  };
4818
- await this.addToken(changeUiToken, true);
4839
+ await this.addToken(changeUiToken);
4819
4840
  this.log(`Conservative split: change token saved: ${changeUiToken.id}`);
4820
4841
  await this.deps.transport.sendTokenTransfer(recipientPubkey, {
4821
4842
  sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
@@ -4824,7 +4845,7 @@ var PaymentsModule = class _PaymentsModule {
4824
4845
  });
4825
4846
  const splitCommitmentRequestId = splitResult.recipientTransferTx?.data?.requestId ?? splitResult.recipientTransferTx?.requestId;
4826
4847
  const splitRequestIdHex = splitCommitmentRequestId instanceof Uint8Array ? Array.from(splitCommitmentRequestId).map((b) => b.toString(16).padStart(2, "0")).join("") : splitCommitmentRequestId ? String(splitCommitmentRequestId) : void 0;
4827
- await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag, true);
4848
+ await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
4828
4849
  result.tokenTransfers.push({
4829
4850
  sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
4830
4851
  method: "split",
@@ -4849,6 +4870,7 @@ var PaymentsModule = class _PaymentsModule {
4849
4870
  this.deps.transport,
4850
4871
  recipientPubkey,
4851
4872
  {
4873
+ memo: request.memo,
4852
4874
  onChangeTokenCreated: async (changeToken) => {
4853
4875
  const changeTokenData = changeToken.toJSON();
4854
4876
  const uiToken = {
@@ -4864,7 +4886,7 @@ var PaymentsModule = class _PaymentsModule {
4864
4886
  updatedAt: Date.now(),
4865
4887
  sdkData: JSON.stringify(changeTokenData)
4866
4888
  };
4867
- await this.addToken(uiToken, true);
4889
+ await this.addToken(uiToken);
4868
4890
  this.log(`Change token saved via background: ${uiToken.id}`);
4869
4891
  },
4870
4892
  onStorageSync: async () => {
@@ -4879,7 +4901,7 @@ var PaymentsModule = class _PaymentsModule {
4879
4901
  if (instantResult.backgroundPromise) {
4880
4902
  this.pendingBackgroundTasks.push(instantResult.backgroundPromise);
4881
4903
  }
4882
- await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag);
4904
+ await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
4883
4905
  result.tokenTransfers.push({
4884
4906
  sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
4885
4907
  method: "split",
@@ -4926,20 +4948,25 @@ var PaymentsModule = class _PaymentsModule {
4926
4948
  requestIdHex
4927
4949
  });
4928
4950
  this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
4929
- await this.removeToken(token.id, recipientNametag, true);
4951
+ await this.removeToken(token.id);
4930
4952
  }
4931
4953
  result.status = "delivered";
4932
4954
  await this.save();
4933
4955
  await this.removeFromOutbox(result.id);
4934
4956
  result.status = "completed";
4957
+ const sentTokenId = result.tokens[0] ? extractTokenIdFromSdkData(result.tokens[0].sdkData) : void 0;
4935
4958
  await this.addToHistory({
4936
4959
  type: "SENT",
4937
4960
  amount: request.amount,
4938
4961
  coinId: request.coinId,
4939
4962
  symbol: this.getCoinSymbol(request.coinId),
4940
4963
  timestamp: Date.now(),
4964
+ recipientPubkey,
4941
4965
  recipientNametag,
4942
- transferId: result.id
4966
+ recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
4967
+ memo: request.memo,
4968
+ transferId: result.id,
4969
+ tokenId: sentTokenId || void 0
4943
4970
  });
4944
4971
  this.deps.emitEvent("transfer:confirmed", result);
4945
4972
  return result;
@@ -5050,6 +5077,7 @@ var PaymentsModule = class _PaymentsModule {
5050
5077
  recipientPubkey,
5051
5078
  {
5052
5079
  ...options,
5080
+ memo: request.memo,
5053
5081
  onChangeTokenCreated: async (changeToken) => {
5054
5082
  const changeTokenData = changeToken.toJSON();
5055
5083
  const uiToken = {
@@ -5065,7 +5093,7 @@ var PaymentsModule = class _PaymentsModule {
5065
5093
  updatedAt: Date.now(),
5066
5094
  sdkData: JSON.stringify(changeTokenData)
5067
5095
  };
5068
- await this.addToken(uiToken, true);
5096
+ await this.addToken(uiToken);
5069
5097
  this.log(`Change token saved via background: ${uiToken.id}`);
5070
5098
  },
5071
5099
  onStorageSync: async () => {
@@ -5078,15 +5106,20 @@ var PaymentsModule = class _PaymentsModule {
5078
5106
  if (result.backgroundPromise) {
5079
5107
  this.pendingBackgroundTasks.push(result.backgroundPromise);
5080
5108
  }
5081
- const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
5082
- await this.removeToken(tokenToSplit.id, recipientNametag, true);
5109
+ await this.removeToken(tokenToSplit.id);
5110
+ const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
5111
+ const splitTokenId = extractTokenIdFromSdkData(tokenToSplit.sdkData);
5083
5112
  await this.addToHistory({
5084
5113
  type: "SENT",
5085
5114
  amount: request.amount,
5086
5115
  coinId: request.coinId,
5087
5116
  symbol: this.getCoinSymbol(request.coinId),
5088
5117
  timestamp: Date.now(),
5089
- recipientNametag
5118
+ recipientPubkey,
5119
+ recipientNametag,
5120
+ recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
5121
+ memo: request.memo,
5122
+ tokenId: splitTokenId || void 0
5090
5123
  });
5091
5124
  await this.save();
5092
5125
  } else {
@@ -5117,15 +5150,18 @@ var PaymentsModule = class _PaymentsModule {
5117
5150
  * @param senderPubkey - Sender's public key for verification
5118
5151
  * @returns Processing result with finalized token
5119
5152
  */
5120
- async processInstantSplitBundle(bundle, senderPubkey) {
5153
+ async processInstantSplitBundle(bundle, senderPubkey, memo) {
5121
5154
  this.ensureInitialized();
5155
+ if (!this.loaded && this.loadedPromise) {
5156
+ await this.loadedPromise;
5157
+ }
5122
5158
  if (!isInstantSplitBundleV5(bundle)) {
5123
- return this.processInstantSplitBundleSync(bundle, senderPubkey);
5159
+ return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
5124
5160
  }
5125
5161
  try {
5126
5162
  const deterministicId = `v5split_${bundle.splitGroupId}`;
5127
- if (this.tokens.has(deterministicId)) {
5128
- 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`);
5129
5165
  return { success: true, durationMs: 0 };
5130
5166
  }
5131
5167
  const registry = TokenRegistry.getInstance();
@@ -5150,17 +5186,33 @@ var PaymentsModule = class _PaymentsModule {
5150
5186
  updatedAt: Date.now(),
5151
5187
  sdkData: JSON.stringify({ _pendingFinalization: pendingData })
5152
5188
  };
5153
- await this.addToken(uiToken, false);
5154
- this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
5189
+ await this.addToken(uiToken);
5190
+ this.processedSplitGroupIds.add(bundle.splitGroupId);
5191
+ await this.saveProcessedSplitGroupIds();
5192
+ const senderInfo = await this.resolveSenderInfo(senderPubkey);
5193
+ await this.addToHistory({
5194
+ type: "RECEIVED",
5195
+ amount: bundle.amount,
5196
+ coinId: bundle.coinId,
5197
+ symbol: uiToken.symbol,
5198
+ timestamp: Date.now(),
5199
+ senderPubkey,
5200
+ ...senderInfo,
5201
+ memo,
5202
+ tokenId: deterministicId
5203
+ });
5155
5204
  this.deps.emitEvent("transfer:incoming", {
5156
5205
  id: bundle.splitGroupId,
5157
5206
  senderPubkey,
5207
+ senderNametag: senderInfo.senderNametag,
5158
5208
  tokens: [uiToken],
5209
+ memo,
5159
5210
  receivedAt: Date.now()
5160
5211
  });
5161
5212
  await this.save();
5162
5213
  this.resolveUnconfirmed().catch(() => {
5163
5214
  });
5215
+ this.scheduleResolveUnconfirmed();
5164
5216
  return { success: true, durationMs: 0 };
5165
5217
  } catch (error) {
5166
5218
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -5175,7 +5227,7 @@ var PaymentsModule = class _PaymentsModule {
5175
5227
  * Synchronous V4 bundle processing (dev mode only).
5176
5228
  * Kept for backward compatibility with V4 bundles.
5177
5229
  */
5178
- async processInstantSplitBundleSync(bundle, senderPubkey) {
5230
+ async processInstantSplitBundleSync(bundle, senderPubkey, memo) {
5179
5231
  try {
5180
5232
  const signingService = await this.createSigningService();
5181
5233
  const stClient = this.deps.oracle.getStateTransitionClient?.();
@@ -5235,19 +5287,26 @@ var PaymentsModule = class _PaymentsModule {
5235
5287
  sdkData: JSON.stringify(tokenData)
5236
5288
  };
5237
5289
  await this.addToken(uiToken);
5290
+ const receivedTokenId = extractTokenIdFromSdkData(uiToken.sdkData);
5291
+ const senderInfo = await this.resolveSenderInfo(senderPubkey);
5238
5292
  await this.addToHistory({
5239
5293
  type: "RECEIVED",
5240
5294
  amount: bundle.amount,
5241
5295
  coinId: info.coinId,
5242
5296
  symbol: info.symbol,
5243
5297
  timestamp: Date.now(),
5244
- senderPubkey
5298
+ senderPubkey,
5299
+ ...senderInfo,
5300
+ memo,
5301
+ tokenId: receivedTokenId || uiToken.id
5245
5302
  });
5246
5303
  await this.save();
5247
5304
  this.deps.emitEvent("transfer:incoming", {
5248
5305
  id: bundle.splitGroupId,
5249
5306
  senderPubkey,
5307
+ senderNametag: senderInfo.senderNametag,
5250
5308
  tokens: [uiToken],
5309
+ memo,
5251
5310
  receivedAt: Date.now()
5252
5311
  });
5253
5312
  }
@@ -5900,28 +5959,70 @@ var PaymentsModule = class _PaymentsModule {
5900
5959
  };
5901
5960
  const stClient = this.deps.oracle.getStateTransitionClient?.();
5902
5961
  const trustBase = this.deps.oracle.getTrustBase?.();
5903
- 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
+ }
5904
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`);
5905
5969
  for (const [tokenId, token] of this.tokens) {
5906
5970
  if (token.status !== "submitted") continue;
5907
5971
  const pending2 = this.parsePendingFinalization(token.sdkData);
5908
5972
  if (!pending2) {
5973
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 16)}: no pending finalization metadata, skipping`);
5909
5974
  result.stillPending++;
5910
5975
  continue;
5911
5976
  }
5912
5977
  if (pending2.type === "v5_bundle") {
5978
+ console.log(`[V5-RESOLVE] Processing ${tokenId.slice(0, 16)}... stage=${pending2.stage} attempt=${pending2.attemptCount}`);
5913
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})`);
5914
5981
  result.details.push({ tokenId, stage: pending2.stage, status: progress });
5915
5982
  if (progress === "resolved") result.resolved++;
5916
5983
  else if (progress === "failed") result.failed++;
5917
5984
  else result.stillPending++;
5918
5985
  }
5919
5986
  }
5920
- 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}`);
5921
5989
  await this.save();
5922
5990
  }
5923
5991
  return result;
5924
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
+ }
5925
6026
  // ===========================================================================
5926
6027
  // Private - V5 Lazy Resolution Helpers
5927
6028
  // ===========================================================================
@@ -5934,10 +6035,12 @@ var PaymentsModule = class _PaymentsModule {
5934
6035
  pending2.lastAttemptAt = Date.now();
5935
6036
  try {
5936
6037
  if (pending2.stage === "RECEIVED") {
6038
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: RECEIVED \u2192 submitting mint commitment...`);
5937
6039
  const mintDataJson = JSON.parse(bundle.recipientMintData);
5938
6040
  const mintData = await MintTransactionData3.fromJSON(mintDataJson);
5939
6041
  const mintCommitment = await MintCommitment3.create(mintData);
5940
6042
  const mintResponse = await stClient.submitMintCommitment(mintCommitment);
6043
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint response status=${mintResponse.status}`);
5941
6044
  if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
5942
6045
  throw new Error(`Mint submission failed: ${mintResponse.status}`);
5943
6046
  }
@@ -5945,22 +6048,27 @@ var PaymentsModule = class _PaymentsModule {
5945
6048
  this.updatePendingFinalization(token, pending2);
5946
6049
  }
5947
6050
  if (pending2.stage === "MINT_SUBMITTED") {
6051
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_SUBMITTED \u2192 checking mint proof...`);
5948
6052
  const mintDataJson = JSON.parse(bundle.recipientMintData);
5949
6053
  const mintData = await MintTransactionData3.fromJSON(mintDataJson);
5950
6054
  const mintCommitment = await MintCommitment3.create(mintData);
5951
6055
  const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
5952
6056
  if (!proof) {
6057
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof not yet available, staying MINT_SUBMITTED`);
5953
6058
  this.updatePendingFinalization(token, pending2);
5954
6059
  return "pending";
5955
6060
  }
6061
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof obtained!`);
5956
6062
  pending2.mintProofJson = JSON.stringify(proof);
5957
6063
  pending2.stage = "MINT_PROVEN";
5958
6064
  this.updatePendingFinalization(token, pending2);
5959
6065
  }
5960
6066
  if (pending2.stage === "MINT_PROVEN") {
6067
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_PROVEN \u2192 submitting transfer commitment...`);
5961
6068
  const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
5962
6069
  const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
5963
6070
  const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
6071
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer response status=${transferResponse.status}`);
5964
6072
  if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
5965
6073
  throw new Error(`Transfer submission failed: ${transferResponse.status}`);
5966
6074
  }
@@ -5968,13 +6076,16 @@ var PaymentsModule = class _PaymentsModule {
5968
6076
  this.updatePendingFinalization(token, pending2);
5969
6077
  }
5970
6078
  if (pending2.stage === "TRANSFER_SUBMITTED") {
6079
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: TRANSFER_SUBMITTED \u2192 checking transfer proof...`);
5971
6080
  const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
5972
6081
  const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
5973
6082
  const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
5974
6083
  if (!proof) {
6084
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof not yet available, staying TRANSFER_SUBMITTED`);
5975
6085
  this.updatePendingFinalization(token, pending2);
5976
6086
  return "pending";
5977
6087
  }
6088
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof obtained! Finalizing...`);
5978
6089
  const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
5979
6090
  const confirmedToken = {
5980
6091
  id: token.id,
@@ -5990,13 +6101,11 @@ var PaymentsModule = class _PaymentsModule {
5990
6101
  sdkData: JSON.stringify(finalizedToken.toJSON())
5991
6102
  };
5992
6103
  this.tokens.set(tokenId, confirmedToken);
5993
- await this.addToHistory({
5994
- type: "RECEIVED",
5995
- amount: confirmedToken.amount,
5996
- coinId: confirmedToken.coinId,
5997
- symbol: confirmedToken.symbol || "UNK",
5998
- timestamp: Date.now(),
5999
- senderPubkey: pending2.senderPubkey
6104
+ this.deps.emitEvent("transfer:confirmed", {
6105
+ id: crypto.randomUUID(),
6106
+ status: "completed",
6107
+ tokens: [confirmedToken],
6108
+ tokenTransfers: []
6000
6109
  });
6001
6110
  this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
6002
6111
  return "resolved";
@@ -6139,11 +6248,20 @@ var PaymentsModule = class _PaymentsModule {
6139
6248
  }
6140
6249
  }
6141
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)`);
6142
6253
  await this.deps.storage.set(
6143
6254
  STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
6144
- JSON.stringify(pendingTokens)
6255
+ json
6145
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
+ }
6146
6263
  } else {
6264
+ this.log(`[V5-PERSIST] No pending V5 tokens to save (total tokens: ${this.tokens.size}), clearing KV`);
6147
6265
  await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
6148
6266
  }
6149
6267
  }
@@ -6153,16 +6271,47 @@ var PaymentsModule = class _PaymentsModule {
6153
6271
  */
6154
6272
  async loadPendingV5Tokens() {
6155
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"}`);
6156
6275
  if (!data) return;
6157
6276
  try {
6158
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(", ")}`);
6159
6279
  for (const token of pendingTokens) {
6160
6280
  if (!this.tokens.has(token.id)) {
6161
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`);
6162
6285
  }
6163
6286
  }
6164
- if (pendingTokens.length > 0) {
6165
- 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);
6166
6315
  }
6167
6316
  } catch {
6168
6317
  }
@@ -6181,10 +6330,9 @@ var PaymentsModule = class _PaymentsModule {
6181
6330
  * the old state is archived and replaced with the incoming one.
6182
6331
  *
6183
6332
  * @param token - The token to add.
6184
- * @param skipHistory - When `true`, do not create a `RECEIVED` transaction history entry (default `false`).
6185
6333
  * @returns `true` if the token was added, `false` if rejected as duplicate or tombstoned.
6186
6334
  */
6187
- async addToken(token, skipHistory = false) {
6335
+ async addToken(token) {
6188
6336
  this.ensureInitialized();
6189
6337
  const incomingTokenId = extractTokenIdFromSdkData(token.sdkData);
6190
6338
  const incomingStateHash = extractStateHashFromSdkData(token.sdkData);
@@ -6230,15 +6378,6 @@ var PaymentsModule = class _PaymentsModule {
6230
6378
  }
6231
6379
  this.tokens.set(token.id, token);
6232
6380
  await this.archiveToken(token);
6233
- if (!skipHistory && token.coinId && token.amount) {
6234
- await this.addToHistory({
6235
- type: "RECEIVED",
6236
- amount: token.amount,
6237
- coinId: token.coinId,
6238
- symbol: token.symbol || "UNK",
6239
- timestamp: token.createdAt || Date.now()
6240
- });
6241
- }
6242
6381
  await this.save();
6243
6382
  this.log(`Added token ${token.id}, total: ${this.tokens.size}`);
6244
6383
  return true;
@@ -6265,7 +6404,7 @@ var PaymentsModule = class _PaymentsModule {
6265
6404
  }
6266
6405
  }
6267
6406
  if (!found) {
6268
- await this.addToken(token, true);
6407
+ await this.addToken(token);
6269
6408
  return;
6270
6409
  }
6271
6410
  await this.archiveToken(token);
@@ -6280,10 +6419,8 @@ var PaymentsModule = class _PaymentsModule {
6280
6419
  * entry is created unless `skipHistory` is `true`.
6281
6420
  *
6282
6421
  * @param tokenId - Local UUID of the token to remove.
6283
- * @param recipientNametag - Optional nametag of the transfer recipient (for history).
6284
- * @param skipHistory - When `true`, skip creating a transaction history entry (default `false`).
6285
6422
  */
6286
- async removeToken(tokenId, recipientNametag, skipHistory = false) {
6423
+ async removeToken(tokenId) {
6287
6424
  this.ensureInitialized();
6288
6425
  const token = this.tokens.get(tokenId);
6289
6426
  if (!token) return;
@@ -6301,16 +6438,6 @@ var PaymentsModule = class _PaymentsModule {
6301
6438
  this.log(`Warning: Could not create tombstone for token ${tokenId.slice(0, 8)}... (missing tokenId or stateHash)`);
6302
6439
  }
6303
6440
  this.tokens.delete(tokenId);
6304
- if (!skipHistory && token.coinId && token.amount) {
6305
- await this.addToHistory({
6306
- type: "SENT",
6307
- amount: token.amount,
6308
- coinId: token.coinId,
6309
- symbol: token.symbol || "UNK",
6310
- timestamp: Date.now(),
6311
- recipientNametag
6312
- });
6313
- }
6314
6441
  await this.save();
6315
6442
  }
6316
6443
  // ===========================================================================
@@ -6535,26 +6662,104 @@ var PaymentsModule = class _PaymentsModule {
6535
6662
  * @returns Array of {@link TransactionHistoryEntry} objects in descending timestamp order.
6536
6663
  */
6537
6664
  getHistory() {
6538
- return [...this.transactionHistory].sort((a, b) => b.timestamp - a.timestamp);
6665
+ return [...this._historyCache].sort((a, b) => b.timestamp - a.timestamp);
6666
+ }
6667
+ /**
6668
+ * Best-effort resolve sender's DIRECT address and nametag from their transport pubkey.
6669
+ * Returns empty object if transport doesn't support resolution or lookup fails.
6670
+ */
6671
+ async resolveSenderInfo(senderTransportPubkey) {
6672
+ try {
6673
+ if (this.deps?.transport?.resolveTransportPubkeyInfo) {
6674
+ const peerInfo = await this.deps.transport.resolveTransportPubkeyInfo(senderTransportPubkey);
6675
+ if (peerInfo) {
6676
+ return {
6677
+ senderAddress: peerInfo.directAddress || void 0,
6678
+ senderNametag: peerInfo.nametag || void 0
6679
+ };
6680
+ }
6681
+ }
6682
+ } catch {
6683
+ }
6684
+ return {};
6539
6685
  }
6540
6686
  /**
6541
6687
  * Append an entry to the transaction history.
6542
6688
  *
6543
- * A unique `id` is auto-generated. The entry is immediately persisted to storage.
6689
+ * A unique `id` and `dedupKey` are auto-generated. The entry is persisted to
6690
+ * the local token storage provider's `history` store (IndexedDB / file).
6691
+ * Duplicate entries with the same `dedupKey` are silently ignored (upsert).
6544
6692
  *
6545
- * @param entry - History entry fields (without `id`).
6693
+ * @param entry - History entry fields (without `id` and `dedupKey`).
6546
6694
  */
6547
6695
  async addToHistory(entry) {
6548
6696
  this.ensureInitialized();
6697
+ const dedupKey = computeHistoryDedupKey(entry.type, entry.tokenId, entry.transferId);
6549
6698
  const historyEntry = {
6550
6699
  id: crypto.randomUUID(),
6700
+ dedupKey,
6551
6701
  ...entry
6552
6702
  };
6553
- this.transactionHistory.push(historyEntry);
6554
- await this.deps.storage.set(
6555
- STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY,
6556
- JSON.stringify(this.transactionHistory)
6557
- );
6703
+ const provider = this.getLocalTokenStorageProvider();
6704
+ if (provider?.addHistoryEntry) {
6705
+ await provider.addHistoryEntry(historyEntry);
6706
+ }
6707
+ const existingIdx = this._historyCache.findIndex((e) => e.dedupKey === dedupKey);
6708
+ if (existingIdx >= 0) {
6709
+ this._historyCache[existingIdx] = historyEntry;
6710
+ } else {
6711
+ this._historyCache.push(historyEntry);
6712
+ }
6713
+ this.deps.emitEvent("history:updated", historyEntry);
6714
+ }
6715
+ /**
6716
+ * Load history from the local token storage provider into the in-memory cache.
6717
+ * Also performs one-time migration from legacy KV storage.
6718
+ */
6719
+ async loadHistory() {
6720
+ const provider = this.getLocalTokenStorageProvider();
6721
+ if (provider?.getHistoryEntries) {
6722
+ this._historyCache = await provider.getHistoryEntries();
6723
+ const legacyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
6724
+ if (legacyData) {
6725
+ try {
6726
+ const legacyEntries = JSON.parse(legacyData);
6727
+ const records = legacyEntries.map((e) => ({
6728
+ ...e,
6729
+ dedupKey: e.dedupKey || computeHistoryDedupKey(e.type, e.tokenId, e.transferId)
6730
+ }));
6731
+ const imported = await provider.importHistoryEntries?.(records) ?? 0;
6732
+ if (imported > 0) {
6733
+ this._historyCache = await provider.getHistoryEntries();
6734
+ this.log(`Migrated ${imported} history entries from KV to history store`);
6735
+ }
6736
+ await this.deps.storage.remove(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
6737
+ } catch {
6738
+ }
6739
+ }
6740
+ } else {
6741
+ const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
6742
+ if (historyData) {
6743
+ try {
6744
+ this._historyCache = JSON.parse(historyData);
6745
+ } catch {
6746
+ this._historyCache = [];
6747
+ }
6748
+ }
6749
+ }
6750
+ }
6751
+ /**
6752
+ * Get the first local token storage provider (for history operations).
6753
+ */
6754
+ getLocalTokenStorageProvider() {
6755
+ const providers = this.getTokenStorageProviders();
6756
+ for (const [, provider] of providers) {
6757
+ if (provider.type === "local") return provider;
6758
+ }
6759
+ for (const [, provider] of providers) {
6760
+ return provider;
6761
+ }
6762
+ return null;
6558
6763
  }
6559
6764
  // ===========================================================================
6560
6765
  // Public API - Nametag
@@ -6761,7 +6966,32 @@ var PaymentsModule = class _PaymentsModule {
6761
6966
  try {
6762
6967
  const result = await provider.sync(localData);
6763
6968
  if (result.success && result.merged) {
6969
+ const savedTokens = new Map(this.tokens);
6764
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
+ }
6765
6995
  if (this.nametags.length === 0 && savedNametags.length > 0) {
6766
6996
  this.nametags = savedNametags;
6767
6997
  }
@@ -7095,16 +7325,30 @@ var PaymentsModule = class _PaymentsModule {
7095
7325
  return;
7096
7326
  }
7097
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}`);
7098
7329
  await this.save();
7099
- 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}`);
7331
+ const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
7100
7332
  const incomingTransfer = {
7101
7333
  id: transfer.id,
7102
7334
  senderPubkey: transfer.senderTransportPubkey,
7335
+ senderNametag: senderInfo.senderNametag,
7103
7336
  tokens: [token],
7104
7337
  memo: payload.memo,
7105
7338
  receivedAt: transfer.timestamp
7106
7339
  };
7107
7340
  this.deps.emitEvent("transfer:incoming", incomingTransfer);
7341
+ await this.addToHistory({
7342
+ type: "RECEIVED",
7343
+ amount: token.amount,
7344
+ coinId: token.coinId,
7345
+ symbol: token.symbol,
7346
+ timestamp: Date.now(),
7347
+ senderPubkey: transfer.senderTransportPubkey,
7348
+ ...senderInfo,
7349
+ memo: payload.memo,
7350
+ tokenId: nostrTokenId || token.id
7351
+ });
7108
7352
  try {
7109
7353
  const commitment = await TransferCommitment4.fromJSON(commitmentInput);
7110
7354
  const requestIdBytes = commitment.requestId;
@@ -7122,7 +7366,7 @@ var PaymentsModule = class _PaymentsModule {
7122
7366
  attemptCount: 0,
7123
7367
  lastAttemptAt: 0,
7124
7368
  onProofReceived: async (tokenId) => {
7125
- await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, transfer.senderTransportPubkey);
7369
+ await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
7126
7370
  }
7127
7371
  });
7128
7372
  } catch (err) {
@@ -7181,7 +7425,7 @@ var PaymentsModule = class _PaymentsModule {
7181
7425
  /**
7182
7426
  * Finalize a received token after proof is available
7183
7427
  */
7184
- async finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, senderPubkey) {
7428
+ async finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput) {
7185
7429
  try {
7186
7430
  const token = this.tokens.get(tokenId);
7187
7431
  if (!token) {
@@ -7229,14 +7473,6 @@ var PaymentsModule = class _PaymentsModule {
7229
7473
  tokens: [finalizedToken],
7230
7474
  tokenTransfers: []
7231
7475
  });
7232
- await this.addToHistory({
7233
- type: "RECEIVED",
7234
- amount: finalizedToken.amount,
7235
- coinId: finalizedToken.coinId,
7236
- symbol: finalizedToken.symbol,
7237
- timestamp: Date.now(),
7238
- senderPubkey
7239
- });
7240
7476
  } catch (error) {
7241
7477
  console.error("[Payments] Failed to finalize received token:", error);
7242
7478
  const token = this.tokens.get(tokenId);
@@ -7248,8 +7484,12 @@ var PaymentsModule = class _PaymentsModule {
7248
7484
  }
7249
7485
  }
7250
7486
  async handleIncomingTransfer(transfer) {
7487
+ if (!this.loaded && this.loadedPromise) {
7488
+ await this.loadedPromise;
7489
+ }
7251
7490
  try {
7252
7491
  const payload = transfer.payload;
7492
+ console.log("[Payments][DEBUG] handleIncomingTransfer: keys=", Object.keys(payload).join(","));
7253
7493
  let instantBundle = null;
7254
7494
  if (isInstantSplitBundle(payload)) {
7255
7495
  instantBundle = payload;
@@ -7267,7 +7507,8 @@ var PaymentsModule = class _PaymentsModule {
7267
7507
  try {
7268
7508
  const result = await this.processInstantSplitBundle(
7269
7509
  instantBundle,
7270
- transfer.senderTransportPubkey
7510
+ transfer.senderTransportPubkey,
7511
+ payload.memo
7271
7512
  );
7272
7513
  if (result.success) {
7273
7514
  this.log("INSTANT_SPLIT processed successfully");
@@ -7280,7 +7521,7 @@ var PaymentsModule = class _PaymentsModule {
7280
7521
  return;
7281
7522
  }
7282
7523
  if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
7283
- this.log("Processing NOSTR-FIRST commitment-only transfer...");
7524
+ console.log("[Payments][DEBUG] >>> NOSTR-FIRST commitment-only transfer detected");
7284
7525
  await this.handleCommitmentOnlyTransfer(transfer, payload);
7285
7526
  return;
7286
7527
  }
@@ -7385,10 +7626,26 @@ var PaymentsModule = class _PaymentsModule {
7385
7626
  updatedAt: Date.now(),
7386
7627
  sdkData: typeof tokenData === "string" ? tokenData : JSON.stringify(tokenData)
7387
7628
  };
7388
- await this.addToken(token);
7629
+ const added = await this.addToken(token);
7630
+ const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
7631
+ if (added) {
7632
+ const incomingTokenId = extractTokenIdFromSdkData(token.sdkData);
7633
+ await this.addToHistory({
7634
+ type: "RECEIVED",
7635
+ amount: token.amount,
7636
+ coinId: token.coinId,
7637
+ symbol: token.symbol,
7638
+ timestamp: Date.now(),
7639
+ senderPubkey: transfer.senderTransportPubkey,
7640
+ ...senderInfo,
7641
+ memo: payload.memo,
7642
+ tokenId: incomingTokenId || token.id
7643
+ });
7644
+ }
7389
7645
  const incomingTransfer = {
7390
7646
  id: transfer.id,
7391
7647
  senderPubkey: transfer.senderTransportPubkey,
7648
+ senderNametag: senderInfo.senderNametag,
7392
7649
  tokens: [token],
7393
7650
  memo: payload.memo,
7394
7651
  receivedAt: transfer.timestamp
@@ -7427,17 +7684,24 @@ var PaymentsModule = class _PaymentsModule {
7427
7684
  // ===========================================================================
7428
7685
  async save() {
7429
7686
  const providers = this.getTokenStorageProviders();
7430
- if (providers.size === 0) {
7431
- this.log("No token storage providers - tokens not persisted");
7432
- return;
7433
- }
7434
- const data = await this.createStorageData();
7435
- for (const [id, provider] of providers) {
7436
- try {
7437
- await provider.save(data);
7438
- } catch (err) {
7439
- 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
+ }
7440
7702
  }
7703
+ } else {
7704
+ console.log("[Payments][DEBUG] save(): No token storage providers - TXF not persisted");
7441
7705
  }
7442
7706
  await this.savePendingV5Tokens();
7443
7707
  }
@@ -7473,6 +7737,7 @@ var PaymentsModule = class _PaymentsModule {
7473
7737
  }
7474
7738
  loadFromStorageData(data) {
7475
7739
  const parsed = parseTxfStorageData(data);
7740
+ console.log(`[Payments][DEBUG] loadFromStorageData: parsed ${parsed.tokens.length} tokens, ${parsed.tombstones.length} tombstones, errors=[${parsed.validationErrors.join("; ")}]`);
7476
7741
  this.tombstones = parsed.tombstones;
7477
7742
  this.tokens.clear();
7478
7743
  for (const token of parsed.tokens) {