@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
@@ -103,7 +103,9 @@ var init_constants = __esm({
103
103
  /** Group chat: members for this address */
104
104
  GROUP_CHAT_MEMBERS: "group_chat_members",
105
105
  /** Group chat: processed event IDs for deduplication */
106
- GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events"
106
+ GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
107
+ /** Processed V5 split group IDs for Nostr re-delivery dedup */
108
+ PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids"
107
109
  };
108
110
  STORAGE_KEYS = {
109
111
  ...STORAGE_KEYS_GLOBAL,
@@ -3513,7 +3515,7 @@ var InstantSplitExecutor = class {
3513
3515
  token: JSON.stringify(bundle),
3514
3516
  proof: null,
3515
3517
  // Proof is included in the bundle
3516
- memo: "INSTANT_SPLIT_V5",
3518
+ memo: options?.memo,
3517
3519
  sender: {
3518
3520
  transportPubkey: senderPubkey
3519
3521
  }
@@ -4072,6 +4074,11 @@ var import_MintCommitment3 = require("@unicitylabs/state-transition-sdk/lib/tran
4072
4074
  var import_MintTransactionData3 = require("@unicitylabs/state-transition-sdk/lib/transaction/MintTransactionData");
4073
4075
  var import_InclusionProofUtils5 = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
4074
4076
  var import_InclusionProof = require("@unicitylabs/state-transition-sdk/lib/transaction/InclusionProof");
4077
+ function computeHistoryDedupKey(type, tokenId, transferId) {
4078
+ if (type === "SENT" && transferId) return `${type}_transfer_${transferId}`;
4079
+ if (tokenId) return `${type}_${tokenId}`;
4080
+ return `${type}_${crypto.randomUUID()}`;
4081
+ }
4075
4082
  function enrichWithRegistry(info) {
4076
4083
  const registry = TokenRegistry.getInstance();
4077
4084
  const def = registry.getDefinition(info.coinId);
@@ -4370,7 +4377,7 @@ var PaymentsModule = class _PaymentsModule {
4370
4377
  tombstones = [];
4371
4378
  archivedTokens = /* @__PURE__ */ new Map();
4372
4379
  forkedTokens = /* @__PURE__ */ new Map();
4373
- transactionHistory = [];
4380
+ _historyCache = [];
4374
4381
  nametags = [];
4375
4382
  // Payment Requests State (Incoming)
4376
4383
  paymentRequests = [];
@@ -4390,6 +4397,17 @@ var PaymentsModule = class _PaymentsModule {
4390
4397
  // Poll every 2s
4391
4398
  static PROOF_POLLING_MAX_ATTEMPTS = 30;
4392
4399
  // Max 30 attempts (~60s)
4400
+ // Periodic retry for resolveUnconfirmed (V5 lazy finalization)
4401
+ resolveUnconfirmedTimer = null;
4402
+ static RESOLVE_UNCONFIRMED_INTERVAL_MS = 1e4;
4403
+ // Retry every 10s
4404
+ // Guard: ensure load() completes before processing incoming bundles
4405
+ loadedPromise = null;
4406
+ loaded = false;
4407
+ // Persistent dedup: tracks splitGroupIds that have been fully processed.
4408
+ // Survives page reloads via KV storage so Nostr re-deliveries are ignored
4409
+ // even when the confirmed token's in-memory ID differs from v5split_{id}.
4410
+ processedSplitGroupIds = /* @__PURE__ */ new Set();
4393
4411
  // Storage event subscriptions (push-based sync)
4394
4412
  storageEventUnsubscribers = [];
4395
4413
  syncDebounceTimer = null;
@@ -4439,7 +4457,7 @@ var PaymentsModule = class _PaymentsModule {
4439
4457
  this.tombstones = [];
4440
4458
  this.archivedTokens.clear();
4441
4459
  this.forkedTokens.clear();
4442
- this.transactionHistory = [];
4460
+ this._historyCache = [];
4443
4461
  this.nametags = [];
4444
4462
  this.deps = deps;
4445
4463
  this.priceProvider = deps.price ?? null;
@@ -4475,38 +4493,40 @@ var PaymentsModule = class _PaymentsModule {
4475
4493
  */
4476
4494
  async load() {
4477
4495
  this.ensureInitialized();
4478
- await TokenRegistry.waitForReady();
4479
- const providers = this.getTokenStorageProviders();
4480
- for (const [id, provider] of providers) {
4481
- try {
4482
- const result = await provider.load();
4483
- if (result.success && result.data) {
4484
- this.loadFromStorageData(result.data);
4485
- this.log(`Loaded metadata from provider ${id}`);
4486
- break;
4496
+ const doLoad = async () => {
4497
+ await TokenRegistry.waitForReady();
4498
+ const providers = this.getTokenStorageProviders();
4499
+ for (const [id, provider] of providers) {
4500
+ try {
4501
+ const result = await provider.load();
4502
+ if (result.success && result.data) {
4503
+ this.loadFromStorageData(result.data);
4504
+ this.log(`Loaded metadata from provider ${id}`);
4505
+ break;
4506
+ }
4507
+ } catch (err) {
4508
+ console.error(`[Payments] Failed to load from provider ${id}:`, err);
4487
4509
  }
4488
- } catch (err) {
4489
- console.error(`[Payments] Failed to load from provider ${id}:`, err);
4490
- }
4491
- }
4492
- await this.loadPendingV5Tokens();
4493
- const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
4494
- if (historyData) {
4495
- try {
4496
- this.transactionHistory = JSON.parse(historyData);
4497
- } catch {
4498
- this.transactionHistory = [];
4499
4510
  }
4500
- }
4501
- const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
4502
- if (pending2) {
4503
- const transfers = JSON.parse(pending2);
4504
- for (const transfer of transfers) {
4505
- this.pendingTransfers.set(transfer.id, transfer);
4511
+ const loadedTokens = Array.from(this.tokens.values()).map((t) => `${t.id.slice(0, 12)}(${t.status})`);
4512
+ console.log(`[Payments][DEBUG] load(): from TXF providers: ${this.tokens.size} tokens [${loadedTokens.join(", ")}]`);
4513
+ await this.loadPendingV5Tokens();
4514
+ await this.loadProcessedSplitGroupIds();
4515
+ await this.loadHistory();
4516
+ const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
4517
+ if (pending2) {
4518
+ const transfers = JSON.parse(pending2);
4519
+ for (const transfer of transfers) {
4520
+ this.pendingTransfers.set(transfer.id, transfer);
4521
+ }
4506
4522
  }
4507
- }
4523
+ this.loaded = true;
4524
+ };
4525
+ this.loadedPromise = doLoad();
4526
+ await this.loadedPromise;
4508
4527
  this.resolveUnconfirmed().catch(() => {
4509
4528
  });
4529
+ this.scheduleResolveUnconfirmed();
4510
4530
  }
4511
4531
  /**
4512
4532
  * Cleanup all subscriptions, polling jobs, and pending resolvers.
@@ -4525,6 +4545,7 @@ var PaymentsModule = class _PaymentsModule {
4525
4545
  this.paymentRequestResponseHandlers.clear();
4526
4546
  this.stopProofPolling();
4527
4547
  this.proofPollingJobs.clear();
4548
+ this.stopResolveUnconfirmedPolling();
4528
4549
  for (const [, resolver] of this.pendingResponseResolvers) {
4529
4550
  clearTimeout(resolver.timeout);
4530
4551
  resolver.reject(new Error("Module destroyed"));
@@ -4584,7 +4605,7 @@ var PaymentsModule = class _PaymentsModule {
4584
4605
  }
4585
4606
  await this.saveToOutbox(result, recipientPubkey);
4586
4607
  result.status = "submitted";
4587
- const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
4608
+ const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
4588
4609
  const transferMode = request.transferMode ?? "instant";
4589
4610
  if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
4590
4611
  if (transferMode === "conservative") {
@@ -4615,7 +4636,7 @@ var PaymentsModule = class _PaymentsModule {
4615
4636
  updatedAt: Date.now(),
4616
4637
  sdkData: JSON.stringify(changeTokenData)
4617
4638
  };
4618
- await this.addToken(changeUiToken, true);
4639
+ await this.addToken(changeUiToken);
4619
4640
  this.log(`Conservative split: change token saved: ${changeUiToken.id}`);
4620
4641
  await this.deps.transport.sendTokenTransfer(recipientPubkey, {
4621
4642
  sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
@@ -4624,7 +4645,7 @@ var PaymentsModule = class _PaymentsModule {
4624
4645
  });
4625
4646
  const splitCommitmentRequestId = splitResult.recipientTransferTx?.data?.requestId ?? splitResult.recipientTransferTx?.requestId;
4626
4647
  const splitRequestIdHex = splitCommitmentRequestId instanceof Uint8Array ? Array.from(splitCommitmentRequestId).map((b) => b.toString(16).padStart(2, "0")).join("") : splitCommitmentRequestId ? String(splitCommitmentRequestId) : void 0;
4627
- await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag, true);
4648
+ await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
4628
4649
  result.tokenTransfers.push({
4629
4650
  sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
4630
4651
  method: "split",
@@ -4649,6 +4670,7 @@ var PaymentsModule = class _PaymentsModule {
4649
4670
  this.deps.transport,
4650
4671
  recipientPubkey,
4651
4672
  {
4673
+ memo: request.memo,
4652
4674
  onChangeTokenCreated: async (changeToken) => {
4653
4675
  const changeTokenData = changeToken.toJSON();
4654
4676
  const uiToken = {
@@ -4664,7 +4686,7 @@ var PaymentsModule = class _PaymentsModule {
4664
4686
  updatedAt: Date.now(),
4665
4687
  sdkData: JSON.stringify(changeTokenData)
4666
4688
  };
4667
- await this.addToken(uiToken, true);
4689
+ await this.addToken(uiToken);
4668
4690
  this.log(`Change token saved via background: ${uiToken.id}`);
4669
4691
  },
4670
4692
  onStorageSync: async () => {
@@ -4679,7 +4701,7 @@ var PaymentsModule = class _PaymentsModule {
4679
4701
  if (instantResult.backgroundPromise) {
4680
4702
  this.pendingBackgroundTasks.push(instantResult.backgroundPromise);
4681
4703
  }
4682
- await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag);
4704
+ await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
4683
4705
  result.tokenTransfers.push({
4684
4706
  sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
4685
4707
  method: "split",
@@ -4726,20 +4748,25 @@ var PaymentsModule = class _PaymentsModule {
4726
4748
  requestIdHex
4727
4749
  });
4728
4750
  this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
4729
- await this.removeToken(token.id, recipientNametag, true);
4751
+ await this.removeToken(token.id);
4730
4752
  }
4731
4753
  result.status = "delivered";
4732
4754
  await this.save();
4733
4755
  await this.removeFromOutbox(result.id);
4734
4756
  result.status = "completed";
4757
+ const sentTokenId = result.tokens[0] ? extractTokenIdFromSdkData(result.tokens[0].sdkData) : void 0;
4735
4758
  await this.addToHistory({
4736
4759
  type: "SENT",
4737
4760
  amount: request.amount,
4738
4761
  coinId: request.coinId,
4739
4762
  symbol: this.getCoinSymbol(request.coinId),
4740
4763
  timestamp: Date.now(),
4764
+ recipientPubkey,
4741
4765
  recipientNametag,
4742
- transferId: result.id
4766
+ recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
4767
+ memo: request.memo,
4768
+ transferId: result.id,
4769
+ tokenId: sentTokenId || void 0
4743
4770
  });
4744
4771
  this.deps.emitEvent("transfer:confirmed", result);
4745
4772
  return result;
@@ -4850,6 +4877,7 @@ var PaymentsModule = class _PaymentsModule {
4850
4877
  recipientPubkey,
4851
4878
  {
4852
4879
  ...options,
4880
+ memo: request.memo,
4853
4881
  onChangeTokenCreated: async (changeToken) => {
4854
4882
  const changeTokenData = changeToken.toJSON();
4855
4883
  const uiToken = {
@@ -4865,7 +4893,7 @@ var PaymentsModule = class _PaymentsModule {
4865
4893
  updatedAt: Date.now(),
4866
4894
  sdkData: JSON.stringify(changeTokenData)
4867
4895
  };
4868
- await this.addToken(uiToken, true);
4896
+ await this.addToken(uiToken);
4869
4897
  this.log(`Change token saved via background: ${uiToken.id}`);
4870
4898
  },
4871
4899
  onStorageSync: async () => {
@@ -4878,15 +4906,20 @@ var PaymentsModule = class _PaymentsModule {
4878
4906
  if (result.backgroundPromise) {
4879
4907
  this.pendingBackgroundTasks.push(result.backgroundPromise);
4880
4908
  }
4881
- const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
4882
- await this.removeToken(tokenToSplit.id, recipientNametag, true);
4909
+ await this.removeToken(tokenToSplit.id);
4910
+ const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
4911
+ const splitTokenId = extractTokenIdFromSdkData(tokenToSplit.sdkData);
4883
4912
  await this.addToHistory({
4884
4913
  type: "SENT",
4885
4914
  amount: request.amount,
4886
4915
  coinId: request.coinId,
4887
4916
  symbol: this.getCoinSymbol(request.coinId),
4888
4917
  timestamp: Date.now(),
4889
- recipientNametag
4918
+ recipientPubkey,
4919
+ recipientNametag,
4920
+ recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
4921
+ memo: request.memo,
4922
+ tokenId: splitTokenId || void 0
4890
4923
  });
4891
4924
  await this.save();
4892
4925
  } else {
@@ -4917,15 +4950,18 @@ var PaymentsModule = class _PaymentsModule {
4917
4950
  * @param senderPubkey - Sender's public key for verification
4918
4951
  * @returns Processing result with finalized token
4919
4952
  */
4920
- async processInstantSplitBundle(bundle, senderPubkey) {
4953
+ async processInstantSplitBundle(bundle, senderPubkey, memo) {
4921
4954
  this.ensureInitialized();
4955
+ if (!this.loaded && this.loadedPromise) {
4956
+ await this.loadedPromise;
4957
+ }
4922
4958
  if (!isInstantSplitBundleV5(bundle)) {
4923
- return this.processInstantSplitBundleSync(bundle, senderPubkey);
4959
+ return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
4924
4960
  }
4925
4961
  try {
4926
4962
  const deterministicId = `v5split_${bundle.splitGroupId}`;
4927
- if (this.tokens.has(deterministicId)) {
4928
- this.log(`V5 bundle ${deterministicId.slice(0, 16)}... already exists, skipping duplicate`);
4963
+ if (this.tokens.has(deterministicId) || this.processedSplitGroupIds.has(bundle.splitGroupId)) {
4964
+ console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
4929
4965
  return { success: true, durationMs: 0 };
4930
4966
  }
4931
4967
  const registry = TokenRegistry.getInstance();
@@ -4950,17 +4986,33 @@ var PaymentsModule = class _PaymentsModule {
4950
4986
  updatedAt: Date.now(),
4951
4987
  sdkData: JSON.stringify({ _pendingFinalization: pendingData })
4952
4988
  };
4953
- await this.addToken(uiToken, false);
4954
- this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
4989
+ await this.addToken(uiToken);
4990
+ this.processedSplitGroupIds.add(bundle.splitGroupId);
4991
+ await this.saveProcessedSplitGroupIds();
4992
+ const senderInfo = await this.resolveSenderInfo(senderPubkey);
4993
+ await this.addToHistory({
4994
+ type: "RECEIVED",
4995
+ amount: bundle.amount,
4996
+ coinId: bundle.coinId,
4997
+ symbol: uiToken.symbol,
4998
+ timestamp: Date.now(),
4999
+ senderPubkey,
5000
+ ...senderInfo,
5001
+ memo,
5002
+ tokenId: deterministicId
5003
+ });
4955
5004
  this.deps.emitEvent("transfer:incoming", {
4956
5005
  id: bundle.splitGroupId,
4957
5006
  senderPubkey,
5007
+ senderNametag: senderInfo.senderNametag,
4958
5008
  tokens: [uiToken],
5009
+ memo,
4959
5010
  receivedAt: Date.now()
4960
5011
  });
4961
5012
  await this.save();
4962
5013
  this.resolveUnconfirmed().catch(() => {
4963
5014
  });
5015
+ this.scheduleResolveUnconfirmed();
4964
5016
  return { success: true, durationMs: 0 };
4965
5017
  } catch (error) {
4966
5018
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -4975,7 +5027,7 @@ var PaymentsModule = class _PaymentsModule {
4975
5027
  * Synchronous V4 bundle processing (dev mode only).
4976
5028
  * Kept for backward compatibility with V4 bundles.
4977
5029
  */
4978
- async processInstantSplitBundleSync(bundle, senderPubkey) {
5030
+ async processInstantSplitBundleSync(bundle, senderPubkey, memo) {
4979
5031
  try {
4980
5032
  const signingService = await this.createSigningService();
4981
5033
  const stClient = this.deps.oracle.getStateTransitionClient?.();
@@ -5035,19 +5087,26 @@ var PaymentsModule = class _PaymentsModule {
5035
5087
  sdkData: JSON.stringify(tokenData)
5036
5088
  };
5037
5089
  await this.addToken(uiToken);
5090
+ const receivedTokenId = extractTokenIdFromSdkData(uiToken.sdkData);
5091
+ const senderInfo = await this.resolveSenderInfo(senderPubkey);
5038
5092
  await this.addToHistory({
5039
5093
  type: "RECEIVED",
5040
5094
  amount: bundle.amount,
5041
5095
  coinId: info.coinId,
5042
5096
  symbol: info.symbol,
5043
5097
  timestamp: Date.now(),
5044
- senderPubkey
5098
+ senderPubkey,
5099
+ ...senderInfo,
5100
+ memo,
5101
+ tokenId: receivedTokenId || uiToken.id
5045
5102
  });
5046
5103
  await this.save();
5047
5104
  this.deps.emitEvent("transfer:incoming", {
5048
5105
  id: bundle.splitGroupId,
5049
5106
  senderPubkey,
5107
+ senderNametag: senderInfo.senderNametag,
5050
5108
  tokens: [uiToken],
5109
+ memo,
5051
5110
  receivedAt: Date.now()
5052
5111
  });
5053
5112
  }
@@ -5700,28 +5759,70 @@ var PaymentsModule = class _PaymentsModule {
5700
5759
  };
5701
5760
  const stClient = this.deps.oracle.getStateTransitionClient?.();
5702
5761
  const trustBase = this.deps.oracle.getTrustBase?.();
5703
- if (!stClient || !trustBase) return result;
5762
+ if (!stClient || !trustBase) {
5763
+ console.log(`[V5-RESOLVE] resolveUnconfirmed: EARLY EXIT \u2014 stClient=${!!stClient} trustBase=${!!trustBase}`);
5764
+ return result;
5765
+ }
5704
5766
  const signingService = await this.createSigningService();
5767
+ const submittedCount = Array.from(this.tokens.values()).filter((t) => t.status === "submitted").length;
5768
+ console.log(`[V5-RESOLVE] resolveUnconfirmed: ${submittedCount} submitted token(s) to process`);
5705
5769
  for (const [tokenId, token] of this.tokens) {
5706
5770
  if (token.status !== "submitted") continue;
5707
5771
  const pending2 = this.parsePendingFinalization(token.sdkData);
5708
5772
  if (!pending2) {
5773
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 16)}: no pending finalization metadata, skipping`);
5709
5774
  result.stillPending++;
5710
5775
  continue;
5711
5776
  }
5712
5777
  if (pending2.type === "v5_bundle") {
5778
+ console.log(`[V5-RESOLVE] Processing ${tokenId.slice(0, 16)}... stage=${pending2.stage} attempt=${pending2.attemptCount}`);
5713
5779
  const progress = await this.resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService);
5780
+ console.log(`[V5-RESOLVE] Result for ${tokenId.slice(0, 16)}...: ${progress} (stage now: ${pending2.stage})`);
5714
5781
  result.details.push({ tokenId, stage: pending2.stage, status: progress });
5715
5782
  if (progress === "resolved") result.resolved++;
5716
5783
  else if (progress === "failed") result.failed++;
5717
5784
  else result.stillPending++;
5718
5785
  }
5719
5786
  }
5720
- if (result.resolved > 0 || result.failed > 0) {
5787
+ if (result.resolved > 0 || result.failed > 0 || result.stillPending > 0) {
5788
+ console.log(`[V5-RESOLVE] Saving: resolved=${result.resolved} failed=${result.failed} stillPending=${result.stillPending}`);
5721
5789
  await this.save();
5722
5790
  }
5723
5791
  return result;
5724
5792
  }
5793
+ /**
5794
+ * Start a periodic interval that retries resolveUnconfirmed() until all
5795
+ * tokens are confirmed or failed. Stops automatically when nothing is
5796
+ * pending and is cleaned up by destroy().
5797
+ */
5798
+ scheduleResolveUnconfirmed() {
5799
+ if (this.resolveUnconfirmedTimer) return;
5800
+ const hasUnconfirmed = Array.from(this.tokens.values()).some(
5801
+ (t) => t.status === "submitted"
5802
+ );
5803
+ if (!hasUnconfirmed) {
5804
+ console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: no submitted tokens, not starting timer`);
5805
+ return;
5806
+ }
5807
+ console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: starting periodic retry (every ${_PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS}ms)`);
5808
+ this.resolveUnconfirmedTimer = setInterval(async () => {
5809
+ try {
5810
+ const result = await this.resolveUnconfirmed();
5811
+ if (result.stillPending === 0) {
5812
+ console.log(`[V5-RESOLVE] All tokens resolved, stopping periodic retry`);
5813
+ this.stopResolveUnconfirmedPolling();
5814
+ }
5815
+ } catch (err) {
5816
+ console.log(`[V5-RESOLVE] Periodic retry error:`, err);
5817
+ }
5818
+ }, _PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS);
5819
+ }
5820
+ stopResolveUnconfirmedPolling() {
5821
+ if (this.resolveUnconfirmedTimer) {
5822
+ clearInterval(this.resolveUnconfirmedTimer);
5823
+ this.resolveUnconfirmedTimer = null;
5824
+ }
5825
+ }
5725
5826
  // ===========================================================================
5726
5827
  // Private - V5 Lazy Resolution Helpers
5727
5828
  // ===========================================================================
@@ -5734,10 +5835,12 @@ var PaymentsModule = class _PaymentsModule {
5734
5835
  pending2.lastAttemptAt = Date.now();
5735
5836
  try {
5736
5837
  if (pending2.stage === "RECEIVED") {
5838
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: RECEIVED \u2192 submitting mint commitment...`);
5737
5839
  const mintDataJson = JSON.parse(bundle.recipientMintData);
5738
5840
  const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
5739
5841
  const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
5740
5842
  const mintResponse = await stClient.submitMintCommitment(mintCommitment);
5843
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint response status=${mintResponse.status}`);
5741
5844
  if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
5742
5845
  throw new Error(`Mint submission failed: ${mintResponse.status}`);
5743
5846
  }
@@ -5745,22 +5848,27 @@ var PaymentsModule = class _PaymentsModule {
5745
5848
  this.updatePendingFinalization(token, pending2);
5746
5849
  }
5747
5850
  if (pending2.stage === "MINT_SUBMITTED") {
5851
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_SUBMITTED \u2192 checking mint proof...`);
5748
5852
  const mintDataJson = JSON.parse(bundle.recipientMintData);
5749
5853
  const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
5750
5854
  const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
5751
5855
  const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
5752
5856
  if (!proof) {
5857
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof not yet available, staying MINT_SUBMITTED`);
5753
5858
  this.updatePendingFinalization(token, pending2);
5754
5859
  return "pending";
5755
5860
  }
5861
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof obtained!`);
5756
5862
  pending2.mintProofJson = JSON.stringify(proof);
5757
5863
  pending2.stage = "MINT_PROVEN";
5758
5864
  this.updatePendingFinalization(token, pending2);
5759
5865
  }
5760
5866
  if (pending2.stage === "MINT_PROVEN") {
5867
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_PROVEN \u2192 submitting transfer commitment...`);
5761
5868
  const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
5762
5869
  const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
5763
5870
  const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
5871
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer response status=${transferResponse.status}`);
5764
5872
  if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
5765
5873
  throw new Error(`Transfer submission failed: ${transferResponse.status}`);
5766
5874
  }
@@ -5768,13 +5876,16 @@ var PaymentsModule = class _PaymentsModule {
5768
5876
  this.updatePendingFinalization(token, pending2);
5769
5877
  }
5770
5878
  if (pending2.stage === "TRANSFER_SUBMITTED") {
5879
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: TRANSFER_SUBMITTED \u2192 checking transfer proof...`);
5771
5880
  const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
5772
5881
  const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
5773
5882
  const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
5774
5883
  if (!proof) {
5884
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof not yet available, staying TRANSFER_SUBMITTED`);
5775
5885
  this.updatePendingFinalization(token, pending2);
5776
5886
  return "pending";
5777
5887
  }
5888
+ console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof obtained! Finalizing...`);
5778
5889
  const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
5779
5890
  const confirmedToken = {
5780
5891
  id: token.id,
@@ -5790,13 +5901,11 @@ var PaymentsModule = class _PaymentsModule {
5790
5901
  sdkData: JSON.stringify(finalizedToken.toJSON())
5791
5902
  };
5792
5903
  this.tokens.set(tokenId, confirmedToken);
5793
- await this.addToHistory({
5794
- type: "RECEIVED",
5795
- amount: confirmedToken.amount,
5796
- coinId: confirmedToken.coinId,
5797
- symbol: confirmedToken.symbol || "UNK",
5798
- timestamp: Date.now(),
5799
- senderPubkey: pending2.senderPubkey
5904
+ this.deps.emitEvent("transfer:confirmed", {
5905
+ id: crypto.randomUUID(),
5906
+ status: "completed",
5907
+ tokens: [confirmedToken],
5908
+ tokenTransfers: []
5800
5909
  });
5801
5910
  this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
5802
5911
  return "resolved";
@@ -5939,11 +6048,20 @@ var PaymentsModule = class _PaymentsModule {
5939
6048
  }
5940
6049
  }
5941
6050
  if (pendingTokens.length > 0) {
6051
+ const json = JSON.stringify(pendingTokens);
6052
+ this.log(`[V5-PERSIST] Saving ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")} (${json.length} bytes)`);
5942
6053
  await this.deps.storage.set(
5943
6054
  STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
5944
- JSON.stringify(pendingTokens)
6055
+ json
5945
6056
  );
6057
+ const verify = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
6058
+ if (!verify) {
6059
+ console.error("[Payments][V5-PERSIST] CRITICAL: KV write succeeded but read-back is empty!");
6060
+ } else {
6061
+ this.log(`[V5-PERSIST] Verified: read-back ${verify.length} bytes`);
6062
+ }
5946
6063
  } else {
6064
+ this.log(`[V5-PERSIST] No pending V5 tokens to save (total tokens: ${this.tokens.size}), clearing KV`);
5947
6065
  await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
5948
6066
  }
5949
6067
  }
@@ -5953,16 +6071,47 @@ var PaymentsModule = class _PaymentsModule {
5953
6071
  */
5954
6072
  async loadPendingV5Tokens() {
5955
6073
  const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
6074
+ this.log(`[V5-PERSIST] loadPendingV5Tokens: KV data = ${data ? `${data.length} bytes` : "null/empty"}`);
5956
6075
  if (!data) return;
5957
6076
  try {
5958
6077
  const pendingTokens = JSON.parse(data);
6078
+ this.log(`[V5-PERSIST] Parsed ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")}`);
5959
6079
  for (const token of pendingTokens) {
5960
6080
  if (!this.tokens.has(token.id)) {
5961
6081
  this.tokens.set(token.id, token);
6082
+ this.log(`[V5-PERSIST] Restored token ${token.id.slice(0, 16)} (status=${token.status})`);
6083
+ } else {
6084
+ this.log(`[V5-PERSIST] Token ${token.id.slice(0, 16)} already in map, skipping`);
5962
6085
  }
5963
6086
  }
5964
- if (pendingTokens.length > 0) {
5965
- this.log(`Restored ${pendingTokens.length} pending V5 token(s)`);
6087
+ } catch (err) {
6088
+ console.error("[Payments][V5-PERSIST] Failed to parse pending V5 tokens:", err);
6089
+ }
6090
+ }
6091
+ /**
6092
+ * Persist the set of processed splitGroupIds to KV storage.
6093
+ * This ensures Nostr re-deliveries are ignored across page reloads,
6094
+ * even when the confirmed token's in-memory ID differs from v5split_{id}.
6095
+ */
6096
+ async saveProcessedSplitGroupIds() {
6097
+ const ids = Array.from(this.processedSplitGroupIds);
6098
+ if (ids.length > 0) {
6099
+ await this.deps.storage.set(
6100
+ STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS,
6101
+ JSON.stringify(ids)
6102
+ );
6103
+ }
6104
+ }
6105
+ /**
6106
+ * Load processed splitGroupIds from KV storage.
6107
+ */
6108
+ async loadProcessedSplitGroupIds() {
6109
+ const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS);
6110
+ if (!data) return;
6111
+ try {
6112
+ const ids = JSON.parse(data);
6113
+ for (const id of ids) {
6114
+ this.processedSplitGroupIds.add(id);
5966
6115
  }
5967
6116
  } catch {
5968
6117
  }
@@ -5981,10 +6130,9 @@ var PaymentsModule = class _PaymentsModule {
5981
6130
  * the old state is archived and replaced with the incoming one.
5982
6131
  *
5983
6132
  * @param token - The token to add.
5984
- * @param skipHistory - When `true`, do not create a `RECEIVED` transaction history entry (default `false`).
5985
6133
  * @returns `true` if the token was added, `false` if rejected as duplicate or tombstoned.
5986
6134
  */
5987
- async addToken(token, skipHistory = false) {
6135
+ async addToken(token) {
5988
6136
  this.ensureInitialized();
5989
6137
  const incomingTokenId = extractTokenIdFromSdkData(token.sdkData);
5990
6138
  const incomingStateHash = extractStateHashFromSdkData(token.sdkData);
@@ -6030,15 +6178,6 @@ var PaymentsModule = class _PaymentsModule {
6030
6178
  }
6031
6179
  this.tokens.set(token.id, token);
6032
6180
  await this.archiveToken(token);
6033
- if (!skipHistory && token.coinId && token.amount) {
6034
- await this.addToHistory({
6035
- type: "RECEIVED",
6036
- amount: token.amount,
6037
- coinId: token.coinId,
6038
- symbol: token.symbol || "UNK",
6039
- timestamp: token.createdAt || Date.now()
6040
- });
6041
- }
6042
6181
  await this.save();
6043
6182
  this.log(`Added token ${token.id}, total: ${this.tokens.size}`);
6044
6183
  return true;
@@ -6065,7 +6204,7 @@ var PaymentsModule = class _PaymentsModule {
6065
6204
  }
6066
6205
  }
6067
6206
  if (!found) {
6068
- await this.addToken(token, true);
6207
+ await this.addToken(token);
6069
6208
  return;
6070
6209
  }
6071
6210
  await this.archiveToken(token);
@@ -6080,10 +6219,8 @@ var PaymentsModule = class _PaymentsModule {
6080
6219
  * entry is created unless `skipHistory` is `true`.
6081
6220
  *
6082
6221
  * @param tokenId - Local UUID of the token to remove.
6083
- * @param recipientNametag - Optional nametag of the transfer recipient (for history).
6084
- * @param skipHistory - When `true`, skip creating a transaction history entry (default `false`).
6085
6222
  */
6086
- async removeToken(tokenId, recipientNametag, skipHistory = false) {
6223
+ async removeToken(tokenId) {
6087
6224
  this.ensureInitialized();
6088
6225
  const token = this.tokens.get(tokenId);
6089
6226
  if (!token) return;
@@ -6101,16 +6238,6 @@ var PaymentsModule = class _PaymentsModule {
6101
6238
  this.log(`Warning: Could not create tombstone for token ${tokenId.slice(0, 8)}... (missing tokenId or stateHash)`);
6102
6239
  }
6103
6240
  this.tokens.delete(tokenId);
6104
- if (!skipHistory && token.coinId && token.amount) {
6105
- await this.addToHistory({
6106
- type: "SENT",
6107
- amount: token.amount,
6108
- coinId: token.coinId,
6109
- symbol: token.symbol || "UNK",
6110
- timestamp: Date.now(),
6111
- recipientNametag
6112
- });
6113
- }
6114
6241
  await this.save();
6115
6242
  }
6116
6243
  // ===========================================================================
@@ -6335,26 +6462,104 @@ var PaymentsModule = class _PaymentsModule {
6335
6462
  * @returns Array of {@link TransactionHistoryEntry} objects in descending timestamp order.
6336
6463
  */
6337
6464
  getHistory() {
6338
- return [...this.transactionHistory].sort((a, b) => b.timestamp - a.timestamp);
6465
+ return [...this._historyCache].sort((a, b) => b.timestamp - a.timestamp);
6466
+ }
6467
+ /**
6468
+ * Best-effort resolve sender's DIRECT address and nametag from their transport pubkey.
6469
+ * Returns empty object if transport doesn't support resolution or lookup fails.
6470
+ */
6471
+ async resolveSenderInfo(senderTransportPubkey) {
6472
+ try {
6473
+ if (this.deps?.transport?.resolveTransportPubkeyInfo) {
6474
+ const peerInfo = await this.deps.transport.resolveTransportPubkeyInfo(senderTransportPubkey);
6475
+ if (peerInfo) {
6476
+ return {
6477
+ senderAddress: peerInfo.directAddress || void 0,
6478
+ senderNametag: peerInfo.nametag || void 0
6479
+ };
6480
+ }
6481
+ }
6482
+ } catch {
6483
+ }
6484
+ return {};
6339
6485
  }
6340
6486
  /**
6341
6487
  * Append an entry to the transaction history.
6342
6488
  *
6343
- * A unique `id` is auto-generated. The entry is immediately persisted to storage.
6489
+ * A unique `id` and `dedupKey` are auto-generated. The entry is persisted to
6490
+ * the local token storage provider's `history` store (IndexedDB / file).
6491
+ * Duplicate entries with the same `dedupKey` are silently ignored (upsert).
6344
6492
  *
6345
- * @param entry - History entry fields (without `id`).
6493
+ * @param entry - History entry fields (without `id` and `dedupKey`).
6346
6494
  */
6347
6495
  async addToHistory(entry) {
6348
6496
  this.ensureInitialized();
6497
+ const dedupKey = computeHistoryDedupKey(entry.type, entry.tokenId, entry.transferId);
6349
6498
  const historyEntry = {
6350
6499
  id: crypto.randomUUID(),
6500
+ dedupKey,
6351
6501
  ...entry
6352
6502
  };
6353
- this.transactionHistory.push(historyEntry);
6354
- await this.deps.storage.set(
6355
- STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY,
6356
- JSON.stringify(this.transactionHistory)
6357
- );
6503
+ const provider = this.getLocalTokenStorageProvider();
6504
+ if (provider?.addHistoryEntry) {
6505
+ await provider.addHistoryEntry(historyEntry);
6506
+ }
6507
+ const existingIdx = this._historyCache.findIndex((e) => e.dedupKey === dedupKey);
6508
+ if (existingIdx >= 0) {
6509
+ this._historyCache[existingIdx] = historyEntry;
6510
+ } else {
6511
+ this._historyCache.push(historyEntry);
6512
+ }
6513
+ this.deps.emitEvent("history:updated", historyEntry);
6514
+ }
6515
+ /**
6516
+ * Load history from the local token storage provider into the in-memory cache.
6517
+ * Also performs one-time migration from legacy KV storage.
6518
+ */
6519
+ async loadHistory() {
6520
+ const provider = this.getLocalTokenStorageProvider();
6521
+ if (provider?.getHistoryEntries) {
6522
+ this._historyCache = await provider.getHistoryEntries();
6523
+ const legacyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
6524
+ if (legacyData) {
6525
+ try {
6526
+ const legacyEntries = JSON.parse(legacyData);
6527
+ const records = legacyEntries.map((e) => ({
6528
+ ...e,
6529
+ dedupKey: e.dedupKey || computeHistoryDedupKey(e.type, e.tokenId, e.transferId)
6530
+ }));
6531
+ const imported = await provider.importHistoryEntries?.(records) ?? 0;
6532
+ if (imported > 0) {
6533
+ this._historyCache = await provider.getHistoryEntries();
6534
+ this.log(`Migrated ${imported} history entries from KV to history store`);
6535
+ }
6536
+ await this.deps.storage.remove(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
6537
+ } catch {
6538
+ }
6539
+ }
6540
+ } else {
6541
+ const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
6542
+ if (historyData) {
6543
+ try {
6544
+ this._historyCache = JSON.parse(historyData);
6545
+ } catch {
6546
+ this._historyCache = [];
6547
+ }
6548
+ }
6549
+ }
6550
+ }
6551
+ /**
6552
+ * Get the first local token storage provider (for history operations).
6553
+ */
6554
+ getLocalTokenStorageProvider() {
6555
+ const providers = this.getTokenStorageProviders();
6556
+ for (const [, provider] of providers) {
6557
+ if (provider.type === "local") return provider;
6558
+ }
6559
+ for (const [, provider] of providers) {
6560
+ return provider;
6561
+ }
6562
+ return null;
6358
6563
  }
6359
6564
  // ===========================================================================
6360
6565
  // Public API - Nametag
@@ -6561,7 +6766,32 @@ var PaymentsModule = class _PaymentsModule {
6561
6766
  try {
6562
6767
  const result = await provider.sync(localData);
6563
6768
  if (result.success && result.merged) {
6769
+ const savedTokens = new Map(this.tokens);
6564
6770
  this.loadFromStorageData(result.merged);
6771
+ let restoredCount = 0;
6772
+ for (const [tokenId, token] of savedTokens) {
6773
+ if (this.tokens.has(tokenId)) continue;
6774
+ const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
6775
+ const stateHash = extractStateHashFromSdkData(token.sdkData);
6776
+ if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
6777
+ continue;
6778
+ }
6779
+ if (sdkTokenId) {
6780
+ let hasEquivalent = false;
6781
+ for (const existing of this.tokens.values()) {
6782
+ if (extractTokenIdFromSdkData(existing.sdkData) === sdkTokenId) {
6783
+ hasEquivalent = true;
6784
+ break;
6785
+ }
6786
+ }
6787
+ if (hasEquivalent) continue;
6788
+ }
6789
+ this.tokens.set(tokenId, token);
6790
+ restoredCount++;
6791
+ }
6792
+ if (restoredCount > 0) {
6793
+ console.log(`[Payments] Sync: restored ${restoredCount} token(s) lost by loadFromStorageData`);
6794
+ }
6565
6795
  if (this.nametags.length === 0 && savedNametags.length > 0) {
6566
6796
  this.nametags = savedNametags;
6567
6797
  }
@@ -6895,16 +7125,30 @@ var PaymentsModule = class _PaymentsModule {
6895
7125
  return;
6896
7126
  }
6897
7127
  this.tokens.set(token.id, token);
7128
+ console.log(`[Payments][DEBUG] NOSTR-FIRST: saving token id=${token.id.slice(0, 16)} status=${token.status} sdkData.length=${token.sdkData?.length}`);
6898
7129
  await this.save();
6899
- this.log(`NOSTR-FIRST: Token ${token.id.slice(0, 8)}... added as submitted (unconfirmed)`);
7130
+ console.log(`[Payments][DEBUG] NOSTR-FIRST: save() completed, tokens.size=${this.tokens.size}`);
7131
+ const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
6900
7132
  const incomingTransfer = {
6901
7133
  id: transfer.id,
6902
7134
  senderPubkey: transfer.senderTransportPubkey,
7135
+ senderNametag: senderInfo.senderNametag,
6903
7136
  tokens: [token],
6904
7137
  memo: payload.memo,
6905
7138
  receivedAt: transfer.timestamp
6906
7139
  };
6907
7140
  this.deps.emitEvent("transfer:incoming", incomingTransfer);
7141
+ await this.addToHistory({
7142
+ type: "RECEIVED",
7143
+ amount: token.amount,
7144
+ coinId: token.coinId,
7145
+ symbol: token.symbol,
7146
+ timestamp: Date.now(),
7147
+ senderPubkey: transfer.senderTransportPubkey,
7148
+ ...senderInfo,
7149
+ memo: payload.memo,
7150
+ tokenId: nostrTokenId || token.id
7151
+ });
6908
7152
  try {
6909
7153
  const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
6910
7154
  const requestIdBytes = commitment.requestId;
@@ -6922,7 +7166,7 @@ var PaymentsModule = class _PaymentsModule {
6922
7166
  attemptCount: 0,
6923
7167
  lastAttemptAt: 0,
6924
7168
  onProofReceived: async (tokenId) => {
6925
- await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, transfer.senderTransportPubkey);
7169
+ await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
6926
7170
  }
6927
7171
  });
6928
7172
  } catch (err) {
@@ -6981,7 +7225,7 @@ var PaymentsModule = class _PaymentsModule {
6981
7225
  /**
6982
7226
  * Finalize a received token after proof is available
6983
7227
  */
6984
- async finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, senderPubkey) {
7228
+ async finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput) {
6985
7229
  try {
6986
7230
  const token = this.tokens.get(tokenId);
6987
7231
  if (!token) {
@@ -7029,14 +7273,6 @@ var PaymentsModule = class _PaymentsModule {
7029
7273
  tokens: [finalizedToken],
7030
7274
  tokenTransfers: []
7031
7275
  });
7032
- await this.addToHistory({
7033
- type: "RECEIVED",
7034
- amount: finalizedToken.amount,
7035
- coinId: finalizedToken.coinId,
7036
- symbol: finalizedToken.symbol,
7037
- timestamp: Date.now(),
7038
- senderPubkey
7039
- });
7040
7276
  } catch (error) {
7041
7277
  console.error("[Payments] Failed to finalize received token:", error);
7042
7278
  const token = this.tokens.get(tokenId);
@@ -7048,8 +7284,12 @@ var PaymentsModule = class _PaymentsModule {
7048
7284
  }
7049
7285
  }
7050
7286
  async handleIncomingTransfer(transfer) {
7287
+ if (!this.loaded && this.loadedPromise) {
7288
+ await this.loadedPromise;
7289
+ }
7051
7290
  try {
7052
7291
  const payload = transfer.payload;
7292
+ console.log("[Payments][DEBUG] handleIncomingTransfer: keys=", Object.keys(payload).join(","));
7053
7293
  let instantBundle = null;
7054
7294
  if (isInstantSplitBundle(payload)) {
7055
7295
  instantBundle = payload;
@@ -7067,7 +7307,8 @@ var PaymentsModule = class _PaymentsModule {
7067
7307
  try {
7068
7308
  const result = await this.processInstantSplitBundle(
7069
7309
  instantBundle,
7070
- transfer.senderTransportPubkey
7310
+ transfer.senderTransportPubkey,
7311
+ payload.memo
7071
7312
  );
7072
7313
  if (result.success) {
7073
7314
  this.log("INSTANT_SPLIT processed successfully");
@@ -7080,7 +7321,7 @@ var PaymentsModule = class _PaymentsModule {
7080
7321
  return;
7081
7322
  }
7082
7323
  if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
7083
- this.log("Processing NOSTR-FIRST commitment-only transfer...");
7324
+ console.log("[Payments][DEBUG] >>> NOSTR-FIRST commitment-only transfer detected");
7084
7325
  await this.handleCommitmentOnlyTransfer(transfer, payload);
7085
7326
  return;
7086
7327
  }
@@ -7185,10 +7426,26 @@ var PaymentsModule = class _PaymentsModule {
7185
7426
  updatedAt: Date.now(),
7186
7427
  sdkData: typeof tokenData === "string" ? tokenData : JSON.stringify(tokenData)
7187
7428
  };
7188
- await this.addToken(token);
7429
+ const added = await this.addToken(token);
7430
+ const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
7431
+ if (added) {
7432
+ const incomingTokenId = extractTokenIdFromSdkData(token.sdkData);
7433
+ await this.addToHistory({
7434
+ type: "RECEIVED",
7435
+ amount: token.amount,
7436
+ coinId: token.coinId,
7437
+ symbol: token.symbol,
7438
+ timestamp: Date.now(),
7439
+ senderPubkey: transfer.senderTransportPubkey,
7440
+ ...senderInfo,
7441
+ memo: payload.memo,
7442
+ tokenId: incomingTokenId || token.id
7443
+ });
7444
+ }
7189
7445
  const incomingTransfer = {
7190
7446
  id: transfer.id,
7191
7447
  senderPubkey: transfer.senderTransportPubkey,
7448
+ senderNametag: senderInfo.senderNametag,
7192
7449
  tokens: [token],
7193
7450
  memo: payload.memo,
7194
7451
  receivedAt: transfer.timestamp
@@ -7227,17 +7484,24 @@ var PaymentsModule = class _PaymentsModule {
7227
7484
  // ===========================================================================
7228
7485
  async save() {
7229
7486
  const providers = this.getTokenStorageProviders();
7230
- if (providers.size === 0) {
7231
- this.log("No token storage providers - tokens not persisted");
7232
- return;
7233
- }
7234
- const data = await this.createStorageData();
7235
- for (const [id, provider] of providers) {
7236
- try {
7237
- await provider.save(data);
7238
- } catch (err) {
7239
- console.error(`[Payments] Failed to save to provider ${id}:`, err);
7487
+ const tokenStats = Array.from(this.tokens.values()).map((t) => {
7488
+ const txf = tokenToTxf(t);
7489
+ return `${t.id.slice(0, 12)}(${t.status},txf=${!!txf})`;
7490
+ });
7491
+ console.log(`[Payments][DEBUG] save(): providers=${providers.size}, tokens=[${tokenStats.join(", ")}]`);
7492
+ if (providers.size > 0) {
7493
+ const data = await this.createStorageData();
7494
+ const dataKeys = Object.keys(data).filter((k) => k.startsWith("token-"));
7495
+ console.log(`[Payments][DEBUG] save(): TXF keys=${dataKeys.length} (${dataKeys.join(", ")})`);
7496
+ for (const [id, provider] of providers) {
7497
+ try {
7498
+ await provider.save(data);
7499
+ } catch (err) {
7500
+ console.error(`[Payments] Failed to save to provider ${id}:`, err);
7501
+ }
7240
7502
  }
7503
+ } else {
7504
+ console.log("[Payments][DEBUG] save(): No token storage providers - TXF not persisted");
7241
7505
  }
7242
7506
  await this.savePendingV5Tokens();
7243
7507
  }
@@ -7273,6 +7537,7 @@ var PaymentsModule = class _PaymentsModule {
7273
7537
  }
7274
7538
  loadFromStorageData(data) {
7275
7539
  const parsed = parseTxfStorageData(data);
7540
+ console.log(`[Payments][DEBUG] loadFromStorageData: parsed ${parsed.tokens.length} tokens, ${parsed.tombstones.length} tombstones, errors=[${parsed.validationErrors.join("; ")}]`);
7276
7541
  this.tombstones = parsed.tombstones;
7277
7542
  this.tokens.clear();
7278
7543
  for (const token of parsed.tokens) {