@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.js
CHANGED
|
@@ -87,7 +87,9 @@ var init_constants = __esm({
|
|
|
87
87
|
/** Group chat: members for this address */
|
|
88
88
|
GROUP_CHAT_MEMBERS: "group_chat_members",
|
|
89
89
|
/** Group chat: processed event IDs for deduplication */
|
|
90
|
-
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events"
|
|
90
|
+
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
|
|
91
|
+
/** Processed V5 split group IDs for Nostr re-delivery dedup */
|
|
92
|
+
PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids"
|
|
91
93
|
};
|
|
92
94
|
STORAGE_KEYS = {
|
|
93
95
|
...STORAGE_KEYS_GLOBAL,
|
|
@@ -3418,7 +3420,7 @@ var InstantSplitExecutor = class {
|
|
|
3418
3420
|
token: JSON.stringify(bundle),
|
|
3419
3421
|
proof: null,
|
|
3420
3422
|
// Proof is included in the bundle
|
|
3421
|
-
memo:
|
|
3423
|
+
memo: options?.memo,
|
|
3422
3424
|
sender: {
|
|
3423
3425
|
transportPubkey: senderPubkey
|
|
3424
3426
|
}
|
|
@@ -3977,6 +3979,11 @@ import { MintCommitment as MintCommitment3 } from "@unicitylabs/state-transition
|
|
|
3977
3979
|
import { MintTransactionData as MintTransactionData3 } from "@unicitylabs/state-transition-sdk/lib/transaction/MintTransactionData";
|
|
3978
3980
|
import { waitInclusionProof as waitInclusionProof5 } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils";
|
|
3979
3981
|
import { InclusionProof } from "@unicitylabs/state-transition-sdk/lib/transaction/InclusionProof";
|
|
3982
|
+
function computeHistoryDedupKey(type, tokenId, transferId) {
|
|
3983
|
+
if (type === "SENT" && transferId) return `${type}_transfer_${transferId}`;
|
|
3984
|
+
if (tokenId) return `${type}_${tokenId}`;
|
|
3985
|
+
return `${type}_${crypto.randomUUID()}`;
|
|
3986
|
+
}
|
|
3980
3987
|
function enrichWithRegistry(info) {
|
|
3981
3988
|
const registry = TokenRegistry.getInstance();
|
|
3982
3989
|
const def = registry.getDefinition(info.coinId);
|
|
@@ -4275,7 +4282,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4275
4282
|
tombstones = [];
|
|
4276
4283
|
archivedTokens = /* @__PURE__ */ new Map();
|
|
4277
4284
|
forkedTokens = /* @__PURE__ */ new Map();
|
|
4278
|
-
|
|
4285
|
+
_historyCache = [];
|
|
4279
4286
|
nametags = [];
|
|
4280
4287
|
// Payment Requests State (Incoming)
|
|
4281
4288
|
paymentRequests = [];
|
|
@@ -4295,6 +4302,17 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4295
4302
|
// Poll every 2s
|
|
4296
4303
|
static PROOF_POLLING_MAX_ATTEMPTS = 30;
|
|
4297
4304
|
// Max 30 attempts (~60s)
|
|
4305
|
+
// Periodic retry for resolveUnconfirmed (V5 lazy finalization)
|
|
4306
|
+
resolveUnconfirmedTimer = null;
|
|
4307
|
+
static RESOLVE_UNCONFIRMED_INTERVAL_MS = 1e4;
|
|
4308
|
+
// Retry every 10s
|
|
4309
|
+
// Guard: ensure load() completes before processing incoming bundles
|
|
4310
|
+
loadedPromise = null;
|
|
4311
|
+
loaded = false;
|
|
4312
|
+
// Persistent dedup: tracks splitGroupIds that have been fully processed.
|
|
4313
|
+
// Survives page reloads via KV storage so Nostr re-deliveries are ignored
|
|
4314
|
+
// even when the confirmed token's in-memory ID differs from v5split_{id}.
|
|
4315
|
+
processedSplitGroupIds = /* @__PURE__ */ new Set();
|
|
4298
4316
|
// Storage event subscriptions (push-based sync)
|
|
4299
4317
|
storageEventUnsubscribers = [];
|
|
4300
4318
|
syncDebounceTimer = null;
|
|
@@ -4344,7 +4362,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4344
4362
|
this.tombstones = [];
|
|
4345
4363
|
this.archivedTokens.clear();
|
|
4346
4364
|
this.forkedTokens.clear();
|
|
4347
|
-
this.
|
|
4365
|
+
this._historyCache = [];
|
|
4348
4366
|
this.nametags = [];
|
|
4349
4367
|
this.deps = deps;
|
|
4350
4368
|
this.priceProvider = deps.price ?? null;
|
|
@@ -4380,38 +4398,40 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4380
4398
|
*/
|
|
4381
4399
|
async load() {
|
|
4382
4400
|
this.ensureInitialized();
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4401
|
+
const doLoad = async () => {
|
|
4402
|
+
await TokenRegistry.waitForReady();
|
|
4403
|
+
const providers = this.getTokenStorageProviders();
|
|
4404
|
+
for (const [id, provider] of providers) {
|
|
4405
|
+
try {
|
|
4406
|
+
const result = await provider.load();
|
|
4407
|
+
if (result.success && result.data) {
|
|
4408
|
+
this.loadFromStorageData(result.data);
|
|
4409
|
+
this.log(`Loaded metadata from provider ${id}`);
|
|
4410
|
+
break;
|
|
4411
|
+
}
|
|
4412
|
+
} catch (err) {
|
|
4413
|
+
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4392
4414
|
}
|
|
4393
|
-
} catch (err) {
|
|
4394
|
-
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4395
|
-
}
|
|
4396
|
-
}
|
|
4397
|
-
await this.loadPendingV5Tokens();
|
|
4398
|
-
const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
|
|
4399
|
-
if (historyData) {
|
|
4400
|
-
try {
|
|
4401
|
-
this.transactionHistory = JSON.parse(historyData);
|
|
4402
|
-
} catch {
|
|
4403
|
-
this.transactionHistory = [];
|
|
4404
4415
|
}
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4416
|
+
const loadedTokens = Array.from(this.tokens.values()).map((t) => `${t.id.slice(0, 12)}(${t.status})`);
|
|
4417
|
+
console.log(`[Payments][DEBUG] load(): from TXF providers: ${this.tokens.size} tokens [${loadedTokens.join(", ")}]`);
|
|
4418
|
+
await this.loadPendingV5Tokens();
|
|
4419
|
+
await this.loadProcessedSplitGroupIds();
|
|
4420
|
+
await this.loadHistory();
|
|
4421
|
+
const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
|
|
4422
|
+
if (pending2) {
|
|
4423
|
+
const transfers = JSON.parse(pending2);
|
|
4424
|
+
for (const transfer of transfers) {
|
|
4425
|
+
this.pendingTransfers.set(transfer.id, transfer);
|
|
4426
|
+
}
|
|
4411
4427
|
}
|
|
4412
|
-
|
|
4428
|
+
this.loaded = true;
|
|
4429
|
+
};
|
|
4430
|
+
this.loadedPromise = doLoad();
|
|
4431
|
+
await this.loadedPromise;
|
|
4413
4432
|
this.resolveUnconfirmed().catch(() => {
|
|
4414
4433
|
});
|
|
4434
|
+
this.scheduleResolveUnconfirmed();
|
|
4415
4435
|
}
|
|
4416
4436
|
/**
|
|
4417
4437
|
* Cleanup all subscriptions, polling jobs, and pending resolvers.
|
|
@@ -4430,6 +4450,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4430
4450
|
this.paymentRequestResponseHandlers.clear();
|
|
4431
4451
|
this.stopProofPolling();
|
|
4432
4452
|
this.proofPollingJobs.clear();
|
|
4453
|
+
this.stopResolveUnconfirmedPolling();
|
|
4433
4454
|
for (const [, resolver] of this.pendingResponseResolvers) {
|
|
4434
4455
|
clearTimeout(resolver.timeout);
|
|
4435
4456
|
resolver.reject(new Error("Module destroyed"));
|
|
@@ -4489,7 +4510,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4489
4510
|
}
|
|
4490
4511
|
await this.saveToOutbox(result, recipientPubkey);
|
|
4491
4512
|
result.status = "submitted";
|
|
4492
|
-
const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
|
|
4513
|
+
const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
|
|
4493
4514
|
const transferMode = request.transferMode ?? "instant";
|
|
4494
4515
|
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4495
4516
|
if (transferMode === "conservative") {
|
|
@@ -4520,7 +4541,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4520
4541
|
updatedAt: Date.now(),
|
|
4521
4542
|
sdkData: JSON.stringify(changeTokenData)
|
|
4522
4543
|
};
|
|
4523
|
-
await this.addToken(changeUiToken
|
|
4544
|
+
await this.addToken(changeUiToken);
|
|
4524
4545
|
this.log(`Conservative split: change token saved: ${changeUiToken.id}`);
|
|
4525
4546
|
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4526
4547
|
sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
|
|
@@ -4529,7 +4550,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4529
4550
|
});
|
|
4530
4551
|
const splitCommitmentRequestId = splitResult.recipientTransferTx?.data?.requestId ?? splitResult.recipientTransferTx?.requestId;
|
|
4531
4552
|
const splitRequestIdHex = splitCommitmentRequestId instanceof Uint8Array ? Array.from(splitCommitmentRequestId).map((b) => b.toString(16).padStart(2, "0")).join("") : splitCommitmentRequestId ? String(splitCommitmentRequestId) : void 0;
|
|
4532
|
-
await this.removeToken(splitPlan.tokenToSplit.uiToken.id
|
|
4553
|
+
await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
|
|
4533
4554
|
result.tokenTransfers.push({
|
|
4534
4555
|
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4535
4556
|
method: "split",
|
|
@@ -4554,6 +4575,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4554
4575
|
this.deps.transport,
|
|
4555
4576
|
recipientPubkey,
|
|
4556
4577
|
{
|
|
4578
|
+
memo: request.memo,
|
|
4557
4579
|
onChangeTokenCreated: async (changeToken) => {
|
|
4558
4580
|
const changeTokenData = changeToken.toJSON();
|
|
4559
4581
|
const uiToken = {
|
|
@@ -4569,7 +4591,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4569
4591
|
updatedAt: Date.now(),
|
|
4570
4592
|
sdkData: JSON.stringify(changeTokenData)
|
|
4571
4593
|
};
|
|
4572
|
-
await this.addToken(uiToken
|
|
4594
|
+
await this.addToken(uiToken);
|
|
4573
4595
|
this.log(`Change token saved via background: ${uiToken.id}`);
|
|
4574
4596
|
},
|
|
4575
4597
|
onStorageSync: async () => {
|
|
@@ -4584,7 +4606,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4584
4606
|
if (instantResult.backgroundPromise) {
|
|
4585
4607
|
this.pendingBackgroundTasks.push(instantResult.backgroundPromise);
|
|
4586
4608
|
}
|
|
4587
|
-
await this.removeToken(splitPlan.tokenToSplit.uiToken.id
|
|
4609
|
+
await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
|
|
4588
4610
|
result.tokenTransfers.push({
|
|
4589
4611
|
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4590
4612
|
method: "split",
|
|
@@ -4631,20 +4653,25 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4631
4653
|
requestIdHex
|
|
4632
4654
|
});
|
|
4633
4655
|
this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
|
|
4634
|
-
await this.removeToken(token.id
|
|
4656
|
+
await this.removeToken(token.id);
|
|
4635
4657
|
}
|
|
4636
4658
|
result.status = "delivered";
|
|
4637
4659
|
await this.save();
|
|
4638
4660
|
await this.removeFromOutbox(result.id);
|
|
4639
4661
|
result.status = "completed";
|
|
4662
|
+
const sentTokenId = result.tokens[0] ? extractTokenIdFromSdkData(result.tokens[0].sdkData) : void 0;
|
|
4640
4663
|
await this.addToHistory({
|
|
4641
4664
|
type: "SENT",
|
|
4642
4665
|
amount: request.amount,
|
|
4643
4666
|
coinId: request.coinId,
|
|
4644
4667
|
symbol: this.getCoinSymbol(request.coinId),
|
|
4645
4668
|
timestamp: Date.now(),
|
|
4669
|
+
recipientPubkey,
|
|
4646
4670
|
recipientNametag,
|
|
4647
|
-
|
|
4671
|
+
recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
|
|
4672
|
+
memo: request.memo,
|
|
4673
|
+
transferId: result.id,
|
|
4674
|
+
tokenId: sentTokenId || void 0
|
|
4648
4675
|
});
|
|
4649
4676
|
this.deps.emitEvent("transfer:confirmed", result);
|
|
4650
4677
|
return result;
|
|
@@ -4755,6 +4782,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4755
4782
|
recipientPubkey,
|
|
4756
4783
|
{
|
|
4757
4784
|
...options,
|
|
4785
|
+
memo: request.memo,
|
|
4758
4786
|
onChangeTokenCreated: async (changeToken) => {
|
|
4759
4787
|
const changeTokenData = changeToken.toJSON();
|
|
4760
4788
|
const uiToken = {
|
|
@@ -4770,7 +4798,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4770
4798
|
updatedAt: Date.now(),
|
|
4771
4799
|
sdkData: JSON.stringify(changeTokenData)
|
|
4772
4800
|
};
|
|
4773
|
-
await this.addToken(uiToken
|
|
4801
|
+
await this.addToken(uiToken);
|
|
4774
4802
|
this.log(`Change token saved via background: ${uiToken.id}`);
|
|
4775
4803
|
},
|
|
4776
4804
|
onStorageSync: async () => {
|
|
@@ -4783,15 +4811,20 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4783
4811
|
if (result.backgroundPromise) {
|
|
4784
4812
|
this.pendingBackgroundTasks.push(result.backgroundPromise);
|
|
4785
4813
|
}
|
|
4786
|
-
|
|
4787
|
-
|
|
4814
|
+
await this.removeToken(tokenToSplit.id);
|
|
4815
|
+
const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
|
|
4816
|
+
const splitTokenId = extractTokenIdFromSdkData(tokenToSplit.sdkData);
|
|
4788
4817
|
await this.addToHistory({
|
|
4789
4818
|
type: "SENT",
|
|
4790
4819
|
amount: request.amount,
|
|
4791
4820
|
coinId: request.coinId,
|
|
4792
4821
|
symbol: this.getCoinSymbol(request.coinId),
|
|
4793
4822
|
timestamp: Date.now(),
|
|
4794
|
-
|
|
4823
|
+
recipientPubkey,
|
|
4824
|
+
recipientNametag,
|
|
4825
|
+
recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
|
|
4826
|
+
memo: request.memo,
|
|
4827
|
+
tokenId: splitTokenId || void 0
|
|
4795
4828
|
});
|
|
4796
4829
|
await this.save();
|
|
4797
4830
|
} else {
|
|
@@ -4822,15 +4855,18 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4822
4855
|
* @param senderPubkey - Sender's public key for verification
|
|
4823
4856
|
* @returns Processing result with finalized token
|
|
4824
4857
|
*/
|
|
4825
|
-
async processInstantSplitBundle(bundle, senderPubkey) {
|
|
4858
|
+
async processInstantSplitBundle(bundle, senderPubkey, memo) {
|
|
4826
4859
|
this.ensureInitialized();
|
|
4860
|
+
if (!this.loaded && this.loadedPromise) {
|
|
4861
|
+
await this.loadedPromise;
|
|
4862
|
+
}
|
|
4827
4863
|
if (!isInstantSplitBundleV5(bundle)) {
|
|
4828
|
-
return this.processInstantSplitBundleSync(bundle, senderPubkey);
|
|
4864
|
+
return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
|
|
4829
4865
|
}
|
|
4830
4866
|
try {
|
|
4831
4867
|
const deterministicId = `v5split_${bundle.splitGroupId}`;
|
|
4832
|
-
if (this.tokens.has(deterministicId)) {
|
|
4833
|
-
|
|
4868
|
+
if (this.tokens.has(deterministicId) || this.processedSplitGroupIds.has(bundle.splitGroupId)) {
|
|
4869
|
+
console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
|
|
4834
4870
|
return { success: true, durationMs: 0 };
|
|
4835
4871
|
}
|
|
4836
4872
|
const registry = TokenRegistry.getInstance();
|
|
@@ -4855,17 +4891,33 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4855
4891
|
updatedAt: Date.now(),
|
|
4856
4892
|
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
4857
4893
|
};
|
|
4858
|
-
await this.addToken(uiToken
|
|
4859
|
-
this.
|
|
4894
|
+
await this.addToken(uiToken);
|
|
4895
|
+
this.processedSplitGroupIds.add(bundle.splitGroupId);
|
|
4896
|
+
await this.saveProcessedSplitGroupIds();
|
|
4897
|
+
const senderInfo = await this.resolveSenderInfo(senderPubkey);
|
|
4898
|
+
await this.addToHistory({
|
|
4899
|
+
type: "RECEIVED",
|
|
4900
|
+
amount: bundle.amount,
|
|
4901
|
+
coinId: bundle.coinId,
|
|
4902
|
+
symbol: uiToken.symbol,
|
|
4903
|
+
timestamp: Date.now(),
|
|
4904
|
+
senderPubkey,
|
|
4905
|
+
...senderInfo,
|
|
4906
|
+
memo,
|
|
4907
|
+
tokenId: deterministicId
|
|
4908
|
+
});
|
|
4860
4909
|
this.deps.emitEvent("transfer:incoming", {
|
|
4861
4910
|
id: bundle.splitGroupId,
|
|
4862
4911
|
senderPubkey,
|
|
4912
|
+
senderNametag: senderInfo.senderNametag,
|
|
4863
4913
|
tokens: [uiToken],
|
|
4914
|
+
memo,
|
|
4864
4915
|
receivedAt: Date.now()
|
|
4865
4916
|
});
|
|
4866
4917
|
await this.save();
|
|
4867
4918
|
this.resolveUnconfirmed().catch(() => {
|
|
4868
4919
|
});
|
|
4920
|
+
this.scheduleResolveUnconfirmed();
|
|
4869
4921
|
return { success: true, durationMs: 0 };
|
|
4870
4922
|
} catch (error) {
|
|
4871
4923
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -4880,7 +4932,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4880
4932
|
* Synchronous V4 bundle processing (dev mode only).
|
|
4881
4933
|
* Kept for backward compatibility with V4 bundles.
|
|
4882
4934
|
*/
|
|
4883
|
-
async processInstantSplitBundleSync(bundle, senderPubkey) {
|
|
4935
|
+
async processInstantSplitBundleSync(bundle, senderPubkey, memo) {
|
|
4884
4936
|
try {
|
|
4885
4937
|
const signingService = await this.createSigningService();
|
|
4886
4938
|
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
@@ -4940,19 +4992,26 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4940
4992
|
sdkData: JSON.stringify(tokenData)
|
|
4941
4993
|
};
|
|
4942
4994
|
await this.addToken(uiToken);
|
|
4995
|
+
const receivedTokenId = extractTokenIdFromSdkData(uiToken.sdkData);
|
|
4996
|
+
const senderInfo = await this.resolveSenderInfo(senderPubkey);
|
|
4943
4997
|
await this.addToHistory({
|
|
4944
4998
|
type: "RECEIVED",
|
|
4945
4999
|
amount: bundle.amount,
|
|
4946
5000
|
coinId: info.coinId,
|
|
4947
5001
|
symbol: info.symbol,
|
|
4948
5002
|
timestamp: Date.now(),
|
|
4949
|
-
senderPubkey
|
|
5003
|
+
senderPubkey,
|
|
5004
|
+
...senderInfo,
|
|
5005
|
+
memo,
|
|
5006
|
+
tokenId: receivedTokenId || uiToken.id
|
|
4950
5007
|
});
|
|
4951
5008
|
await this.save();
|
|
4952
5009
|
this.deps.emitEvent("transfer:incoming", {
|
|
4953
5010
|
id: bundle.splitGroupId,
|
|
4954
5011
|
senderPubkey,
|
|
5012
|
+
senderNametag: senderInfo.senderNametag,
|
|
4955
5013
|
tokens: [uiToken],
|
|
5014
|
+
memo,
|
|
4956
5015
|
receivedAt: Date.now()
|
|
4957
5016
|
});
|
|
4958
5017
|
}
|
|
@@ -5605,28 +5664,70 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5605
5664
|
};
|
|
5606
5665
|
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5607
5666
|
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
5608
|
-
if (!stClient || !trustBase)
|
|
5667
|
+
if (!stClient || !trustBase) {
|
|
5668
|
+
console.log(`[V5-RESOLVE] resolveUnconfirmed: EARLY EXIT \u2014 stClient=${!!stClient} trustBase=${!!trustBase}`);
|
|
5669
|
+
return result;
|
|
5670
|
+
}
|
|
5609
5671
|
const signingService = await this.createSigningService();
|
|
5672
|
+
const submittedCount = Array.from(this.tokens.values()).filter((t) => t.status === "submitted").length;
|
|
5673
|
+
console.log(`[V5-RESOLVE] resolveUnconfirmed: ${submittedCount} submitted token(s) to process`);
|
|
5610
5674
|
for (const [tokenId, token] of this.tokens) {
|
|
5611
5675
|
if (token.status !== "submitted") continue;
|
|
5612
5676
|
const pending2 = this.parsePendingFinalization(token.sdkData);
|
|
5613
5677
|
if (!pending2) {
|
|
5678
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 16)}: no pending finalization metadata, skipping`);
|
|
5614
5679
|
result.stillPending++;
|
|
5615
5680
|
continue;
|
|
5616
5681
|
}
|
|
5617
5682
|
if (pending2.type === "v5_bundle") {
|
|
5683
|
+
console.log(`[V5-RESOLVE] Processing ${tokenId.slice(0, 16)}... stage=${pending2.stage} attempt=${pending2.attemptCount}`);
|
|
5618
5684
|
const progress = await this.resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService);
|
|
5685
|
+
console.log(`[V5-RESOLVE] Result for ${tokenId.slice(0, 16)}...: ${progress} (stage now: ${pending2.stage})`);
|
|
5619
5686
|
result.details.push({ tokenId, stage: pending2.stage, status: progress });
|
|
5620
5687
|
if (progress === "resolved") result.resolved++;
|
|
5621
5688
|
else if (progress === "failed") result.failed++;
|
|
5622
5689
|
else result.stillPending++;
|
|
5623
5690
|
}
|
|
5624
5691
|
}
|
|
5625
|
-
if (result.resolved > 0 || result.failed > 0) {
|
|
5692
|
+
if (result.resolved > 0 || result.failed > 0 || result.stillPending > 0) {
|
|
5693
|
+
console.log(`[V5-RESOLVE] Saving: resolved=${result.resolved} failed=${result.failed} stillPending=${result.stillPending}`);
|
|
5626
5694
|
await this.save();
|
|
5627
5695
|
}
|
|
5628
5696
|
return result;
|
|
5629
5697
|
}
|
|
5698
|
+
/**
|
|
5699
|
+
* Start a periodic interval that retries resolveUnconfirmed() until all
|
|
5700
|
+
* tokens are confirmed or failed. Stops automatically when nothing is
|
|
5701
|
+
* pending and is cleaned up by destroy().
|
|
5702
|
+
*/
|
|
5703
|
+
scheduleResolveUnconfirmed() {
|
|
5704
|
+
if (this.resolveUnconfirmedTimer) return;
|
|
5705
|
+
const hasUnconfirmed = Array.from(this.tokens.values()).some(
|
|
5706
|
+
(t) => t.status === "submitted"
|
|
5707
|
+
);
|
|
5708
|
+
if (!hasUnconfirmed) {
|
|
5709
|
+
console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: no submitted tokens, not starting timer`);
|
|
5710
|
+
return;
|
|
5711
|
+
}
|
|
5712
|
+
console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: starting periodic retry (every ${_PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS}ms)`);
|
|
5713
|
+
this.resolveUnconfirmedTimer = setInterval(async () => {
|
|
5714
|
+
try {
|
|
5715
|
+
const result = await this.resolveUnconfirmed();
|
|
5716
|
+
if (result.stillPending === 0) {
|
|
5717
|
+
console.log(`[V5-RESOLVE] All tokens resolved, stopping periodic retry`);
|
|
5718
|
+
this.stopResolveUnconfirmedPolling();
|
|
5719
|
+
}
|
|
5720
|
+
} catch (err) {
|
|
5721
|
+
console.log(`[V5-RESOLVE] Periodic retry error:`, err);
|
|
5722
|
+
}
|
|
5723
|
+
}, _PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS);
|
|
5724
|
+
}
|
|
5725
|
+
stopResolveUnconfirmedPolling() {
|
|
5726
|
+
if (this.resolveUnconfirmedTimer) {
|
|
5727
|
+
clearInterval(this.resolveUnconfirmedTimer);
|
|
5728
|
+
this.resolveUnconfirmedTimer = null;
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5630
5731
|
// ===========================================================================
|
|
5631
5732
|
// Private - V5 Lazy Resolution Helpers
|
|
5632
5733
|
// ===========================================================================
|
|
@@ -5639,10 +5740,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5639
5740
|
pending2.lastAttemptAt = Date.now();
|
|
5640
5741
|
try {
|
|
5641
5742
|
if (pending2.stage === "RECEIVED") {
|
|
5743
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: RECEIVED \u2192 submitting mint commitment...`);
|
|
5642
5744
|
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5643
5745
|
const mintData = await MintTransactionData3.fromJSON(mintDataJson);
|
|
5644
5746
|
const mintCommitment = await MintCommitment3.create(mintData);
|
|
5645
5747
|
const mintResponse = await stClient.submitMintCommitment(mintCommitment);
|
|
5748
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint response status=${mintResponse.status}`);
|
|
5646
5749
|
if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5647
5750
|
throw new Error(`Mint submission failed: ${mintResponse.status}`);
|
|
5648
5751
|
}
|
|
@@ -5650,22 +5753,27 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5650
5753
|
this.updatePendingFinalization(token, pending2);
|
|
5651
5754
|
}
|
|
5652
5755
|
if (pending2.stage === "MINT_SUBMITTED") {
|
|
5756
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_SUBMITTED \u2192 checking mint proof...`);
|
|
5653
5757
|
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5654
5758
|
const mintData = await MintTransactionData3.fromJSON(mintDataJson);
|
|
5655
5759
|
const mintCommitment = await MintCommitment3.create(mintData);
|
|
5656
5760
|
const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
|
|
5657
5761
|
if (!proof) {
|
|
5762
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof not yet available, staying MINT_SUBMITTED`);
|
|
5658
5763
|
this.updatePendingFinalization(token, pending2);
|
|
5659
5764
|
return "pending";
|
|
5660
5765
|
}
|
|
5766
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof obtained!`);
|
|
5661
5767
|
pending2.mintProofJson = JSON.stringify(proof);
|
|
5662
5768
|
pending2.stage = "MINT_PROVEN";
|
|
5663
5769
|
this.updatePendingFinalization(token, pending2);
|
|
5664
5770
|
}
|
|
5665
5771
|
if (pending2.stage === "MINT_PROVEN") {
|
|
5772
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_PROVEN \u2192 submitting transfer commitment...`);
|
|
5666
5773
|
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5667
5774
|
const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
|
|
5668
5775
|
const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
|
|
5776
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer response status=${transferResponse.status}`);
|
|
5669
5777
|
if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5670
5778
|
throw new Error(`Transfer submission failed: ${transferResponse.status}`);
|
|
5671
5779
|
}
|
|
@@ -5673,13 +5781,16 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5673
5781
|
this.updatePendingFinalization(token, pending2);
|
|
5674
5782
|
}
|
|
5675
5783
|
if (pending2.stage === "TRANSFER_SUBMITTED") {
|
|
5784
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: TRANSFER_SUBMITTED \u2192 checking transfer proof...`);
|
|
5676
5785
|
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5677
5786
|
const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
|
|
5678
5787
|
const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
|
|
5679
5788
|
if (!proof) {
|
|
5789
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof not yet available, staying TRANSFER_SUBMITTED`);
|
|
5680
5790
|
this.updatePendingFinalization(token, pending2);
|
|
5681
5791
|
return "pending";
|
|
5682
5792
|
}
|
|
5793
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof obtained! Finalizing...`);
|
|
5683
5794
|
const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
|
|
5684
5795
|
const confirmedToken = {
|
|
5685
5796
|
id: token.id,
|
|
@@ -5695,13 +5806,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5695
5806
|
sdkData: JSON.stringify(finalizedToken.toJSON())
|
|
5696
5807
|
};
|
|
5697
5808
|
this.tokens.set(tokenId, confirmedToken);
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
timestamp: Date.now(),
|
|
5704
|
-
senderPubkey: pending2.senderPubkey
|
|
5809
|
+
this.deps.emitEvent("transfer:confirmed", {
|
|
5810
|
+
id: crypto.randomUUID(),
|
|
5811
|
+
status: "completed",
|
|
5812
|
+
tokens: [confirmedToken],
|
|
5813
|
+
tokenTransfers: []
|
|
5705
5814
|
});
|
|
5706
5815
|
this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
|
|
5707
5816
|
return "resolved";
|
|
@@ -5844,11 +5953,20 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5844
5953
|
}
|
|
5845
5954
|
}
|
|
5846
5955
|
if (pendingTokens.length > 0) {
|
|
5956
|
+
const json = JSON.stringify(pendingTokens);
|
|
5957
|
+
this.log(`[V5-PERSIST] Saving ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")} (${json.length} bytes)`);
|
|
5847
5958
|
await this.deps.storage.set(
|
|
5848
5959
|
STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
|
|
5849
|
-
|
|
5960
|
+
json
|
|
5850
5961
|
);
|
|
5962
|
+
const verify = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
|
|
5963
|
+
if (!verify) {
|
|
5964
|
+
console.error("[Payments][V5-PERSIST] CRITICAL: KV write succeeded but read-back is empty!");
|
|
5965
|
+
} else {
|
|
5966
|
+
this.log(`[V5-PERSIST] Verified: read-back ${verify.length} bytes`);
|
|
5967
|
+
}
|
|
5851
5968
|
} else {
|
|
5969
|
+
this.log(`[V5-PERSIST] No pending V5 tokens to save (total tokens: ${this.tokens.size}), clearing KV`);
|
|
5852
5970
|
await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
|
|
5853
5971
|
}
|
|
5854
5972
|
}
|
|
@@ -5858,16 +5976,47 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5858
5976
|
*/
|
|
5859
5977
|
async loadPendingV5Tokens() {
|
|
5860
5978
|
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
|
|
5979
|
+
this.log(`[V5-PERSIST] loadPendingV5Tokens: KV data = ${data ? `${data.length} bytes` : "null/empty"}`);
|
|
5861
5980
|
if (!data) return;
|
|
5862
5981
|
try {
|
|
5863
5982
|
const pendingTokens = JSON.parse(data);
|
|
5983
|
+
this.log(`[V5-PERSIST] Parsed ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")}`);
|
|
5864
5984
|
for (const token of pendingTokens) {
|
|
5865
5985
|
if (!this.tokens.has(token.id)) {
|
|
5866
5986
|
this.tokens.set(token.id, token);
|
|
5987
|
+
this.log(`[V5-PERSIST] Restored token ${token.id.slice(0, 16)} (status=${token.status})`);
|
|
5988
|
+
} else {
|
|
5989
|
+
this.log(`[V5-PERSIST] Token ${token.id.slice(0, 16)} already in map, skipping`);
|
|
5867
5990
|
}
|
|
5868
5991
|
}
|
|
5869
|
-
|
|
5870
|
-
|
|
5992
|
+
} catch (err) {
|
|
5993
|
+
console.error("[Payments][V5-PERSIST] Failed to parse pending V5 tokens:", err);
|
|
5994
|
+
}
|
|
5995
|
+
}
|
|
5996
|
+
/**
|
|
5997
|
+
* Persist the set of processed splitGroupIds to KV storage.
|
|
5998
|
+
* This ensures Nostr re-deliveries are ignored across page reloads,
|
|
5999
|
+
* even when the confirmed token's in-memory ID differs from v5split_{id}.
|
|
6000
|
+
*/
|
|
6001
|
+
async saveProcessedSplitGroupIds() {
|
|
6002
|
+
const ids = Array.from(this.processedSplitGroupIds);
|
|
6003
|
+
if (ids.length > 0) {
|
|
6004
|
+
await this.deps.storage.set(
|
|
6005
|
+
STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS,
|
|
6006
|
+
JSON.stringify(ids)
|
|
6007
|
+
);
|
|
6008
|
+
}
|
|
6009
|
+
}
|
|
6010
|
+
/**
|
|
6011
|
+
* Load processed splitGroupIds from KV storage.
|
|
6012
|
+
*/
|
|
6013
|
+
async loadProcessedSplitGroupIds() {
|
|
6014
|
+
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS);
|
|
6015
|
+
if (!data) return;
|
|
6016
|
+
try {
|
|
6017
|
+
const ids = JSON.parse(data);
|
|
6018
|
+
for (const id of ids) {
|
|
6019
|
+
this.processedSplitGroupIds.add(id);
|
|
5871
6020
|
}
|
|
5872
6021
|
} catch {
|
|
5873
6022
|
}
|
|
@@ -5886,10 +6035,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5886
6035
|
* the old state is archived and replaced with the incoming one.
|
|
5887
6036
|
*
|
|
5888
6037
|
* @param token - The token to add.
|
|
5889
|
-
* @param skipHistory - When `true`, do not create a `RECEIVED` transaction history entry (default `false`).
|
|
5890
6038
|
* @returns `true` if the token was added, `false` if rejected as duplicate or tombstoned.
|
|
5891
6039
|
*/
|
|
5892
|
-
async addToken(token
|
|
6040
|
+
async addToken(token) {
|
|
5893
6041
|
this.ensureInitialized();
|
|
5894
6042
|
const incomingTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
5895
6043
|
const incomingStateHash = extractStateHashFromSdkData(token.sdkData);
|
|
@@ -5935,15 +6083,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5935
6083
|
}
|
|
5936
6084
|
this.tokens.set(token.id, token);
|
|
5937
6085
|
await this.archiveToken(token);
|
|
5938
|
-
if (!skipHistory && token.coinId && token.amount) {
|
|
5939
|
-
await this.addToHistory({
|
|
5940
|
-
type: "RECEIVED",
|
|
5941
|
-
amount: token.amount,
|
|
5942
|
-
coinId: token.coinId,
|
|
5943
|
-
symbol: token.symbol || "UNK",
|
|
5944
|
-
timestamp: token.createdAt || Date.now()
|
|
5945
|
-
});
|
|
5946
|
-
}
|
|
5947
6086
|
await this.save();
|
|
5948
6087
|
this.log(`Added token ${token.id}, total: ${this.tokens.size}`);
|
|
5949
6088
|
return true;
|
|
@@ -5970,7 +6109,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5970
6109
|
}
|
|
5971
6110
|
}
|
|
5972
6111
|
if (!found) {
|
|
5973
|
-
await this.addToken(token
|
|
6112
|
+
await this.addToken(token);
|
|
5974
6113
|
return;
|
|
5975
6114
|
}
|
|
5976
6115
|
await this.archiveToken(token);
|
|
@@ -5985,10 +6124,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5985
6124
|
* entry is created unless `skipHistory` is `true`.
|
|
5986
6125
|
*
|
|
5987
6126
|
* @param tokenId - Local UUID of the token to remove.
|
|
5988
|
-
* @param recipientNametag - Optional nametag of the transfer recipient (for history).
|
|
5989
|
-
* @param skipHistory - When `true`, skip creating a transaction history entry (default `false`).
|
|
5990
6127
|
*/
|
|
5991
|
-
async removeToken(tokenId
|
|
6128
|
+
async removeToken(tokenId) {
|
|
5992
6129
|
this.ensureInitialized();
|
|
5993
6130
|
const token = this.tokens.get(tokenId);
|
|
5994
6131
|
if (!token) return;
|
|
@@ -6006,16 +6143,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6006
6143
|
this.log(`Warning: Could not create tombstone for token ${tokenId.slice(0, 8)}... (missing tokenId or stateHash)`);
|
|
6007
6144
|
}
|
|
6008
6145
|
this.tokens.delete(tokenId);
|
|
6009
|
-
if (!skipHistory && token.coinId && token.amount) {
|
|
6010
|
-
await this.addToHistory({
|
|
6011
|
-
type: "SENT",
|
|
6012
|
-
amount: token.amount,
|
|
6013
|
-
coinId: token.coinId,
|
|
6014
|
-
symbol: token.symbol || "UNK",
|
|
6015
|
-
timestamp: Date.now(),
|
|
6016
|
-
recipientNametag
|
|
6017
|
-
});
|
|
6018
|
-
}
|
|
6019
6146
|
await this.save();
|
|
6020
6147
|
}
|
|
6021
6148
|
// ===========================================================================
|
|
@@ -6240,26 +6367,104 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6240
6367
|
* @returns Array of {@link TransactionHistoryEntry} objects in descending timestamp order.
|
|
6241
6368
|
*/
|
|
6242
6369
|
getHistory() {
|
|
6243
|
-
return [...this.
|
|
6370
|
+
return [...this._historyCache].sort((a, b) => b.timestamp - a.timestamp);
|
|
6371
|
+
}
|
|
6372
|
+
/**
|
|
6373
|
+
* Best-effort resolve sender's DIRECT address and nametag from their transport pubkey.
|
|
6374
|
+
* Returns empty object if transport doesn't support resolution or lookup fails.
|
|
6375
|
+
*/
|
|
6376
|
+
async resolveSenderInfo(senderTransportPubkey) {
|
|
6377
|
+
try {
|
|
6378
|
+
if (this.deps?.transport?.resolveTransportPubkeyInfo) {
|
|
6379
|
+
const peerInfo = await this.deps.transport.resolveTransportPubkeyInfo(senderTransportPubkey);
|
|
6380
|
+
if (peerInfo) {
|
|
6381
|
+
return {
|
|
6382
|
+
senderAddress: peerInfo.directAddress || void 0,
|
|
6383
|
+
senderNametag: peerInfo.nametag || void 0
|
|
6384
|
+
};
|
|
6385
|
+
}
|
|
6386
|
+
}
|
|
6387
|
+
} catch {
|
|
6388
|
+
}
|
|
6389
|
+
return {};
|
|
6244
6390
|
}
|
|
6245
6391
|
/**
|
|
6246
6392
|
* Append an entry to the transaction history.
|
|
6247
6393
|
*
|
|
6248
|
-
* A unique `id`
|
|
6394
|
+
* A unique `id` and `dedupKey` are auto-generated. The entry is persisted to
|
|
6395
|
+
* the local token storage provider's `history` store (IndexedDB / file).
|
|
6396
|
+
* Duplicate entries with the same `dedupKey` are silently ignored (upsert).
|
|
6249
6397
|
*
|
|
6250
|
-
* @param entry - History entry fields (without `id`).
|
|
6398
|
+
* @param entry - History entry fields (without `id` and `dedupKey`).
|
|
6251
6399
|
*/
|
|
6252
6400
|
async addToHistory(entry) {
|
|
6253
6401
|
this.ensureInitialized();
|
|
6402
|
+
const dedupKey = computeHistoryDedupKey(entry.type, entry.tokenId, entry.transferId);
|
|
6254
6403
|
const historyEntry = {
|
|
6255
6404
|
id: crypto.randomUUID(),
|
|
6405
|
+
dedupKey,
|
|
6256
6406
|
...entry
|
|
6257
6407
|
};
|
|
6258
|
-
this.
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
);
|
|
6408
|
+
const provider = this.getLocalTokenStorageProvider();
|
|
6409
|
+
if (provider?.addHistoryEntry) {
|
|
6410
|
+
await provider.addHistoryEntry(historyEntry);
|
|
6411
|
+
}
|
|
6412
|
+
const existingIdx = this._historyCache.findIndex((e) => e.dedupKey === dedupKey);
|
|
6413
|
+
if (existingIdx >= 0) {
|
|
6414
|
+
this._historyCache[existingIdx] = historyEntry;
|
|
6415
|
+
} else {
|
|
6416
|
+
this._historyCache.push(historyEntry);
|
|
6417
|
+
}
|
|
6418
|
+
this.deps.emitEvent("history:updated", historyEntry);
|
|
6419
|
+
}
|
|
6420
|
+
/**
|
|
6421
|
+
* Load history from the local token storage provider into the in-memory cache.
|
|
6422
|
+
* Also performs one-time migration from legacy KV storage.
|
|
6423
|
+
*/
|
|
6424
|
+
async loadHistory() {
|
|
6425
|
+
const provider = this.getLocalTokenStorageProvider();
|
|
6426
|
+
if (provider?.getHistoryEntries) {
|
|
6427
|
+
this._historyCache = await provider.getHistoryEntries();
|
|
6428
|
+
const legacyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
|
|
6429
|
+
if (legacyData) {
|
|
6430
|
+
try {
|
|
6431
|
+
const legacyEntries = JSON.parse(legacyData);
|
|
6432
|
+
const records = legacyEntries.map((e) => ({
|
|
6433
|
+
...e,
|
|
6434
|
+
dedupKey: e.dedupKey || computeHistoryDedupKey(e.type, e.tokenId, e.transferId)
|
|
6435
|
+
}));
|
|
6436
|
+
const imported = await provider.importHistoryEntries?.(records) ?? 0;
|
|
6437
|
+
if (imported > 0) {
|
|
6438
|
+
this._historyCache = await provider.getHistoryEntries();
|
|
6439
|
+
this.log(`Migrated ${imported} history entries from KV to history store`);
|
|
6440
|
+
}
|
|
6441
|
+
await this.deps.storage.remove(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
|
|
6442
|
+
} catch {
|
|
6443
|
+
}
|
|
6444
|
+
}
|
|
6445
|
+
} else {
|
|
6446
|
+
const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
|
|
6447
|
+
if (historyData) {
|
|
6448
|
+
try {
|
|
6449
|
+
this._historyCache = JSON.parse(historyData);
|
|
6450
|
+
} catch {
|
|
6451
|
+
this._historyCache = [];
|
|
6452
|
+
}
|
|
6453
|
+
}
|
|
6454
|
+
}
|
|
6455
|
+
}
|
|
6456
|
+
/**
|
|
6457
|
+
* Get the first local token storage provider (for history operations).
|
|
6458
|
+
*/
|
|
6459
|
+
getLocalTokenStorageProvider() {
|
|
6460
|
+
const providers = this.getTokenStorageProviders();
|
|
6461
|
+
for (const [, provider] of providers) {
|
|
6462
|
+
if (provider.type === "local") return provider;
|
|
6463
|
+
}
|
|
6464
|
+
for (const [, provider] of providers) {
|
|
6465
|
+
return provider;
|
|
6466
|
+
}
|
|
6467
|
+
return null;
|
|
6263
6468
|
}
|
|
6264
6469
|
// ===========================================================================
|
|
6265
6470
|
// Public API - Nametag
|
|
@@ -6466,7 +6671,32 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6466
6671
|
try {
|
|
6467
6672
|
const result = await provider.sync(localData);
|
|
6468
6673
|
if (result.success && result.merged) {
|
|
6674
|
+
const savedTokens = new Map(this.tokens);
|
|
6469
6675
|
this.loadFromStorageData(result.merged);
|
|
6676
|
+
let restoredCount = 0;
|
|
6677
|
+
for (const [tokenId, token] of savedTokens) {
|
|
6678
|
+
if (this.tokens.has(tokenId)) continue;
|
|
6679
|
+
const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
6680
|
+
const stateHash = extractStateHashFromSdkData(token.sdkData);
|
|
6681
|
+
if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
|
|
6682
|
+
continue;
|
|
6683
|
+
}
|
|
6684
|
+
if (sdkTokenId) {
|
|
6685
|
+
let hasEquivalent = false;
|
|
6686
|
+
for (const existing of this.tokens.values()) {
|
|
6687
|
+
if (extractTokenIdFromSdkData(existing.sdkData) === sdkTokenId) {
|
|
6688
|
+
hasEquivalent = true;
|
|
6689
|
+
break;
|
|
6690
|
+
}
|
|
6691
|
+
}
|
|
6692
|
+
if (hasEquivalent) continue;
|
|
6693
|
+
}
|
|
6694
|
+
this.tokens.set(tokenId, token);
|
|
6695
|
+
restoredCount++;
|
|
6696
|
+
}
|
|
6697
|
+
if (restoredCount > 0) {
|
|
6698
|
+
console.log(`[Payments] Sync: restored ${restoredCount} token(s) lost by loadFromStorageData`);
|
|
6699
|
+
}
|
|
6470
6700
|
if (this.nametags.length === 0 && savedNametags.length > 0) {
|
|
6471
6701
|
this.nametags = savedNametags;
|
|
6472
6702
|
}
|
|
@@ -6800,16 +7030,30 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6800
7030
|
return;
|
|
6801
7031
|
}
|
|
6802
7032
|
this.tokens.set(token.id, token);
|
|
7033
|
+
console.log(`[Payments][DEBUG] NOSTR-FIRST: saving token id=${token.id.slice(0, 16)} status=${token.status} sdkData.length=${token.sdkData?.length}`);
|
|
6803
7034
|
await this.save();
|
|
6804
|
-
|
|
7035
|
+
console.log(`[Payments][DEBUG] NOSTR-FIRST: save() completed, tokens.size=${this.tokens.size}`);
|
|
7036
|
+
const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
|
|
6805
7037
|
const incomingTransfer = {
|
|
6806
7038
|
id: transfer.id,
|
|
6807
7039
|
senderPubkey: transfer.senderTransportPubkey,
|
|
7040
|
+
senderNametag: senderInfo.senderNametag,
|
|
6808
7041
|
tokens: [token],
|
|
6809
7042
|
memo: payload.memo,
|
|
6810
7043
|
receivedAt: transfer.timestamp
|
|
6811
7044
|
};
|
|
6812
7045
|
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
7046
|
+
await this.addToHistory({
|
|
7047
|
+
type: "RECEIVED",
|
|
7048
|
+
amount: token.amount,
|
|
7049
|
+
coinId: token.coinId,
|
|
7050
|
+
symbol: token.symbol,
|
|
7051
|
+
timestamp: Date.now(),
|
|
7052
|
+
senderPubkey: transfer.senderTransportPubkey,
|
|
7053
|
+
...senderInfo,
|
|
7054
|
+
memo: payload.memo,
|
|
7055
|
+
tokenId: nostrTokenId || token.id
|
|
7056
|
+
});
|
|
6813
7057
|
try {
|
|
6814
7058
|
const commitment = await TransferCommitment4.fromJSON(commitmentInput);
|
|
6815
7059
|
const requestIdBytes = commitment.requestId;
|
|
@@ -6827,7 +7071,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6827
7071
|
attemptCount: 0,
|
|
6828
7072
|
lastAttemptAt: 0,
|
|
6829
7073
|
onProofReceived: async (tokenId) => {
|
|
6830
|
-
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput
|
|
7074
|
+
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
|
|
6831
7075
|
}
|
|
6832
7076
|
});
|
|
6833
7077
|
} catch (err) {
|
|
@@ -6886,7 +7130,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6886
7130
|
/**
|
|
6887
7131
|
* Finalize a received token after proof is available
|
|
6888
7132
|
*/
|
|
6889
|
-
async finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput
|
|
7133
|
+
async finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput) {
|
|
6890
7134
|
try {
|
|
6891
7135
|
const token = this.tokens.get(tokenId);
|
|
6892
7136
|
if (!token) {
|
|
@@ -6934,14 +7178,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6934
7178
|
tokens: [finalizedToken],
|
|
6935
7179
|
tokenTransfers: []
|
|
6936
7180
|
});
|
|
6937
|
-
await this.addToHistory({
|
|
6938
|
-
type: "RECEIVED",
|
|
6939
|
-
amount: finalizedToken.amount,
|
|
6940
|
-
coinId: finalizedToken.coinId,
|
|
6941
|
-
symbol: finalizedToken.symbol,
|
|
6942
|
-
timestamp: Date.now(),
|
|
6943
|
-
senderPubkey
|
|
6944
|
-
});
|
|
6945
7181
|
} catch (error) {
|
|
6946
7182
|
console.error("[Payments] Failed to finalize received token:", error);
|
|
6947
7183
|
const token = this.tokens.get(tokenId);
|
|
@@ -6953,8 +7189,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6953
7189
|
}
|
|
6954
7190
|
}
|
|
6955
7191
|
async handleIncomingTransfer(transfer) {
|
|
7192
|
+
if (!this.loaded && this.loadedPromise) {
|
|
7193
|
+
await this.loadedPromise;
|
|
7194
|
+
}
|
|
6956
7195
|
try {
|
|
6957
7196
|
const payload = transfer.payload;
|
|
7197
|
+
console.log("[Payments][DEBUG] handleIncomingTransfer: keys=", Object.keys(payload).join(","));
|
|
6958
7198
|
let instantBundle = null;
|
|
6959
7199
|
if (isInstantSplitBundle(payload)) {
|
|
6960
7200
|
instantBundle = payload;
|
|
@@ -6972,7 +7212,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6972
7212
|
try {
|
|
6973
7213
|
const result = await this.processInstantSplitBundle(
|
|
6974
7214
|
instantBundle,
|
|
6975
|
-
transfer.senderTransportPubkey
|
|
7215
|
+
transfer.senderTransportPubkey,
|
|
7216
|
+
payload.memo
|
|
6976
7217
|
);
|
|
6977
7218
|
if (result.success) {
|
|
6978
7219
|
this.log("INSTANT_SPLIT processed successfully");
|
|
@@ -6985,7 +7226,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6985
7226
|
return;
|
|
6986
7227
|
}
|
|
6987
7228
|
if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
|
|
6988
|
-
|
|
7229
|
+
console.log("[Payments][DEBUG] >>> NOSTR-FIRST commitment-only transfer detected");
|
|
6989
7230
|
await this.handleCommitmentOnlyTransfer(transfer, payload);
|
|
6990
7231
|
return;
|
|
6991
7232
|
}
|
|
@@ -7090,10 +7331,26 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7090
7331
|
updatedAt: Date.now(),
|
|
7091
7332
|
sdkData: typeof tokenData === "string" ? tokenData : JSON.stringify(tokenData)
|
|
7092
7333
|
};
|
|
7093
|
-
await this.addToken(token);
|
|
7334
|
+
const added = await this.addToken(token);
|
|
7335
|
+
const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
|
|
7336
|
+
if (added) {
|
|
7337
|
+
const incomingTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7338
|
+
await this.addToHistory({
|
|
7339
|
+
type: "RECEIVED",
|
|
7340
|
+
amount: token.amount,
|
|
7341
|
+
coinId: token.coinId,
|
|
7342
|
+
symbol: token.symbol,
|
|
7343
|
+
timestamp: Date.now(),
|
|
7344
|
+
senderPubkey: transfer.senderTransportPubkey,
|
|
7345
|
+
...senderInfo,
|
|
7346
|
+
memo: payload.memo,
|
|
7347
|
+
tokenId: incomingTokenId || token.id
|
|
7348
|
+
});
|
|
7349
|
+
}
|
|
7094
7350
|
const incomingTransfer = {
|
|
7095
7351
|
id: transfer.id,
|
|
7096
7352
|
senderPubkey: transfer.senderTransportPubkey,
|
|
7353
|
+
senderNametag: senderInfo.senderNametag,
|
|
7097
7354
|
tokens: [token],
|
|
7098
7355
|
memo: payload.memo,
|
|
7099
7356
|
receivedAt: transfer.timestamp
|
|
@@ -7132,17 +7389,24 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7132
7389
|
// ===========================================================================
|
|
7133
7390
|
async save() {
|
|
7134
7391
|
const providers = this.getTokenStorageProviders();
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
return
|
|
7138
|
-
}
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7392
|
+
const tokenStats = Array.from(this.tokens.values()).map((t) => {
|
|
7393
|
+
const txf = tokenToTxf(t);
|
|
7394
|
+
return `${t.id.slice(0, 12)}(${t.status},txf=${!!txf})`;
|
|
7395
|
+
});
|
|
7396
|
+
console.log(`[Payments][DEBUG] save(): providers=${providers.size}, tokens=[${tokenStats.join(", ")}]`);
|
|
7397
|
+
if (providers.size > 0) {
|
|
7398
|
+
const data = await this.createStorageData();
|
|
7399
|
+
const dataKeys = Object.keys(data).filter((k) => k.startsWith("token-"));
|
|
7400
|
+
console.log(`[Payments][DEBUG] save(): TXF keys=${dataKeys.length} (${dataKeys.join(", ")})`);
|
|
7401
|
+
for (const [id, provider] of providers) {
|
|
7402
|
+
try {
|
|
7403
|
+
await provider.save(data);
|
|
7404
|
+
} catch (err) {
|
|
7405
|
+
console.error(`[Payments] Failed to save to provider ${id}:`, err);
|
|
7406
|
+
}
|
|
7145
7407
|
}
|
|
7408
|
+
} else {
|
|
7409
|
+
console.log("[Payments][DEBUG] save(): No token storage providers - TXF not persisted");
|
|
7146
7410
|
}
|
|
7147
7411
|
await this.savePendingV5Tokens();
|
|
7148
7412
|
}
|
|
@@ -7178,6 +7442,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7178
7442
|
}
|
|
7179
7443
|
loadFromStorageData(data) {
|
|
7180
7444
|
const parsed = parseTxfStorageData(data);
|
|
7445
|
+
console.log(`[Payments][DEBUG] loadFromStorageData: parsed ${parsed.tokens.length} tokens, ${parsed.tombstones.length} tombstones, errors=[${parsed.validationErrors.join("; ")}]`);
|
|
7181
7446
|
this.tombstones = parsed.tombstones;
|
|
7182
7447
|
this.tokens.clear();
|
|
7183
7448
|
for (const token of parsed.tokens) {
|