@unicitylabs/sphere-sdk 0.5.0 → 0.5.2
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 +5 -1
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/index.js +5 -1
- package/dist/connect/index.js.map +1 -1
- package/dist/core/index.cjs +813 -309
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +71 -2
- package/dist/core/index.d.ts +71 -2
- package/dist/core/index.js +813 -309
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/connect/index.cjs +5 -1
- package/dist/impl/browser/connect/index.cjs.map +1 -1
- package/dist/impl/browser/connect/index.js +5 -1
- package/dist/impl/browser/connect/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +7 -2
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +7 -2
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/browser/ipfs.cjs +5 -1
- package/dist/impl/browser/ipfs.cjs.map +1 -1
- package/dist/impl/browser/ipfs.js +5 -1
- package/dist/impl/browser/ipfs.js.map +1 -1
- package/dist/impl/nodejs/connect/index.cjs +5 -1
- package/dist/impl/nodejs/connect/index.cjs.map +1 -1
- package/dist/impl/nodejs/connect/index.js +5 -1
- package/dist/impl/nodejs/connect/index.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +7 -2
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +6 -0
- package/dist/impl/nodejs/index.d.ts +6 -0
- package/dist/impl/nodejs/index.js +7 -2
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +815 -309
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +144 -3
- package/dist/index.d.ts +144 -3
- package/dist/index.js +814 -309
- package/dist/index.js.map +1 -1
- package/dist/l1/index.cjs +5 -1
- package/dist/l1/index.cjs.map +1 -1
- package/dist/l1/index.js +5 -1
- package/dist/l1/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -107,7 +107,11 @@ var init_constants = __esm({
|
|
|
107
107
|
/** Group chat: members for this address */
|
|
108
108
|
GROUP_CHAT_MEMBERS: "group_chat_members",
|
|
109
109
|
/** Group chat: processed event IDs for deduplication */
|
|
110
|
-
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events"
|
|
110
|
+
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
|
|
111
|
+
/** Processed V5 split group IDs for Nostr re-delivery dedup */
|
|
112
|
+
PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids",
|
|
113
|
+
/** Processed V6 combined transfer IDs for Nostr re-delivery dedup */
|
|
114
|
+
PROCESSED_COMBINED_TRANSFER_IDS: "processed_combined_transfer_ids"
|
|
111
115
|
};
|
|
112
116
|
STORAGE_KEYS = {
|
|
113
117
|
...STORAGE_KEYS_GLOBAL,
|
|
@@ -790,6 +794,7 @@ __export(index_exports, {
|
|
|
790
794
|
identityFromMnemonicSync: () => identityFromMnemonicSync,
|
|
791
795
|
initSphere: () => initSphere,
|
|
792
796
|
isArchivedKey: () => isArchivedKey,
|
|
797
|
+
isCombinedTransferBundleV6: () => isCombinedTransferBundleV6,
|
|
793
798
|
isForkedKey: () => isForkedKey,
|
|
794
799
|
isInstantSplitBundle: () => isInstantSplitBundle,
|
|
795
800
|
isInstantSplitBundleV4: () => isInstantSplitBundleV4,
|
|
@@ -3740,14 +3745,149 @@ var InstantSplitExecutor = class {
|
|
|
3740
3745
|
this.devMode = config.devMode ?? false;
|
|
3741
3746
|
}
|
|
3742
3747
|
/**
|
|
3743
|
-
*
|
|
3748
|
+
* Build a V5 split bundle WITHOUT sending it via transport.
|
|
3744
3749
|
*
|
|
3745
|
-
*
|
|
3750
|
+
* Steps 1-5 of the V5 flow:
|
|
3746
3751
|
* 1. Create and submit burn commitment
|
|
3747
3752
|
* 2. Wait for burn proof
|
|
3748
3753
|
* 3. Create mint commitments with SplitMintReason
|
|
3749
3754
|
* 4. Create transfer commitment (no mint proof needed)
|
|
3750
|
-
* 5.
|
|
3755
|
+
* 5. Package V5 bundle
|
|
3756
|
+
*
|
|
3757
|
+
* The caller is responsible for sending the bundle and then calling
|
|
3758
|
+
* `startBackground()` on the result to begin mint proof + change token creation.
|
|
3759
|
+
*/
|
|
3760
|
+
async buildSplitBundle(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, options) {
|
|
3761
|
+
const splitGroupId = crypto.randomUUID();
|
|
3762
|
+
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
3763
|
+
console.log(`[InstantSplit] Building V5 bundle for token ${tokenIdHex.slice(0, 8)}...`);
|
|
3764
|
+
const coinId = new import_CoinId3.CoinId(fromHex2(coinIdHex));
|
|
3765
|
+
const seedString = `${tokenIdHex}_${splitAmount.toString()}_${remainderAmount.toString()}_${Date.now()}`;
|
|
3766
|
+
const recipientTokenId = new import_TokenId3.TokenId(await sha2563(seedString));
|
|
3767
|
+
const senderTokenId = new import_TokenId3.TokenId(await sha2563(seedString + "_sender"));
|
|
3768
|
+
const recipientSalt = await sha2563(seedString + "_recipient_salt");
|
|
3769
|
+
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
3770
|
+
const senderAddressRef = await import_UnmaskedPredicateReference2.UnmaskedPredicateReference.create(
|
|
3771
|
+
tokenToSplit.type,
|
|
3772
|
+
this.signingService.algorithm,
|
|
3773
|
+
this.signingService.publicKey,
|
|
3774
|
+
import_HashAlgorithm3.HashAlgorithm.SHA256
|
|
3775
|
+
);
|
|
3776
|
+
const senderAddress = await senderAddressRef.toAddress();
|
|
3777
|
+
const builder = new import_TokenSplitBuilder2.TokenSplitBuilder();
|
|
3778
|
+
const coinDataA = import_TokenCoinData2.TokenCoinData.create([[coinId, splitAmount]]);
|
|
3779
|
+
builder.createToken(
|
|
3780
|
+
recipientTokenId,
|
|
3781
|
+
tokenToSplit.type,
|
|
3782
|
+
new Uint8Array(0),
|
|
3783
|
+
coinDataA,
|
|
3784
|
+
senderAddress,
|
|
3785
|
+
// Mint to sender first, then transfer
|
|
3786
|
+
recipientSalt,
|
|
3787
|
+
null
|
|
3788
|
+
);
|
|
3789
|
+
const coinDataB = import_TokenCoinData2.TokenCoinData.create([[coinId, remainderAmount]]);
|
|
3790
|
+
builder.createToken(
|
|
3791
|
+
senderTokenId,
|
|
3792
|
+
tokenToSplit.type,
|
|
3793
|
+
new Uint8Array(0),
|
|
3794
|
+
coinDataB,
|
|
3795
|
+
senderAddress,
|
|
3796
|
+
senderSalt,
|
|
3797
|
+
null
|
|
3798
|
+
);
|
|
3799
|
+
const split = await builder.build(tokenToSplit);
|
|
3800
|
+
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
3801
|
+
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
3802
|
+
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
3803
|
+
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3804
|
+
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3805
|
+
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3806
|
+
}
|
|
3807
|
+
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3808
|
+
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, burnCommitment);
|
|
3809
|
+
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3810
|
+
console.log(`[InstantSplit] Burn proof received`);
|
|
3811
|
+
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3812
|
+
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3813
|
+
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3814
|
+
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3815
|
+
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3816
|
+
const recipientMintCommitment = mintCommitments.find(
|
|
3817
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3818
|
+
);
|
|
3819
|
+
const senderMintCommitment = mintCommitments.find(
|
|
3820
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3821
|
+
);
|
|
3822
|
+
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3823
|
+
throw new Error("Failed to find expected mint commitments");
|
|
3824
|
+
}
|
|
3825
|
+
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3826
|
+
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3827
|
+
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3828
|
+
recipientMintCommitment.transactionData,
|
|
3829
|
+
recipientAddress,
|
|
3830
|
+
transferSalt,
|
|
3831
|
+
this.signingService
|
|
3832
|
+
);
|
|
3833
|
+
const mintedPredicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
|
|
3834
|
+
recipientTokenId,
|
|
3835
|
+
tokenToSplit.type,
|
|
3836
|
+
this.signingService,
|
|
3837
|
+
import_HashAlgorithm3.HashAlgorithm.SHA256,
|
|
3838
|
+
recipientSalt
|
|
3839
|
+
);
|
|
3840
|
+
const mintedState = new import_TokenState3.TokenState(mintedPredicate, null);
|
|
3841
|
+
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3842
|
+
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3843
|
+
let nametagTokenJson;
|
|
3844
|
+
const recipientAddressStr = recipientAddress.toString();
|
|
3845
|
+
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3846
|
+
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3847
|
+
}
|
|
3848
|
+
const bundle = {
|
|
3849
|
+
version: "5.0",
|
|
3850
|
+
type: "INSTANT_SPLIT",
|
|
3851
|
+
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3852
|
+
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3853
|
+
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3854
|
+
amount: splitAmount.toString(),
|
|
3855
|
+
coinId: coinIdHex,
|
|
3856
|
+
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3857
|
+
splitGroupId,
|
|
3858
|
+
senderPubkey,
|
|
3859
|
+
recipientSaltHex: toHex2(recipientSalt),
|
|
3860
|
+
transferSaltHex: toHex2(transferSalt),
|
|
3861
|
+
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3862
|
+
finalRecipientStateJson: "",
|
|
3863
|
+
// Recipient creates their own
|
|
3864
|
+
recipientAddressJson: recipientAddressStr,
|
|
3865
|
+
nametagTokenJson
|
|
3866
|
+
};
|
|
3867
|
+
return {
|
|
3868
|
+
bundle,
|
|
3869
|
+
splitGroupId,
|
|
3870
|
+
startBackground: async () => {
|
|
3871
|
+
if (!options?.skipBackground) {
|
|
3872
|
+
await this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3873
|
+
signingService: this.signingService,
|
|
3874
|
+
tokenType: tokenToSplit.type,
|
|
3875
|
+
coinId,
|
|
3876
|
+
senderTokenId,
|
|
3877
|
+
senderSalt,
|
|
3878
|
+
onProgress: options?.onBackgroundProgress,
|
|
3879
|
+
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3880
|
+
onStorageSync: options?.onStorageSync
|
|
3881
|
+
});
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
};
|
|
3885
|
+
}
|
|
3886
|
+
/**
|
|
3887
|
+
* Execute an instant split transfer with V5 optimized flow.
|
|
3888
|
+
*
|
|
3889
|
+
* Builds the bundle via buildSplitBundle(), sends via transport,
|
|
3890
|
+
* and starts background processing.
|
|
3751
3891
|
*
|
|
3752
3892
|
* @param tokenToSplit - The SDK token to split
|
|
3753
3893
|
* @param splitAmount - Amount to send to recipient
|
|
@@ -3761,117 +3901,19 @@ var InstantSplitExecutor = class {
|
|
|
3761
3901
|
*/
|
|
3762
3902
|
async executeSplitInstant(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, transport, recipientPubkey, options) {
|
|
3763
3903
|
const startTime = performance.now();
|
|
3764
|
-
const splitGroupId = crypto.randomUUID();
|
|
3765
|
-
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
3766
|
-
console.log(`[InstantSplit] Starting V5 split for token ${tokenIdHex.slice(0, 8)}...`);
|
|
3767
3904
|
try {
|
|
3768
|
-
const
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
3774
|
-
const senderAddressRef = await import_UnmaskedPredicateReference2.UnmaskedPredicateReference.create(
|
|
3775
|
-
tokenToSplit.type,
|
|
3776
|
-
this.signingService.algorithm,
|
|
3777
|
-
this.signingService.publicKey,
|
|
3778
|
-
import_HashAlgorithm3.HashAlgorithm.SHA256
|
|
3779
|
-
);
|
|
3780
|
-
const senderAddress = await senderAddressRef.toAddress();
|
|
3781
|
-
const builder = new import_TokenSplitBuilder2.TokenSplitBuilder();
|
|
3782
|
-
const coinDataA = import_TokenCoinData2.TokenCoinData.create([[coinId, splitAmount]]);
|
|
3783
|
-
builder.createToken(
|
|
3784
|
-
recipientTokenId,
|
|
3785
|
-
tokenToSplit.type,
|
|
3786
|
-
new Uint8Array(0),
|
|
3787
|
-
coinDataA,
|
|
3788
|
-
senderAddress,
|
|
3789
|
-
// Mint to sender first, then transfer
|
|
3790
|
-
recipientSalt,
|
|
3791
|
-
null
|
|
3792
|
-
);
|
|
3793
|
-
const coinDataB = import_TokenCoinData2.TokenCoinData.create([[coinId, remainderAmount]]);
|
|
3794
|
-
builder.createToken(
|
|
3795
|
-
senderTokenId,
|
|
3796
|
-
tokenToSplit.type,
|
|
3797
|
-
new Uint8Array(0),
|
|
3798
|
-
coinDataB,
|
|
3799
|
-
senderAddress,
|
|
3800
|
-
senderSalt,
|
|
3801
|
-
null
|
|
3802
|
-
);
|
|
3803
|
-
const split = await builder.build(tokenToSplit);
|
|
3804
|
-
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
3805
|
-
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
3806
|
-
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
3807
|
-
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3808
|
-
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3809
|
-
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3810
|
-
}
|
|
3811
|
-
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3812
|
-
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, burnCommitment);
|
|
3813
|
-
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3814
|
-
const burnDuration = performance.now() - startTime;
|
|
3815
|
-
console.log(`[InstantSplit] Burn proof received in ${burnDuration.toFixed(0)}ms`);
|
|
3816
|
-
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3817
|
-
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3818
|
-
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3819
|
-
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3820
|
-
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3821
|
-
const recipientMintCommitment = mintCommitments.find(
|
|
3822
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3823
|
-
);
|
|
3824
|
-
const senderMintCommitment = mintCommitments.find(
|
|
3825
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3826
|
-
);
|
|
3827
|
-
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3828
|
-
throw new Error("Failed to find expected mint commitments");
|
|
3829
|
-
}
|
|
3830
|
-
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3831
|
-
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3832
|
-
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3833
|
-
recipientMintCommitment.transactionData,
|
|
3905
|
+
const buildResult = await this.buildSplitBundle(
|
|
3906
|
+
tokenToSplit,
|
|
3907
|
+
splitAmount,
|
|
3908
|
+
remainderAmount,
|
|
3909
|
+
coinIdHex,
|
|
3834
3910
|
recipientAddress,
|
|
3835
|
-
|
|
3836
|
-
this.signingService
|
|
3911
|
+
options
|
|
3837
3912
|
);
|
|
3838
|
-
|
|
3839
|
-
recipientTokenId,
|
|
3840
|
-
tokenToSplit.type,
|
|
3841
|
-
this.signingService,
|
|
3842
|
-
import_HashAlgorithm3.HashAlgorithm.SHA256,
|
|
3843
|
-
recipientSalt
|
|
3844
|
-
);
|
|
3845
|
-
const mintedState = new import_TokenState3.TokenState(mintedPredicate, null);
|
|
3846
|
-
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3913
|
+
console.log("[InstantSplit] Sending via transport...");
|
|
3847
3914
|
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3848
|
-
let nametagTokenJson;
|
|
3849
|
-
const recipientAddressStr = recipientAddress.toString();
|
|
3850
|
-
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3851
|
-
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3852
|
-
}
|
|
3853
|
-
const bundle = {
|
|
3854
|
-
version: "5.0",
|
|
3855
|
-
type: "INSTANT_SPLIT",
|
|
3856
|
-
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3857
|
-
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3858
|
-
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3859
|
-
amount: splitAmount.toString(),
|
|
3860
|
-
coinId: coinIdHex,
|
|
3861
|
-
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3862
|
-
splitGroupId,
|
|
3863
|
-
senderPubkey,
|
|
3864
|
-
recipientSaltHex: toHex2(recipientSalt),
|
|
3865
|
-
transferSaltHex: toHex2(transferSalt),
|
|
3866
|
-
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3867
|
-
finalRecipientStateJson: "",
|
|
3868
|
-
// Recipient creates their own
|
|
3869
|
-
recipientAddressJson: recipientAddressStr,
|
|
3870
|
-
nametagTokenJson
|
|
3871
|
-
};
|
|
3872
|
-
console.log("[InstantSplit] Step 6: Sending via transport...");
|
|
3873
3915
|
const nostrEventId = await transport.sendTokenTransfer(recipientPubkey, {
|
|
3874
|
-
token: JSON.stringify(bundle),
|
|
3916
|
+
token: JSON.stringify(buildResult.bundle),
|
|
3875
3917
|
proof: null,
|
|
3876
3918
|
// Proof is included in the bundle
|
|
3877
3919
|
memo: options?.memo,
|
|
@@ -3882,25 +3924,13 @@ var InstantSplitExecutor = class {
|
|
|
3882
3924
|
const criticalPathDuration = performance.now() - startTime;
|
|
3883
3925
|
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3884
3926
|
options?.onNostrDelivered?.(nostrEventId);
|
|
3885
|
-
|
|
3886
|
-
if (!options?.skipBackground) {
|
|
3887
|
-
backgroundPromise = this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3888
|
-
signingService: this.signingService,
|
|
3889
|
-
tokenType: tokenToSplit.type,
|
|
3890
|
-
coinId,
|
|
3891
|
-
senderTokenId,
|
|
3892
|
-
senderSalt,
|
|
3893
|
-
onProgress: options?.onBackgroundProgress,
|
|
3894
|
-
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3895
|
-
onStorageSync: options?.onStorageSync
|
|
3896
|
-
});
|
|
3897
|
-
}
|
|
3927
|
+
const backgroundPromise = buildResult.startBackground();
|
|
3898
3928
|
return {
|
|
3899
3929
|
success: true,
|
|
3900
3930
|
nostrEventId,
|
|
3901
|
-
splitGroupId,
|
|
3931
|
+
splitGroupId: buildResult.splitGroupId,
|
|
3902
3932
|
criticalPathDurationMs: criticalPathDuration,
|
|
3903
|
-
backgroundStarted:
|
|
3933
|
+
backgroundStarted: true,
|
|
3904
3934
|
backgroundPromise
|
|
3905
3935
|
};
|
|
3906
3936
|
} catch (error) {
|
|
@@ -3909,7 +3939,6 @@ var InstantSplitExecutor = class {
|
|
|
3909
3939
|
console.error(`[InstantSplit] Failed after ${duration.toFixed(0)}ms:`, error);
|
|
3910
3940
|
return {
|
|
3911
3941
|
success: false,
|
|
3912
|
-
splitGroupId,
|
|
3913
3942
|
criticalPathDurationMs: duration,
|
|
3914
3943
|
error: errorMessage,
|
|
3915
3944
|
backgroundStarted: false
|
|
@@ -4114,6 +4143,11 @@ function isInstantSplitBundleV4(obj) {
|
|
|
4114
4143
|
function isInstantSplitBundleV5(obj) {
|
|
4115
4144
|
return isInstantSplitBundle(obj) && obj.version === "5.0";
|
|
4116
4145
|
}
|
|
4146
|
+
function isCombinedTransferBundleV6(obj) {
|
|
4147
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
4148
|
+
const b = obj;
|
|
4149
|
+
return b.version === "6.0" && b.type === "COMBINED_TRANSFER";
|
|
4150
|
+
}
|
|
4117
4151
|
|
|
4118
4152
|
// modules/payments/InstantSplitProcessor.ts
|
|
4119
4153
|
function fromHex3(hex) {
|
|
@@ -4756,6 +4790,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4756
4790
|
// Poll every 2s
|
|
4757
4791
|
static PROOF_POLLING_MAX_ATTEMPTS = 30;
|
|
4758
4792
|
// Max 30 attempts (~60s)
|
|
4793
|
+
// Periodic retry for resolveUnconfirmed (V5 lazy finalization)
|
|
4794
|
+
resolveUnconfirmedTimer = null;
|
|
4795
|
+
static RESOLVE_UNCONFIRMED_INTERVAL_MS = 1e4;
|
|
4796
|
+
// Retry every 10s
|
|
4797
|
+
// Guard: ensure load() completes before processing incoming bundles
|
|
4798
|
+
loadedPromise = null;
|
|
4799
|
+
loaded = false;
|
|
4800
|
+
// Persistent dedup: tracks splitGroupIds that have been fully processed.
|
|
4801
|
+
// Survives page reloads via KV storage so Nostr re-deliveries are ignored
|
|
4802
|
+
// even when the confirmed token's in-memory ID differs from v5split_{id}.
|
|
4803
|
+
processedSplitGroupIds = /* @__PURE__ */ new Set();
|
|
4804
|
+
// Persistent dedup: tracks V6 combined transfer IDs that have been processed.
|
|
4805
|
+
processedCombinedTransferIds = /* @__PURE__ */ new Set();
|
|
4759
4806
|
// Storage event subscriptions (push-based sync)
|
|
4760
4807
|
storageEventUnsubscribers = [];
|
|
4761
4808
|
syncDebounceTimer = null;
|
|
@@ -4841,31 +4888,53 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4841
4888
|
*/
|
|
4842
4889
|
async load() {
|
|
4843
4890
|
this.ensureInitialized();
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4891
|
+
const doLoad = async () => {
|
|
4892
|
+
await TokenRegistry.waitForReady();
|
|
4893
|
+
const providers = this.getTokenStorageProviders();
|
|
4894
|
+
for (const [id, provider] of providers) {
|
|
4895
|
+
try {
|
|
4896
|
+
const result = await provider.load();
|
|
4897
|
+
if (result.success && result.data) {
|
|
4898
|
+
this.loadFromStorageData(result.data);
|
|
4899
|
+
this.log(`Loaded metadata from provider ${id}`);
|
|
4900
|
+
break;
|
|
4901
|
+
}
|
|
4902
|
+
} catch (err) {
|
|
4903
|
+
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4853
4904
|
}
|
|
4854
|
-
} catch (err) {
|
|
4855
|
-
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4856
4905
|
}
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4906
|
+
for (const [id, token] of this.tokens) {
|
|
4907
|
+
try {
|
|
4908
|
+
if (token.sdkData) {
|
|
4909
|
+
const data = JSON.parse(token.sdkData);
|
|
4910
|
+
if (data?._placeholder) {
|
|
4911
|
+
this.tokens.delete(id);
|
|
4912
|
+
console.log(`[Payments] Removed stale placeholder token: ${id}`);
|
|
4913
|
+
}
|
|
4914
|
+
}
|
|
4915
|
+
} catch {
|
|
4916
|
+
}
|
|
4865
4917
|
}
|
|
4866
|
-
|
|
4918
|
+
const loadedTokens = Array.from(this.tokens.values()).map((t) => `${t.id.slice(0, 12)}(${t.status})`);
|
|
4919
|
+
console.log(`[Payments][DEBUG] load(): from TXF providers: ${this.tokens.size} tokens [${loadedTokens.join(", ")}]`);
|
|
4920
|
+
await this.loadPendingV5Tokens();
|
|
4921
|
+
await this.loadProcessedSplitGroupIds();
|
|
4922
|
+
await this.loadProcessedCombinedTransferIds();
|
|
4923
|
+
await this.loadHistory();
|
|
4924
|
+
const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
|
|
4925
|
+
if (pending2) {
|
|
4926
|
+
const transfers = JSON.parse(pending2);
|
|
4927
|
+
for (const transfer of transfers) {
|
|
4928
|
+
this.pendingTransfers.set(transfer.id, transfer);
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
this.loaded = true;
|
|
4932
|
+
};
|
|
4933
|
+
this.loadedPromise = doLoad();
|
|
4934
|
+
await this.loadedPromise;
|
|
4867
4935
|
this.resolveUnconfirmed().catch(() => {
|
|
4868
4936
|
});
|
|
4937
|
+
this.scheduleResolveUnconfirmed();
|
|
4869
4938
|
}
|
|
4870
4939
|
/**
|
|
4871
4940
|
* Cleanup all subscriptions, polling jobs, and pending resolvers.
|
|
@@ -4884,6 +4953,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4884
4953
|
this.paymentRequestResponseHandlers.clear();
|
|
4885
4954
|
this.stopProofPolling();
|
|
4886
4955
|
this.proofPollingJobs.clear();
|
|
4956
|
+
this.stopResolveUnconfirmedPolling();
|
|
4887
4957
|
for (const [, resolver] of this.pendingResponseResolvers) {
|
|
4888
4958
|
clearTimeout(resolver.timeout);
|
|
4889
4959
|
resolver.reject(new Error("Module destroyed"));
|
|
@@ -4941,12 +5011,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4941
5011
|
token.status = "transferring";
|
|
4942
5012
|
this.tokens.set(token.id, token);
|
|
4943
5013
|
}
|
|
5014
|
+
await this.save();
|
|
4944
5015
|
await this.saveToOutbox(result, recipientPubkey);
|
|
4945
5016
|
result.status = "submitted";
|
|
4946
5017
|
const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
|
|
4947
5018
|
const transferMode = request.transferMode ?? "instant";
|
|
4948
|
-
if (
|
|
4949
|
-
if (
|
|
5019
|
+
if (transferMode === "conservative") {
|
|
5020
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4950
5021
|
this.log("Executing conservative split...");
|
|
4951
5022
|
const splitExecutor = new TokenSplitExecutor({
|
|
4952
5023
|
stateTransitionClient: stClient,
|
|
@@ -4990,27 +5061,59 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4990
5061
|
requestIdHex: splitRequestIdHex
|
|
4991
5062
|
});
|
|
4992
5063
|
this.log(`Conservative split transfer completed`);
|
|
4993
|
-
}
|
|
4994
|
-
|
|
4995
|
-
const
|
|
5064
|
+
}
|
|
5065
|
+
for (const tokenWithAmount of splitPlan.tokensToTransferDirectly) {
|
|
5066
|
+
const token = tokenWithAmount.uiToken;
|
|
5067
|
+
const commitment = await this.createSdkCommitment(token, recipientAddress, signingService);
|
|
5068
|
+
console.log(`[Payments] CONSERVATIVE: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
5069
|
+
const submitResponse = await stClient.submitTransferCommitment(commitment);
|
|
5070
|
+
if (submitResponse.status !== "SUCCESS" && submitResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5071
|
+
throw new Error(`Transfer commitment failed: ${submitResponse.status}`);
|
|
5072
|
+
}
|
|
5073
|
+
const inclusionProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment);
|
|
5074
|
+
const transferTx = commitment.toTransaction(inclusionProof);
|
|
5075
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
5076
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
5077
|
+
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
5078
|
+
memo: request.memo
|
|
5079
|
+
});
|
|
5080
|
+
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
5081
|
+
const requestIdBytes = commitment.requestId;
|
|
5082
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
5083
|
+
result.tokenTransfers.push({
|
|
5084
|
+
sourceTokenId: token.id,
|
|
5085
|
+
method: "direct",
|
|
5086
|
+
requestIdHex
|
|
5087
|
+
});
|
|
5088
|
+
this.log(`Token ${token.id} sent via CONSERVATIVE, requestId: ${requestIdHex}`);
|
|
5089
|
+
await this.removeToken(token.id);
|
|
5090
|
+
}
|
|
5091
|
+
} else {
|
|
5092
|
+
const devMode = this.deps.oracle.isDevMode?.() ?? false;
|
|
5093
|
+
const senderPubkey = this.deps.identity.chainPubkey;
|
|
5094
|
+
let changeTokenPlaceholderId = null;
|
|
5095
|
+
let builtSplit = null;
|
|
5096
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
5097
|
+
this.log("Building instant split bundle...");
|
|
4996
5098
|
const executor = new InstantSplitExecutor({
|
|
4997
5099
|
stateTransitionClient: stClient,
|
|
4998
5100
|
trustBase,
|
|
4999
5101
|
signingService,
|
|
5000
5102
|
devMode
|
|
5001
5103
|
});
|
|
5002
|
-
|
|
5104
|
+
builtSplit = await executor.buildSplitBundle(
|
|
5003
5105
|
splitPlan.tokenToSplit.sdkToken,
|
|
5004
5106
|
splitPlan.splitAmount,
|
|
5005
5107
|
splitPlan.remainderAmount,
|
|
5006
5108
|
splitPlan.coinId,
|
|
5007
5109
|
recipientAddress,
|
|
5008
|
-
this.deps.transport,
|
|
5009
|
-
recipientPubkey,
|
|
5010
5110
|
{
|
|
5011
5111
|
memo: request.memo,
|
|
5012
5112
|
onChangeTokenCreated: async (changeToken) => {
|
|
5013
5113
|
const changeTokenData = changeToken.toJSON();
|
|
5114
|
+
if (changeTokenPlaceholderId && this.tokens.has(changeTokenPlaceholderId)) {
|
|
5115
|
+
this.tokens.delete(changeTokenPlaceholderId);
|
|
5116
|
+
}
|
|
5014
5117
|
const uiToken = {
|
|
5015
5118
|
id: crypto.randomUUID(),
|
|
5016
5119
|
coinId: request.coinId,
|
|
@@ -5033,65 +5136,103 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5033
5136
|
}
|
|
5034
5137
|
}
|
|
5035
5138
|
);
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
this.
|
|
5041
|
-
|
|
5139
|
+
this.log(`Split bundle built: splitGroupId=${builtSplit.splitGroupId}`);
|
|
5140
|
+
}
|
|
5141
|
+
const directCommitments = await Promise.all(
|
|
5142
|
+
splitPlan.tokensToTransferDirectly.map(
|
|
5143
|
+
(tw) => this.createSdkCommitment(tw.uiToken, recipientAddress, signingService)
|
|
5144
|
+
)
|
|
5145
|
+
);
|
|
5146
|
+
const directTokenEntries = splitPlan.tokensToTransferDirectly.map(
|
|
5147
|
+
(tw, i) => ({
|
|
5148
|
+
sourceToken: JSON.stringify(tw.sdkToken.toJSON()),
|
|
5149
|
+
commitmentData: JSON.stringify(directCommitments[i].toJSON()),
|
|
5150
|
+
amount: tw.uiToken.amount,
|
|
5151
|
+
coinId: tw.uiToken.coinId,
|
|
5152
|
+
tokenId: extractTokenIdFromSdkData(tw.uiToken.sdkData) || void 0
|
|
5153
|
+
})
|
|
5154
|
+
);
|
|
5155
|
+
const combinedBundle = {
|
|
5156
|
+
version: "6.0",
|
|
5157
|
+
type: "COMBINED_TRANSFER",
|
|
5158
|
+
transferId: result.id,
|
|
5159
|
+
splitBundle: builtSplit?.bundle ?? null,
|
|
5160
|
+
directTokens: directTokenEntries,
|
|
5161
|
+
totalAmount: request.amount.toString(),
|
|
5162
|
+
coinId: request.coinId,
|
|
5163
|
+
senderPubkey,
|
|
5164
|
+
memo: request.memo
|
|
5165
|
+
};
|
|
5166
|
+
console.log(
|
|
5167
|
+
`[Payments] Sending V6 combined bundle: transfer=${result.id.slice(0, 8)}... split=${!!builtSplit} direct=${directTokenEntries.length}`
|
|
5168
|
+
);
|
|
5169
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
5170
|
+
token: JSON.stringify(combinedBundle),
|
|
5171
|
+
proof: null,
|
|
5172
|
+
memo: request.memo,
|
|
5173
|
+
sender: { transportPubkey: senderPubkey }
|
|
5174
|
+
});
|
|
5175
|
+
console.log(`[Payments] V6 combined bundle sent successfully`);
|
|
5176
|
+
if (builtSplit) {
|
|
5177
|
+
const bgPromise = builtSplit.startBackground();
|
|
5178
|
+
this.pendingBackgroundTasks.push(bgPromise);
|
|
5179
|
+
}
|
|
5180
|
+
if (builtSplit && splitPlan.remainderAmount) {
|
|
5181
|
+
changeTokenPlaceholderId = crypto.randomUUID();
|
|
5182
|
+
const placeholder = {
|
|
5183
|
+
id: changeTokenPlaceholderId,
|
|
5184
|
+
coinId: request.coinId,
|
|
5185
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
5186
|
+
name: this.getCoinName(request.coinId),
|
|
5187
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
5188
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
5189
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
5190
|
+
status: "transferring",
|
|
5191
|
+
createdAt: Date.now(),
|
|
5192
|
+
updatedAt: Date.now(),
|
|
5193
|
+
sdkData: JSON.stringify({ _placeholder: true })
|
|
5194
|
+
};
|
|
5195
|
+
this.tokens.set(placeholder.id, placeholder);
|
|
5196
|
+
this.log(`Placeholder change token created: ${placeholder.id} (${placeholder.amount})`);
|
|
5197
|
+
}
|
|
5198
|
+
for (const commitment of directCommitments) {
|
|
5199
|
+
stClient.submitTransferCommitment(commitment).catch(
|
|
5200
|
+
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
5201
|
+
);
|
|
5202
|
+
}
|
|
5203
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
5042
5204
|
await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
|
|
5043
5205
|
result.tokenTransfers.push({
|
|
5044
5206
|
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
5045
5207
|
method: "split",
|
|
5046
|
-
splitGroupId:
|
|
5047
|
-
nostrEventId: instantResult.nostrEventId
|
|
5208
|
+
splitGroupId: builtSplit.splitGroupId
|
|
5048
5209
|
});
|
|
5049
|
-
this.log(`Instant split transfer completed`);
|
|
5050
5210
|
}
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
}
|
|
5061
|
-
const inclusionProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment);
|
|
5062
|
-
const transferTx = commitment.toTransaction(inclusionProof);
|
|
5063
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
5064
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
5065
|
-
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
5066
|
-
memo: request.memo
|
|
5067
|
-
});
|
|
5068
|
-
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
5069
|
-
} else {
|
|
5070
|
-
console.log(`[Payments] NOSTR-FIRST: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
5071
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
5072
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
5073
|
-
commitmentData: JSON.stringify(commitment.toJSON()),
|
|
5074
|
-
memo: request.memo
|
|
5211
|
+
for (let i = 0; i < splitPlan.tokensToTransferDirectly.length; i++) {
|
|
5212
|
+
const token = splitPlan.tokensToTransferDirectly[i].uiToken;
|
|
5213
|
+
const commitment = directCommitments[i];
|
|
5214
|
+
const requestIdBytes = commitment.requestId;
|
|
5215
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
5216
|
+
result.tokenTransfers.push({
|
|
5217
|
+
sourceTokenId: token.id,
|
|
5218
|
+
method: "direct",
|
|
5219
|
+
requestIdHex
|
|
5075
5220
|
});
|
|
5076
|
-
|
|
5077
|
-
stClient.submitTransferCommitment(commitment).catch(
|
|
5078
|
-
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
5079
|
-
);
|
|
5221
|
+
await this.removeToken(token.id);
|
|
5080
5222
|
}
|
|
5081
|
-
|
|
5082
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
5083
|
-
result.tokenTransfers.push({
|
|
5084
|
-
sourceTokenId: token.id,
|
|
5085
|
-
method: "direct",
|
|
5086
|
-
requestIdHex
|
|
5087
|
-
});
|
|
5088
|
-
this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
|
|
5089
|
-
await this.removeToken(token.id);
|
|
5223
|
+
this.log(`V6 combined transfer completed`);
|
|
5090
5224
|
}
|
|
5091
5225
|
result.status = "delivered";
|
|
5092
5226
|
await this.save();
|
|
5093
5227
|
await this.removeFromOutbox(result.id);
|
|
5094
5228
|
result.status = "completed";
|
|
5229
|
+
const tokenMap = new Map(result.tokens.map((t) => [t.id, t]));
|
|
5230
|
+
const sentTokenIds = result.tokenTransfers.map((tt) => ({
|
|
5231
|
+
id: tt.sourceTokenId,
|
|
5232
|
+
// For split tokens, use splitAmount (the portion sent), not the original token amount
|
|
5233
|
+
amount: tt.method === "split" ? splitPlan.splitAmount?.toString() || "0" : tokenMap.get(tt.sourceTokenId)?.amount || "0",
|
|
5234
|
+
source: tt.method === "split" ? "split" : "direct"
|
|
5235
|
+
}));
|
|
5095
5236
|
const sentTokenId = result.tokens[0] ? extractTokenIdFromSdkData(result.tokens[0].sdkData) : void 0;
|
|
5096
5237
|
await this.addToHistory({
|
|
5097
5238
|
type: "SENT",
|
|
@@ -5104,7 +5245,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5104
5245
|
recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
|
|
5105
5246
|
memo: request.memo,
|
|
5106
5247
|
transferId: result.id,
|
|
5107
|
-
tokenId: sentTokenId || void 0
|
|
5248
|
+
tokenId: sentTokenId || void 0,
|
|
5249
|
+
tokenIds: sentTokenIds.length > 0 ? sentTokenIds : void 0
|
|
5108
5250
|
});
|
|
5109
5251
|
this.deps.emitEvent("transfer:confirmed", result);
|
|
5110
5252
|
return result;
|
|
@@ -5274,6 +5416,267 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5274
5416
|
};
|
|
5275
5417
|
}
|
|
5276
5418
|
}
|
|
5419
|
+
// ===========================================================================
|
|
5420
|
+
// Shared Helpers for V5 and V6 Receiver Processing
|
|
5421
|
+
// ===========================================================================
|
|
5422
|
+
/**
|
|
5423
|
+
* Save a V5 split bundle as an unconfirmed token (shared by V5 standalone and V6 combined).
|
|
5424
|
+
* Returns the created UI token, or null if deduped.
|
|
5425
|
+
*
|
|
5426
|
+
* @param deferPersistence - If true, skip addToken/save calls (caller batches them).
|
|
5427
|
+
* The token is still added to the in-memory map for dedup; caller must call save().
|
|
5428
|
+
*/
|
|
5429
|
+
async saveUnconfirmedV5Token(bundle, senderPubkey, deferPersistence = false) {
|
|
5430
|
+
const deterministicId = `v5split_${bundle.splitGroupId}`;
|
|
5431
|
+
if (this.tokens.has(deterministicId) || this.processedSplitGroupIds.has(bundle.splitGroupId)) {
|
|
5432
|
+
console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
|
|
5433
|
+
return null;
|
|
5434
|
+
}
|
|
5435
|
+
const registry = TokenRegistry.getInstance();
|
|
5436
|
+
const pendingData = {
|
|
5437
|
+
type: "v5_bundle",
|
|
5438
|
+
stage: "RECEIVED",
|
|
5439
|
+
bundleJson: JSON.stringify(bundle),
|
|
5440
|
+
senderPubkey,
|
|
5441
|
+
savedAt: Date.now(),
|
|
5442
|
+
attemptCount: 0
|
|
5443
|
+
};
|
|
5444
|
+
const uiToken = {
|
|
5445
|
+
id: deterministicId,
|
|
5446
|
+
coinId: bundle.coinId,
|
|
5447
|
+
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
5448
|
+
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
5449
|
+
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
5450
|
+
amount: bundle.amount,
|
|
5451
|
+
status: "submitted",
|
|
5452
|
+
// UNCONFIRMED
|
|
5453
|
+
createdAt: Date.now(),
|
|
5454
|
+
updatedAt: Date.now(),
|
|
5455
|
+
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
5456
|
+
};
|
|
5457
|
+
this.processedSplitGroupIds.add(bundle.splitGroupId);
|
|
5458
|
+
if (deferPersistence) {
|
|
5459
|
+
this.tokens.set(uiToken.id, uiToken);
|
|
5460
|
+
} else {
|
|
5461
|
+
await this.addToken(uiToken);
|
|
5462
|
+
await this.saveProcessedSplitGroupIds();
|
|
5463
|
+
}
|
|
5464
|
+
return uiToken;
|
|
5465
|
+
}
|
|
5466
|
+
/**
|
|
5467
|
+
* Save a commitment-only (NOSTR-FIRST) token and start proof polling.
|
|
5468
|
+
* Shared by standalone NOSTR-FIRST handler and V6 combined handler.
|
|
5469
|
+
* Returns the created UI token, or null if deduped/tombstoned.
|
|
5470
|
+
*
|
|
5471
|
+
* @param deferPersistence - If true, skip save() and commitment submission
|
|
5472
|
+
* (caller batches them). Token is added to in-memory map + proof polling is queued.
|
|
5473
|
+
* @param skipGenesisDedup - If true, skip genesis-ID-only dedup. V6 handler sets this
|
|
5474
|
+
* because bundle-level dedup protects against replays, and split children share genesis IDs.
|
|
5475
|
+
*/
|
|
5476
|
+
async saveCommitmentOnlyToken(sourceTokenInput, commitmentInput, senderPubkey, deferPersistence = false, skipGenesisDedup = false) {
|
|
5477
|
+
const tokenInfo = await parseTokenInfo(sourceTokenInput);
|
|
5478
|
+
const sdkData = typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput);
|
|
5479
|
+
const nostrTokenId = extractTokenIdFromSdkData(sdkData);
|
|
5480
|
+
const nostrStateHash = extractStateHashFromSdkData(sdkData);
|
|
5481
|
+
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
5482
|
+
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
5483
|
+
return null;
|
|
5484
|
+
}
|
|
5485
|
+
if (nostrTokenId) {
|
|
5486
|
+
for (const existing of this.tokens.values()) {
|
|
5487
|
+
const existingTokenId = extractTokenIdFromSdkData(existing.sdkData);
|
|
5488
|
+
if (existingTokenId !== nostrTokenId) continue;
|
|
5489
|
+
const existingStateHash = extractStateHashFromSdkData(existing.sdkData);
|
|
5490
|
+
if (nostrStateHash && existingStateHash === nostrStateHash) {
|
|
5491
|
+
console.log(
|
|
5492
|
+
`[Payments] NOSTR-FIRST: Skipping duplicate token state ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`
|
|
5493
|
+
);
|
|
5494
|
+
return null;
|
|
5495
|
+
}
|
|
5496
|
+
if (!skipGenesisDedup) {
|
|
5497
|
+
console.log(
|
|
5498
|
+
`[Payments] NOSTR-FIRST: Skipping replay of finalized token ${nostrTokenId.slice(0, 8)}...`
|
|
5499
|
+
);
|
|
5500
|
+
return null;
|
|
5501
|
+
}
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
const token = {
|
|
5505
|
+
id: crypto.randomUUID(),
|
|
5506
|
+
coinId: tokenInfo.coinId,
|
|
5507
|
+
symbol: tokenInfo.symbol,
|
|
5508
|
+
name: tokenInfo.name,
|
|
5509
|
+
decimals: tokenInfo.decimals,
|
|
5510
|
+
iconUrl: tokenInfo.iconUrl,
|
|
5511
|
+
amount: tokenInfo.amount,
|
|
5512
|
+
status: "submitted",
|
|
5513
|
+
// NOSTR-FIRST: unconfirmed until proof
|
|
5514
|
+
createdAt: Date.now(),
|
|
5515
|
+
updatedAt: Date.now(),
|
|
5516
|
+
sdkData
|
|
5517
|
+
};
|
|
5518
|
+
this.tokens.set(token.id, token);
|
|
5519
|
+
if (!deferPersistence) {
|
|
5520
|
+
await this.save();
|
|
5521
|
+
}
|
|
5522
|
+
try {
|
|
5523
|
+
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
|
|
5524
|
+
const requestIdBytes = commitment.requestId;
|
|
5525
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
5526
|
+
if (!deferPersistence) {
|
|
5527
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5528
|
+
if (stClient) {
|
|
5529
|
+
const response = await stClient.submitTransferCommitment(commitment);
|
|
5530
|
+
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
5531
|
+
}
|
|
5532
|
+
}
|
|
5533
|
+
this.addProofPollingJob({
|
|
5534
|
+
tokenId: token.id,
|
|
5535
|
+
requestIdHex,
|
|
5536
|
+
commitmentJson: JSON.stringify(commitmentInput),
|
|
5537
|
+
startedAt: Date.now(),
|
|
5538
|
+
attemptCount: 0,
|
|
5539
|
+
lastAttemptAt: 0,
|
|
5540
|
+
onProofReceived: async (tokenId) => {
|
|
5541
|
+
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
|
|
5542
|
+
}
|
|
5543
|
+
});
|
|
5544
|
+
} catch (err) {
|
|
5545
|
+
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
5546
|
+
}
|
|
5547
|
+
return token;
|
|
5548
|
+
}
|
|
5549
|
+
// ===========================================================================
|
|
5550
|
+
// Combined Transfer V6 — Receiver
|
|
5551
|
+
// ===========================================================================
|
|
5552
|
+
/**
|
|
5553
|
+
* Process a received COMBINED_TRANSFER V6 bundle.
|
|
5554
|
+
*
|
|
5555
|
+
* Unpacks a single Nostr message into its component tokens:
|
|
5556
|
+
* - Optional V5 split bundle (saved as unconfirmed, resolved lazily)
|
|
5557
|
+
* - Zero or more direct tokens (saved as unconfirmed, proof-polled)
|
|
5558
|
+
*
|
|
5559
|
+
* Emits ONE transfer:incoming event and records ONE history entry.
|
|
5560
|
+
*/
|
|
5561
|
+
async processCombinedTransferBundle(bundle, senderPubkey) {
|
|
5562
|
+
this.ensureInitialized();
|
|
5563
|
+
if (!this.loaded && this.loadedPromise) {
|
|
5564
|
+
await this.loadedPromise;
|
|
5565
|
+
}
|
|
5566
|
+
if (this.processedCombinedTransferIds.has(bundle.transferId)) {
|
|
5567
|
+
console.log(`[Payments] V6 combined transfer ${bundle.transferId.slice(0, 12)}... already processed, skipping`);
|
|
5568
|
+
return;
|
|
5569
|
+
}
|
|
5570
|
+
console.log(
|
|
5571
|
+
`[Payments] Processing V6 combined transfer ${bundle.transferId.slice(0, 12)}... (split=${!!bundle.splitBundle}, direct=${bundle.directTokens.length})`
|
|
5572
|
+
);
|
|
5573
|
+
const allTokens = [];
|
|
5574
|
+
const tokenBreakdown = [];
|
|
5575
|
+
const parsedDirectEntries = bundle.directTokens.map((entry) => ({
|
|
5576
|
+
sourceToken: typeof entry.sourceToken === "string" ? JSON.parse(entry.sourceToken) : entry.sourceToken,
|
|
5577
|
+
commitment: typeof entry.commitmentData === "string" ? JSON.parse(entry.commitmentData) : entry.commitmentData
|
|
5578
|
+
}));
|
|
5579
|
+
if (bundle.splitBundle) {
|
|
5580
|
+
const splitToken = await this.saveUnconfirmedV5Token(bundle.splitBundle, senderPubkey, true);
|
|
5581
|
+
if (splitToken) {
|
|
5582
|
+
allTokens.push(splitToken);
|
|
5583
|
+
tokenBreakdown.push({ id: splitToken.id, amount: splitToken.amount, source: "split" });
|
|
5584
|
+
} else {
|
|
5585
|
+
console.warn(`[Payments] V6: split token was deduped/failed \u2014 amount=${bundle.splitBundle.amount}`);
|
|
5586
|
+
}
|
|
5587
|
+
}
|
|
5588
|
+
const directResults = await Promise.all(
|
|
5589
|
+
parsedDirectEntries.map(
|
|
5590
|
+
({ sourceToken, commitment }) => this.saveCommitmentOnlyToken(sourceToken, commitment, senderPubkey, true, true)
|
|
5591
|
+
)
|
|
5592
|
+
);
|
|
5593
|
+
for (let i = 0; i < directResults.length; i++) {
|
|
5594
|
+
const token = directResults[i];
|
|
5595
|
+
if (token) {
|
|
5596
|
+
allTokens.push(token);
|
|
5597
|
+
tokenBreakdown.push({ id: token.id, amount: token.amount, source: "direct" });
|
|
5598
|
+
} else {
|
|
5599
|
+
const entry = bundle.directTokens[i];
|
|
5600
|
+
console.warn(
|
|
5601
|
+
`[Payments] V6: direct token #${i} dropped (amount=${entry.amount}, tokenId=${entry.tokenId?.slice(0, 12) ?? "N/A"})`
|
|
5602
|
+
);
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
if (allTokens.length === 0) {
|
|
5606
|
+
console.log(`[Payments] V6 combined transfer: all tokens deduped, nothing to save`);
|
|
5607
|
+
return;
|
|
5608
|
+
}
|
|
5609
|
+
this.processedCombinedTransferIds.add(bundle.transferId);
|
|
5610
|
+
const [senderInfo] = await Promise.all([
|
|
5611
|
+
this.resolveSenderInfo(senderPubkey),
|
|
5612
|
+
this.save(),
|
|
5613
|
+
this.saveProcessedCombinedTransferIds(),
|
|
5614
|
+
...bundle.splitBundle ? [this.saveProcessedSplitGroupIds()] : []
|
|
5615
|
+
]);
|
|
5616
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5617
|
+
if (stClient) {
|
|
5618
|
+
for (const { commitment } of parsedDirectEntries) {
|
|
5619
|
+
import_TransferCommitment4.TransferCommitment.fromJSON(commitment).then(
|
|
5620
|
+
(c) => stClient.submitTransferCommitment(c)
|
|
5621
|
+
).catch(
|
|
5622
|
+
(err) => console.error("[Payments] V6 background commitment submit failed:", err)
|
|
5623
|
+
);
|
|
5624
|
+
}
|
|
5625
|
+
}
|
|
5626
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
5627
|
+
id: bundle.transferId,
|
|
5628
|
+
senderPubkey,
|
|
5629
|
+
senderNametag: senderInfo.senderNametag,
|
|
5630
|
+
tokens: allTokens,
|
|
5631
|
+
memo: bundle.memo,
|
|
5632
|
+
receivedAt: Date.now()
|
|
5633
|
+
});
|
|
5634
|
+
const actualAmount = allTokens.reduce((sum, t) => sum + BigInt(t.amount || "0"), 0n).toString();
|
|
5635
|
+
await this.addToHistory({
|
|
5636
|
+
type: "RECEIVED",
|
|
5637
|
+
amount: actualAmount,
|
|
5638
|
+
coinId: bundle.coinId,
|
|
5639
|
+
symbol: allTokens[0]?.symbol || bundle.coinId,
|
|
5640
|
+
timestamp: Date.now(),
|
|
5641
|
+
senderPubkey,
|
|
5642
|
+
...senderInfo,
|
|
5643
|
+
memo: bundle.memo,
|
|
5644
|
+
transferId: bundle.transferId,
|
|
5645
|
+
tokenId: allTokens[0]?.id,
|
|
5646
|
+
tokenIds: tokenBreakdown
|
|
5647
|
+
});
|
|
5648
|
+
if (bundle.splitBundle) {
|
|
5649
|
+
this.resolveUnconfirmed().catch(() => {
|
|
5650
|
+
});
|
|
5651
|
+
this.scheduleResolveUnconfirmed();
|
|
5652
|
+
}
|
|
5653
|
+
}
|
|
5654
|
+
/**
|
|
5655
|
+
* Persist processed combined transfer IDs to KV storage.
|
|
5656
|
+
*/
|
|
5657
|
+
async saveProcessedCombinedTransferIds() {
|
|
5658
|
+
const ids = Array.from(this.processedCombinedTransferIds);
|
|
5659
|
+
if (ids.length > 0) {
|
|
5660
|
+
await this.deps.storage.set(
|
|
5661
|
+
STORAGE_KEYS_ADDRESS.PROCESSED_COMBINED_TRANSFER_IDS,
|
|
5662
|
+
JSON.stringify(ids)
|
|
5663
|
+
);
|
|
5664
|
+
}
|
|
5665
|
+
}
|
|
5666
|
+
/**
|
|
5667
|
+
* Load processed combined transfer IDs from KV storage.
|
|
5668
|
+
*/
|
|
5669
|
+
async loadProcessedCombinedTransferIds() {
|
|
5670
|
+
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PROCESSED_COMBINED_TRANSFER_IDS);
|
|
5671
|
+
if (!data) return;
|
|
5672
|
+
try {
|
|
5673
|
+
const ids = JSON.parse(data);
|
|
5674
|
+
for (const id of ids) {
|
|
5675
|
+
this.processedCombinedTransferIds.add(id);
|
|
5676
|
+
}
|
|
5677
|
+
} catch {
|
|
5678
|
+
}
|
|
5679
|
+
}
|
|
5277
5680
|
/**
|
|
5278
5681
|
* Process a received INSTANT_SPLIT bundle.
|
|
5279
5682
|
*
|
|
@@ -5290,39 +5693,17 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5290
5693
|
*/
|
|
5291
5694
|
async processInstantSplitBundle(bundle, senderPubkey, memo) {
|
|
5292
5695
|
this.ensureInitialized();
|
|
5696
|
+
if (!this.loaded && this.loadedPromise) {
|
|
5697
|
+
await this.loadedPromise;
|
|
5698
|
+
}
|
|
5293
5699
|
if (!isInstantSplitBundleV5(bundle)) {
|
|
5294
5700
|
return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
|
|
5295
5701
|
}
|
|
5296
5702
|
try {
|
|
5297
|
-
const
|
|
5298
|
-
if (
|
|
5299
|
-
this.log(`V5 bundle ${deterministicId.slice(0, 16)}... already exists, skipping duplicate`);
|
|
5703
|
+
const uiToken = await this.saveUnconfirmedV5Token(bundle, senderPubkey);
|
|
5704
|
+
if (!uiToken) {
|
|
5300
5705
|
return { success: true, durationMs: 0 };
|
|
5301
5706
|
}
|
|
5302
|
-
const registry = TokenRegistry.getInstance();
|
|
5303
|
-
const pendingData = {
|
|
5304
|
-
type: "v5_bundle",
|
|
5305
|
-
stage: "RECEIVED",
|
|
5306
|
-
bundleJson: JSON.stringify(bundle),
|
|
5307
|
-
senderPubkey,
|
|
5308
|
-
savedAt: Date.now(),
|
|
5309
|
-
attemptCount: 0
|
|
5310
|
-
};
|
|
5311
|
-
const uiToken = {
|
|
5312
|
-
id: deterministicId,
|
|
5313
|
-
coinId: bundle.coinId,
|
|
5314
|
-
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
5315
|
-
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
5316
|
-
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
5317
|
-
amount: bundle.amount,
|
|
5318
|
-
status: "submitted",
|
|
5319
|
-
// UNCONFIRMED
|
|
5320
|
-
createdAt: Date.now(),
|
|
5321
|
-
updatedAt: Date.now(),
|
|
5322
|
-
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
5323
|
-
};
|
|
5324
|
-
await this.addToken(uiToken);
|
|
5325
|
-
this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
|
|
5326
5707
|
const senderInfo = await this.resolveSenderInfo(senderPubkey);
|
|
5327
5708
|
await this.addToHistory({
|
|
5328
5709
|
type: "RECEIVED",
|
|
@@ -5333,7 +5714,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5333
5714
|
senderPubkey,
|
|
5334
5715
|
...senderInfo,
|
|
5335
5716
|
memo,
|
|
5336
|
-
tokenId:
|
|
5717
|
+
tokenId: uiToken.id
|
|
5337
5718
|
});
|
|
5338
5719
|
this.deps.emitEvent("transfer:incoming", {
|
|
5339
5720
|
id: bundle.splitGroupId,
|
|
@@ -5346,6 +5727,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5346
5727
|
await this.save();
|
|
5347
5728
|
this.resolveUnconfirmed().catch(() => {
|
|
5348
5729
|
});
|
|
5730
|
+
this.scheduleResolveUnconfirmed();
|
|
5349
5731
|
return { success: true, durationMs: 0 };
|
|
5350
5732
|
} catch (error) {
|
|
5351
5733
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -5982,16 +6364,18 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5982
6364
|
}
|
|
5983
6365
|
/**
|
|
5984
6366
|
* Aggregate tokens by coinId with confirmed/unconfirmed breakdown.
|
|
5985
|
-
* Excludes tokens with status 'spent'
|
|
6367
|
+
* Excludes tokens with status 'spent' or 'invalid'.
|
|
6368
|
+
* Tokens with status 'transferring' are counted as unconfirmed (visible in UI as "Sending").
|
|
5986
6369
|
*/
|
|
5987
6370
|
aggregateTokens(coinId) {
|
|
5988
6371
|
const assetsMap = /* @__PURE__ */ new Map();
|
|
5989
6372
|
for (const token of this.tokens.values()) {
|
|
5990
|
-
if (token.status === "spent" || token.status === "invalid"
|
|
6373
|
+
if (token.status === "spent" || token.status === "invalid") continue;
|
|
5991
6374
|
if (coinId && token.coinId !== coinId) continue;
|
|
5992
6375
|
const key = token.coinId;
|
|
5993
6376
|
const amount = BigInt(token.amount);
|
|
5994
6377
|
const isConfirmed = token.status === "confirmed";
|
|
6378
|
+
const isTransferring = token.status === "transferring";
|
|
5995
6379
|
const existing = assetsMap.get(key);
|
|
5996
6380
|
if (existing) {
|
|
5997
6381
|
if (isConfirmed) {
|
|
@@ -6001,6 +6385,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6001
6385
|
existing.unconfirmedAmount += amount;
|
|
6002
6386
|
existing.unconfirmedTokenCount++;
|
|
6003
6387
|
}
|
|
6388
|
+
if (isTransferring) existing.transferringTokenCount++;
|
|
6004
6389
|
} else {
|
|
6005
6390
|
assetsMap.set(key, {
|
|
6006
6391
|
coinId: token.coinId,
|
|
@@ -6011,7 +6396,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6011
6396
|
confirmedAmount: isConfirmed ? amount : 0n,
|
|
6012
6397
|
unconfirmedAmount: isConfirmed ? 0n : amount,
|
|
6013
6398
|
confirmedTokenCount: isConfirmed ? 1 : 0,
|
|
6014
|
-
unconfirmedTokenCount: isConfirmed ? 0 : 1
|
|
6399
|
+
unconfirmedTokenCount: isConfirmed ? 0 : 1,
|
|
6400
|
+
transferringTokenCount: isTransferring ? 1 : 0
|
|
6015
6401
|
});
|
|
6016
6402
|
}
|
|
6017
6403
|
}
|
|
@@ -6029,6 +6415,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6029
6415
|
unconfirmedAmount: raw.unconfirmedAmount.toString(),
|
|
6030
6416
|
confirmedTokenCount: raw.confirmedTokenCount,
|
|
6031
6417
|
unconfirmedTokenCount: raw.unconfirmedTokenCount,
|
|
6418
|
+
transferringTokenCount: raw.transferringTokenCount,
|
|
6032
6419
|
priceUsd: null,
|
|
6033
6420
|
priceEur: null,
|
|
6034
6421
|
change24h: null,
|
|
@@ -6092,28 +6479,70 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6092
6479
|
};
|
|
6093
6480
|
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
6094
6481
|
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
6095
|
-
if (!stClient || !trustBase)
|
|
6482
|
+
if (!stClient || !trustBase) {
|
|
6483
|
+
console.log(`[V5-RESOLVE] resolveUnconfirmed: EARLY EXIT \u2014 stClient=${!!stClient} trustBase=${!!trustBase}`);
|
|
6484
|
+
return result;
|
|
6485
|
+
}
|
|
6096
6486
|
const signingService = await this.createSigningService();
|
|
6487
|
+
const submittedCount = Array.from(this.tokens.values()).filter((t) => t.status === "submitted").length;
|
|
6488
|
+
console.log(`[V5-RESOLVE] resolveUnconfirmed: ${submittedCount} submitted token(s) to process`);
|
|
6097
6489
|
for (const [tokenId, token] of this.tokens) {
|
|
6098
6490
|
if (token.status !== "submitted") continue;
|
|
6099
6491
|
const pending2 = this.parsePendingFinalization(token.sdkData);
|
|
6100
6492
|
if (!pending2) {
|
|
6493
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 16)}: no pending finalization metadata, skipping`);
|
|
6101
6494
|
result.stillPending++;
|
|
6102
6495
|
continue;
|
|
6103
6496
|
}
|
|
6104
6497
|
if (pending2.type === "v5_bundle") {
|
|
6498
|
+
console.log(`[V5-RESOLVE] Processing ${tokenId.slice(0, 16)}... stage=${pending2.stage} attempt=${pending2.attemptCount}`);
|
|
6105
6499
|
const progress = await this.resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService);
|
|
6500
|
+
console.log(`[V5-RESOLVE] Result for ${tokenId.slice(0, 16)}...: ${progress} (stage now: ${pending2.stage})`);
|
|
6106
6501
|
result.details.push({ tokenId, stage: pending2.stage, status: progress });
|
|
6107
6502
|
if (progress === "resolved") result.resolved++;
|
|
6108
6503
|
else if (progress === "failed") result.failed++;
|
|
6109
6504
|
else result.stillPending++;
|
|
6110
6505
|
}
|
|
6111
6506
|
}
|
|
6112
|
-
if (result.resolved > 0 || result.failed > 0) {
|
|
6507
|
+
if (result.resolved > 0 || result.failed > 0 || result.stillPending > 0) {
|
|
6508
|
+
console.log(`[V5-RESOLVE] Saving: resolved=${result.resolved} failed=${result.failed} stillPending=${result.stillPending}`);
|
|
6113
6509
|
await this.save();
|
|
6114
6510
|
}
|
|
6115
6511
|
return result;
|
|
6116
6512
|
}
|
|
6513
|
+
/**
|
|
6514
|
+
* Start a periodic interval that retries resolveUnconfirmed() until all
|
|
6515
|
+
* tokens are confirmed or failed. Stops automatically when nothing is
|
|
6516
|
+
* pending and is cleaned up by destroy().
|
|
6517
|
+
*/
|
|
6518
|
+
scheduleResolveUnconfirmed() {
|
|
6519
|
+
if (this.resolveUnconfirmedTimer) return;
|
|
6520
|
+
const hasUnconfirmed = Array.from(this.tokens.values()).some(
|
|
6521
|
+
(t) => t.status === "submitted"
|
|
6522
|
+
);
|
|
6523
|
+
if (!hasUnconfirmed) {
|
|
6524
|
+
console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: no submitted tokens, not starting timer`);
|
|
6525
|
+
return;
|
|
6526
|
+
}
|
|
6527
|
+
console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: starting periodic retry (every ${_PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS}ms)`);
|
|
6528
|
+
this.resolveUnconfirmedTimer = setInterval(async () => {
|
|
6529
|
+
try {
|
|
6530
|
+
const result = await this.resolveUnconfirmed();
|
|
6531
|
+
if (result.stillPending === 0) {
|
|
6532
|
+
console.log(`[V5-RESOLVE] All tokens resolved, stopping periodic retry`);
|
|
6533
|
+
this.stopResolveUnconfirmedPolling();
|
|
6534
|
+
}
|
|
6535
|
+
} catch (err) {
|
|
6536
|
+
console.log(`[V5-RESOLVE] Periodic retry error:`, err);
|
|
6537
|
+
}
|
|
6538
|
+
}, _PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS);
|
|
6539
|
+
}
|
|
6540
|
+
stopResolveUnconfirmedPolling() {
|
|
6541
|
+
if (this.resolveUnconfirmedTimer) {
|
|
6542
|
+
clearInterval(this.resolveUnconfirmedTimer);
|
|
6543
|
+
this.resolveUnconfirmedTimer = null;
|
|
6544
|
+
}
|
|
6545
|
+
}
|
|
6117
6546
|
// ===========================================================================
|
|
6118
6547
|
// Private - V5 Lazy Resolution Helpers
|
|
6119
6548
|
// ===========================================================================
|
|
@@ -6126,10 +6555,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6126
6555
|
pending2.lastAttemptAt = Date.now();
|
|
6127
6556
|
try {
|
|
6128
6557
|
if (pending2.stage === "RECEIVED") {
|
|
6558
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: RECEIVED \u2192 submitting mint commitment...`);
|
|
6129
6559
|
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
6130
6560
|
const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
|
|
6131
6561
|
const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
|
|
6132
6562
|
const mintResponse = await stClient.submitMintCommitment(mintCommitment);
|
|
6563
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint response status=${mintResponse.status}`);
|
|
6133
6564
|
if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
|
|
6134
6565
|
throw new Error(`Mint submission failed: ${mintResponse.status}`);
|
|
6135
6566
|
}
|
|
@@ -6137,22 +6568,27 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6137
6568
|
this.updatePendingFinalization(token, pending2);
|
|
6138
6569
|
}
|
|
6139
6570
|
if (pending2.stage === "MINT_SUBMITTED") {
|
|
6571
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_SUBMITTED \u2192 checking mint proof...`);
|
|
6140
6572
|
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
6141
6573
|
const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
|
|
6142
6574
|
const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
|
|
6143
6575
|
const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
|
|
6144
6576
|
if (!proof) {
|
|
6577
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof not yet available, staying MINT_SUBMITTED`);
|
|
6145
6578
|
this.updatePendingFinalization(token, pending2);
|
|
6146
6579
|
return "pending";
|
|
6147
6580
|
}
|
|
6581
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof obtained!`);
|
|
6148
6582
|
pending2.mintProofJson = JSON.stringify(proof);
|
|
6149
6583
|
pending2.stage = "MINT_PROVEN";
|
|
6150
6584
|
this.updatePendingFinalization(token, pending2);
|
|
6151
6585
|
}
|
|
6152
6586
|
if (pending2.stage === "MINT_PROVEN") {
|
|
6587
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_PROVEN \u2192 submitting transfer commitment...`);
|
|
6153
6588
|
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
6154
6589
|
const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
|
|
6155
6590
|
const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
|
|
6591
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer response status=${transferResponse.status}`);
|
|
6156
6592
|
if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
|
|
6157
6593
|
throw new Error(`Transfer submission failed: ${transferResponse.status}`);
|
|
6158
6594
|
}
|
|
@@ -6160,13 +6596,16 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6160
6596
|
this.updatePendingFinalization(token, pending2);
|
|
6161
6597
|
}
|
|
6162
6598
|
if (pending2.stage === "TRANSFER_SUBMITTED") {
|
|
6599
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: TRANSFER_SUBMITTED \u2192 checking transfer proof...`);
|
|
6163
6600
|
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
6164
6601
|
const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
|
|
6165
6602
|
const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
|
|
6166
6603
|
if (!proof) {
|
|
6604
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof not yet available, staying TRANSFER_SUBMITTED`);
|
|
6167
6605
|
this.updatePendingFinalization(token, pending2);
|
|
6168
6606
|
return "pending";
|
|
6169
6607
|
}
|
|
6608
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof obtained! Finalizing...`);
|
|
6170
6609
|
const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
|
|
6171
6610
|
const confirmedToken = {
|
|
6172
6611
|
id: token.id,
|
|
@@ -6182,6 +6621,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6182
6621
|
sdkData: JSON.stringify(finalizedToken.toJSON())
|
|
6183
6622
|
};
|
|
6184
6623
|
this.tokens.set(tokenId, confirmedToken);
|
|
6624
|
+
this.deps.emitEvent("transfer:confirmed", {
|
|
6625
|
+
id: crypto.randomUUID(),
|
|
6626
|
+
status: "completed",
|
|
6627
|
+
tokens: [confirmedToken],
|
|
6628
|
+
tokenTransfers: []
|
|
6629
|
+
});
|
|
6185
6630
|
this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
|
|
6186
6631
|
return "resolved";
|
|
6187
6632
|
}
|
|
@@ -6323,11 +6768,20 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6323
6768
|
}
|
|
6324
6769
|
}
|
|
6325
6770
|
if (pendingTokens.length > 0) {
|
|
6771
|
+
const json = JSON.stringify(pendingTokens);
|
|
6772
|
+
this.log(`[V5-PERSIST] Saving ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")} (${json.length} bytes)`);
|
|
6326
6773
|
await this.deps.storage.set(
|
|
6327
6774
|
STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
|
|
6328
|
-
|
|
6775
|
+
json
|
|
6329
6776
|
);
|
|
6777
|
+
const verify = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
|
|
6778
|
+
if (!verify) {
|
|
6779
|
+
console.error("[Payments][V5-PERSIST] CRITICAL: KV write succeeded but read-back is empty!");
|
|
6780
|
+
} else {
|
|
6781
|
+
this.log(`[V5-PERSIST] Verified: read-back ${verify.length} bytes`);
|
|
6782
|
+
}
|
|
6330
6783
|
} else {
|
|
6784
|
+
this.log(`[V5-PERSIST] No pending V5 tokens to save (total tokens: ${this.tokens.size}), clearing KV`);
|
|
6331
6785
|
await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
|
|
6332
6786
|
}
|
|
6333
6787
|
}
|
|
@@ -6337,16 +6791,47 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6337
6791
|
*/
|
|
6338
6792
|
async loadPendingV5Tokens() {
|
|
6339
6793
|
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
|
|
6794
|
+
this.log(`[V5-PERSIST] loadPendingV5Tokens: KV data = ${data ? `${data.length} bytes` : "null/empty"}`);
|
|
6340
6795
|
if (!data) return;
|
|
6341
6796
|
try {
|
|
6342
6797
|
const pendingTokens = JSON.parse(data);
|
|
6798
|
+
this.log(`[V5-PERSIST] Parsed ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")}`);
|
|
6343
6799
|
for (const token of pendingTokens) {
|
|
6344
6800
|
if (!this.tokens.has(token.id)) {
|
|
6345
6801
|
this.tokens.set(token.id, token);
|
|
6802
|
+
this.log(`[V5-PERSIST] Restored token ${token.id.slice(0, 16)} (status=${token.status})`);
|
|
6803
|
+
} else {
|
|
6804
|
+
this.log(`[V5-PERSIST] Token ${token.id.slice(0, 16)} already in map, skipping`);
|
|
6346
6805
|
}
|
|
6347
6806
|
}
|
|
6348
|
-
|
|
6349
|
-
|
|
6807
|
+
} catch (err) {
|
|
6808
|
+
console.error("[Payments][V5-PERSIST] Failed to parse pending V5 tokens:", err);
|
|
6809
|
+
}
|
|
6810
|
+
}
|
|
6811
|
+
/**
|
|
6812
|
+
* Persist the set of processed splitGroupIds to KV storage.
|
|
6813
|
+
* This ensures Nostr re-deliveries are ignored across page reloads,
|
|
6814
|
+
* even when the confirmed token's in-memory ID differs from v5split_{id}.
|
|
6815
|
+
*/
|
|
6816
|
+
async saveProcessedSplitGroupIds() {
|
|
6817
|
+
const ids = Array.from(this.processedSplitGroupIds);
|
|
6818
|
+
if (ids.length > 0) {
|
|
6819
|
+
await this.deps.storage.set(
|
|
6820
|
+
STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS,
|
|
6821
|
+
JSON.stringify(ids)
|
|
6822
|
+
);
|
|
6823
|
+
}
|
|
6824
|
+
}
|
|
6825
|
+
/**
|
|
6826
|
+
* Load processed splitGroupIds from KV storage.
|
|
6827
|
+
*/
|
|
6828
|
+
async loadProcessedSplitGroupIds() {
|
|
6829
|
+
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS);
|
|
6830
|
+
if (!data) return;
|
|
6831
|
+
try {
|
|
6832
|
+
const ids = JSON.parse(data);
|
|
6833
|
+
for (const id of ids) {
|
|
6834
|
+
this.processedSplitGroupIds.add(id);
|
|
6350
6835
|
}
|
|
6351
6836
|
} catch {
|
|
6352
6837
|
}
|
|
@@ -7001,7 +7486,32 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7001
7486
|
try {
|
|
7002
7487
|
const result = await provider.sync(localData);
|
|
7003
7488
|
if (result.success && result.merged) {
|
|
7489
|
+
const savedTokens = new Map(this.tokens);
|
|
7004
7490
|
this.loadFromStorageData(result.merged);
|
|
7491
|
+
let restoredCount = 0;
|
|
7492
|
+
for (const [tokenId, token] of savedTokens) {
|
|
7493
|
+
if (this.tokens.has(tokenId)) continue;
|
|
7494
|
+
const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7495
|
+
const stateHash = extractStateHashFromSdkData(token.sdkData);
|
|
7496
|
+
if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
|
|
7497
|
+
continue;
|
|
7498
|
+
}
|
|
7499
|
+
if (sdkTokenId) {
|
|
7500
|
+
let hasEquivalent = false;
|
|
7501
|
+
for (const existing of this.tokens.values()) {
|
|
7502
|
+
if (extractTokenIdFromSdkData(existing.sdkData) === sdkTokenId) {
|
|
7503
|
+
hasEquivalent = true;
|
|
7504
|
+
break;
|
|
7505
|
+
}
|
|
7506
|
+
}
|
|
7507
|
+
if (hasEquivalent) continue;
|
|
7508
|
+
}
|
|
7509
|
+
this.tokens.set(tokenId, token);
|
|
7510
|
+
restoredCount++;
|
|
7511
|
+
}
|
|
7512
|
+
if (restoredCount > 0) {
|
|
7513
|
+
console.log(`[Payments] Sync: restored ${restoredCount} token(s) lost by loadFromStorageData`);
|
|
7514
|
+
}
|
|
7005
7515
|
if (this.nametags.length === 0 && savedNametags.length > 0) {
|
|
7006
7516
|
this.nametags = savedNametags;
|
|
7007
7517
|
}
|
|
@@ -7303,7 +7813,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7303
7813
|
/**
|
|
7304
7814
|
* Handle NOSTR-FIRST commitment-only transfer (recipient side)
|
|
7305
7815
|
* This is called when receiving a transfer with only commitmentData and no proof yet.
|
|
7306
|
-
*
|
|
7816
|
+
* Delegates to saveCommitmentOnlyToken() helper, then emits event + records history.
|
|
7307
7817
|
*/
|
|
7308
7818
|
async handleCommitmentOnlyTransfer(transfer, payload) {
|
|
7309
7819
|
try {
|
|
@@ -7313,40 +7823,22 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7313
7823
|
console.warn("[Payments] Invalid NOSTR-FIRST transfer format");
|
|
7314
7824
|
return;
|
|
7315
7825
|
}
|
|
7316
|
-
const
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
decimals: tokenInfo.decimals,
|
|
7323
|
-
iconUrl: tokenInfo.iconUrl,
|
|
7324
|
-
amount: tokenInfo.amount,
|
|
7325
|
-
status: "submitted",
|
|
7326
|
-
// NOSTR-FIRST: unconfirmed until proof
|
|
7327
|
-
createdAt: Date.now(),
|
|
7328
|
-
updatedAt: Date.now(),
|
|
7329
|
-
sdkData: typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput)
|
|
7330
|
-
};
|
|
7331
|
-
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7332
|
-
const nostrStateHash = extractStateHashFromSdkData(token.sdkData);
|
|
7333
|
-
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
7334
|
-
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
7335
|
-
return;
|
|
7336
|
-
}
|
|
7337
|
-
this.tokens.set(token.id, token);
|
|
7338
|
-
await this.save();
|
|
7339
|
-
this.log(`NOSTR-FIRST: Token ${token.id.slice(0, 8)}... added as submitted (unconfirmed)`);
|
|
7826
|
+
const token = await this.saveCommitmentOnlyToken(
|
|
7827
|
+
sourceTokenInput,
|
|
7828
|
+
commitmentInput,
|
|
7829
|
+
transfer.senderTransportPubkey
|
|
7830
|
+
);
|
|
7831
|
+
if (!token) return;
|
|
7340
7832
|
const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
|
|
7341
|
-
|
|
7833
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
7342
7834
|
id: transfer.id,
|
|
7343
7835
|
senderPubkey: transfer.senderTransportPubkey,
|
|
7344
7836
|
senderNametag: senderInfo.senderNametag,
|
|
7345
7837
|
tokens: [token],
|
|
7346
7838
|
memo: payload.memo,
|
|
7347
7839
|
receivedAt: transfer.timestamp
|
|
7348
|
-
};
|
|
7349
|
-
|
|
7840
|
+
});
|
|
7841
|
+
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7350
7842
|
await this.addToHistory({
|
|
7351
7843
|
type: "RECEIVED",
|
|
7352
7844
|
amount: token.amount,
|
|
@@ -7358,29 +7850,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7358
7850
|
memo: payload.memo,
|
|
7359
7851
|
tokenId: nostrTokenId || token.id
|
|
7360
7852
|
});
|
|
7361
|
-
try {
|
|
7362
|
-
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
|
|
7363
|
-
const requestIdBytes = commitment.requestId;
|
|
7364
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
7365
|
-
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
7366
|
-
if (stClient) {
|
|
7367
|
-
const response = await stClient.submitTransferCommitment(commitment);
|
|
7368
|
-
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
7369
|
-
}
|
|
7370
|
-
this.addProofPollingJob({
|
|
7371
|
-
tokenId: token.id,
|
|
7372
|
-
requestIdHex,
|
|
7373
|
-
commitmentJson: JSON.stringify(commitmentInput),
|
|
7374
|
-
startedAt: Date.now(),
|
|
7375
|
-
attemptCount: 0,
|
|
7376
|
-
lastAttemptAt: 0,
|
|
7377
|
-
onProofReceived: async (tokenId) => {
|
|
7378
|
-
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
|
|
7379
|
-
}
|
|
7380
|
-
});
|
|
7381
|
-
} catch (err) {
|
|
7382
|
-
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
7383
|
-
}
|
|
7384
7853
|
} catch (error) {
|
|
7385
7854
|
console.error("[Payments] Failed to process NOSTR-FIRST transfer:", error);
|
|
7386
7855
|
}
|
|
@@ -7493,8 +7962,34 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7493
7962
|
}
|
|
7494
7963
|
}
|
|
7495
7964
|
async handleIncomingTransfer(transfer) {
|
|
7965
|
+
if (!this.loaded && this.loadedPromise) {
|
|
7966
|
+
await this.loadedPromise;
|
|
7967
|
+
}
|
|
7496
7968
|
try {
|
|
7497
7969
|
const payload = transfer.payload;
|
|
7970
|
+
console.log("[Payments][DEBUG] handleIncomingTransfer: keys=", Object.keys(payload).join(","));
|
|
7971
|
+
let combinedBundle = null;
|
|
7972
|
+
if (isCombinedTransferBundleV6(payload)) {
|
|
7973
|
+
combinedBundle = payload;
|
|
7974
|
+
} else if (payload.token) {
|
|
7975
|
+
try {
|
|
7976
|
+
const inner = typeof payload.token === "string" ? JSON.parse(payload.token) : payload.token;
|
|
7977
|
+
if (isCombinedTransferBundleV6(inner)) {
|
|
7978
|
+
combinedBundle = inner;
|
|
7979
|
+
}
|
|
7980
|
+
} catch {
|
|
7981
|
+
}
|
|
7982
|
+
}
|
|
7983
|
+
if (combinedBundle) {
|
|
7984
|
+
this.log("Processing COMBINED_TRANSFER V6 bundle...");
|
|
7985
|
+
try {
|
|
7986
|
+
await this.processCombinedTransferBundle(combinedBundle, transfer.senderTransportPubkey);
|
|
7987
|
+
this.log("COMBINED_TRANSFER V6 processed successfully");
|
|
7988
|
+
} catch (err) {
|
|
7989
|
+
console.error("[Payments] COMBINED_TRANSFER V6 processing error:", err);
|
|
7990
|
+
}
|
|
7991
|
+
return;
|
|
7992
|
+
}
|
|
7498
7993
|
let instantBundle = null;
|
|
7499
7994
|
if (isInstantSplitBundle(payload)) {
|
|
7500
7995
|
instantBundle = payload;
|
|
@@ -7526,7 +8021,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7526
8021
|
return;
|
|
7527
8022
|
}
|
|
7528
8023
|
if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
|
|
7529
|
-
|
|
8024
|
+
console.log("[Payments][DEBUG] >>> NOSTR-FIRST commitment-only transfer detected");
|
|
7530
8025
|
await this.handleCommitmentOnlyTransfer(transfer, payload);
|
|
7531
8026
|
return;
|
|
7532
8027
|
}
|
|
@@ -7646,17 +8141,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7646
8141
|
memo: payload.memo,
|
|
7647
8142
|
tokenId: incomingTokenId || token.id
|
|
7648
8143
|
});
|
|
8144
|
+
const incomingTransfer = {
|
|
8145
|
+
id: transfer.id,
|
|
8146
|
+
senderPubkey: transfer.senderTransportPubkey,
|
|
8147
|
+
senderNametag: senderInfo.senderNametag,
|
|
8148
|
+
tokens: [token],
|
|
8149
|
+
memo: payload.memo,
|
|
8150
|
+
receivedAt: transfer.timestamp
|
|
8151
|
+
};
|
|
8152
|
+
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
8153
|
+
this.log(`Incoming transfer processed: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
8154
|
+
} else {
|
|
8155
|
+
this.log(`Duplicate transfer ignored: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7649
8156
|
}
|
|
7650
|
-
const incomingTransfer = {
|
|
7651
|
-
id: transfer.id,
|
|
7652
|
-
senderPubkey: transfer.senderTransportPubkey,
|
|
7653
|
-
senderNametag: senderInfo.senderNametag,
|
|
7654
|
-
tokens: [token],
|
|
7655
|
-
memo: payload.memo,
|
|
7656
|
-
receivedAt: transfer.timestamp
|
|
7657
|
-
};
|
|
7658
|
-
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
7659
|
-
this.log(`Incoming transfer processed: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7660
8157
|
} catch (error) {
|
|
7661
8158
|
console.error("[Payments] Failed to process incoming transfer:", error);
|
|
7662
8159
|
}
|
|
@@ -7689,17 +8186,24 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7689
8186
|
// ===========================================================================
|
|
7690
8187
|
async save() {
|
|
7691
8188
|
const providers = this.getTokenStorageProviders();
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
return
|
|
7695
|
-
}
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
8189
|
+
const tokenStats = Array.from(this.tokens.values()).map((t) => {
|
|
8190
|
+
const txf = tokenToTxf(t);
|
|
8191
|
+
return `${t.id.slice(0, 12)}(${t.status},txf=${!!txf})`;
|
|
8192
|
+
});
|
|
8193
|
+
console.log(`[Payments][DEBUG] save(): providers=${providers.size}, tokens=[${tokenStats.join(", ")}]`);
|
|
8194
|
+
if (providers.size > 0) {
|
|
8195
|
+
const data = await this.createStorageData();
|
|
8196
|
+
const dataKeys = Object.keys(data).filter((k) => k.startsWith("token-"));
|
|
8197
|
+
console.log(`[Payments][DEBUG] save(): TXF keys=${dataKeys.length} (${dataKeys.join(", ")})`);
|
|
8198
|
+
for (const [id, provider] of providers) {
|
|
8199
|
+
try {
|
|
8200
|
+
await provider.save(data);
|
|
8201
|
+
} catch (err) {
|
|
8202
|
+
console.error(`[Payments] Failed to save to provider ${id}:`, err);
|
|
8203
|
+
}
|
|
7702
8204
|
}
|
|
8205
|
+
} else {
|
|
8206
|
+
console.log("[Payments][DEBUG] save(): No token storage providers - TXF not persisted");
|
|
7703
8207
|
}
|
|
7704
8208
|
await this.savePendingV5Tokens();
|
|
7705
8209
|
}
|
|
@@ -7735,6 +8239,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7735
8239
|
}
|
|
7736
8240
|
loadFromStorageData(data) {
|
|
7737
8241
|
const parsed = parseTxfStorageData(data);
|
|
8242
|
+
console.log(`[Payments][DEBUG] loadFromStorageData: parsed ${parsed.tokens.length} tokens, ${parsed.tombstones.length} tombstones, errors=[${parsed.validationErrors.join("; ")}]`);
|
|
7738
8243
|
this.tombstones = parsed.tombstones;
|
|
7739
8244
|
this.tokens.clear();
|
|
7740
8245
|
for (const token of parsed.tokens) {
|
|
@@ -16761,6 +17266,7 @@ function createPriceProvider(config) {
|
|
|
16761
17266
|
identityFromMnemonicSync,
|
|
16762
17267
|
initSphere,
|
|
16763
17268
|
isArchivedKey,
|
|
17269
|
+
isCombinedTransferBundleV6,
|
|
16764
17270
|
isForkedKey,
|
|
16765
17271
|
isInstantSplitBundle,
|
|
16766
17272
|
isInstantSplitBundleV4,
|