@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.
- package/dist/connect/index.cjs +3 -1
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/index.js +3 -1
- package/dist/connect/index.js.map +1 -1
- package/dist/core/index.cjs +384 -119
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +244 -194
- package/dist/core/index.d.ts +244 -194
- package/dist/core/index.js +384 -119
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/connect/index.cjs +3 -1
- package/dist/impl/browser/connect/index.cjs.map +1 -1
- package/dist/impl/browser/connect/index.js +3 -1
- package/dist/impl/browser/connect/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +67 -3
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +67 -3
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/browser/ipfs.cjs +3 -1
- package/dist/impl/browser/ipfs.cjs.map +1 -1
- package/dist/impl/browser/ipfs.js +3 -1
- package/dist/impl/browser/ipfs.js.map +1 -1
- package/dist/impl/nodejs/connect/index.cjs +3 -1
- package/dist/impl/nodejs/connect/index.cjs.map +1 -1
- package/dist/impl/nodejs/connect/index.js +3 -1
- package/dist/impl/nodejs/connect/index.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +64 -5
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +668 -628
- package/dist/impl/nodejs/index.d.ts +668 -628
- package/dist/impl/nodejs/index.js +64 -5
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +384 -119
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +248 -194
- package/dist/index.d.ts +248 -194
- package/dist/index.js +384 -119
- package/dist/index.js.map +1 -1
- package/dist/l1/index.cjs +3 -1
- package/dist/l1/index.cjs.map +1 -1
- package/dist/l1/index.js +3 -1
- package/dist/l1/index.js.map +1 -1
- package/package.json +1 -1
package/dist/core/index.cjs
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
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
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4882
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4954
|
-
this.
|
|
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)
|
|
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
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5965
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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`
|
|
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.
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
7231
|
-
|
|
7232
|
-
return
|
|
7233
|
-
}
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
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) {
|