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