@unicitylabs/sphere-sdk 0.5.1 → 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 +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 +615 -275
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +49 -2
- package/dist/core/index.d.ts +49 -2
- package/dist/core/index.js +615 -275
- 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 +5 -2
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +5 -2
- 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 +5 -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 +5 -2
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +617 -275
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +118 -3
- package/dist/index.d.ts +118 -3
- package/dist/index.js +616 -275
- 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.cjs
CHANGED
|
@@ -109,7 +109,9 @@ var init_constants = __esm({
|
|
|
109
109
|
/** Group chat: processed event IDs for deduplication */
|
|
110
110
|
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
|
|
111
111
|
/** Processed V5 split group IDs for Nostr re-delivery dedup */
|
|
112
|
-
PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids"
|
|
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"
|
|
113
115
|
};
|
|
114
116
|
STORAGE_KEYS = {
|
|
115
117
|
...STORAGE_KEYS_GLOBAL,
|
|
@@ -792,6 +794,7 @@ __export(index_exports, {
|
|
|
792
794
|
identityFromMnemonicSync: () => identityFromMnemonicSync,
|
|
793
795
|
initSphere: () => initSphere,
|
|
794
796
|
isArchivedKey: () => isArchivedKey,
|
|
797
|
+
isCombinedTransferBundleV6: () => isCombinedTransferBundleV6,
|
|
795
798
|
isForkedKey: () => isForkedKey,
|
|
796
799
|
isInstantSplitBundle: () => isInstantSplitBundle,
|
|
797
800
|
isInstantSplitBundleV4: () => isInstantSplitBundleV4,
|
|
@@ -3742,14 +3745,149 @@ var InstantSplitExecutor = class {
|
|
|
3742
3745
|
this.devMode = config.devMode ?? false;
|
|
3743
3746
|
}
|
|
3744
3747
|
/**
|
|
3745
|
-
*
|
|
3748
|
+
* Build a V5 split bundle WITHOUT sending it via transport.
|
|
3746
3749
|
*
|
|
3747
|
-
*
|
|
3750
|
+
* Steps 1-5 of the V5 flow:
|
|
3748
3751
|
* 1. Create and submit burn commitment
|
|
3749
3752
|
* 2. Wait for burn proof
|
|
3750
3753
|
* 3. Create mint commitments with SplitMintReason
|
|
3751
3754
|
* 4. Create transfer commitment (no mint proof needed)
|
|
3752
|
-
* 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.
|
|
3753
3891
|
*
|
|
3754
3892
|
* @param tokenToSplit - The SDK token to split
|
|
3755
3893
|
* @param splitAmount - Amount to send to recipient
|
|
@@ -3763,117 +3901,19 @@ var InstantSplitExecutor = class {
|
|
|
3763
3901
|
*/
|
|
3764
3902
|
async executeSplitInstant(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, transport, recipientPubkey, options) {
|
|
3765
3903
|
const startTime = performance.now();
|
|
3766
|
-
const splitGroupId = crypto.randomUUID();
|
|
3767
|
-
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
3768
|
-
console.log(`[InstantSplit] Starting V5 split for token ${tokenIdHex.slice(0, 8)}...`);
|
|
3769
3904
|
try {
|
|
3770
|
-
const
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
3776
|
-
const senderAddressRef = await import_UnmaskedPredicateReference2.UnmaskedPredicateReference.create(
|
|
3777
|
-
tokenToSplit.type,
|
|
3778
|
-
this.signingService.algorithm,
|
|
3779
|
-
this.signingService.publicKey,
|
|
3780
|
-
import_HashAlgorithm3.HashAlgorithm.SHA256
|
|
3781
|
-
);
|
|
3782
|
-
const senderAddress = await senderAddressRef.toAddress();
|
|
3783
|
-
const builder = new import_TokenSplitBuilder2.TokenSplitBuilder();
|
|
3784
|
-
const coinDataA = import_TokenCoinData2.TokenCoinData.create([[coinId, splitAmount]]);
|
|
3785
|
-
builder.createToken(
|
|
3786
|
-
recipientTokenId,
|
|
3787
|
-
tokenToSplit.type,
|
|
3788
|
-
new Uint8Array(0),
|
|
3789
|
-
coinDataA,
|
|
3790
|
-
senderAddress,
|
|
3791
|
-
// Mint to sender first, then transfer
|
|
3792
|
-
recipientSalt,
|
|
3793
|
-
null
|
|
3794
|
-
);
|
|
3795
|
-
const coinDataB = import_TokenCoinData2.TokenCoinData.create([[coinId, remainderAmount]]);
|
|
3796
|
-
builder.createToken(
|
|
3797
|
-
senderTokenId,
|
|
3798
|
-
tokenToSplit.type,
|
|
3799
|
-
new Uint8Array(0),
|
|
3800
|
-
coinDataB,
|
|
3801
|
-
senderAddress,
|
|
3802
|
-
senderSalt,
|
|
3803
|
-
null
|
|
3804
|
-
);
|
|
3805
|
-
const split = await builder.build(tokenToSplit);
|
|
3806
|
-
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
3807
|
-
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
3808
|
-
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
3809
|
-
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3810
|
-
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3811
|
-
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3812
|
-
}
|
|
3813
|
-
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3814
|
-
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, burnCommitment);
|
|
3815
|
-
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3816
|
-
const burnDuration = performance.now() - startTime;
|
|
3817
|
-
console.log(`[InstantSplit] Burn proof received in ${burnDuration.toFixed(0)}ms`);
|
|
3818
|
-
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3819
|
-
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3820
|
-
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3821
|
-
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3822
|
-
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3823
|
-
const recipientMintCommitment = mintCommitments.find(
|
|
3824
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3825
|
-
);
|
|
3826
|
-
const senderMintCommitment = mintCommitments.find(
|
|
3827
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3828
|
-
);
|
|
3829
|
-
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3830
|
-
throw new Error("Failed to find expected mint commitments");
|
|
3831
|
-
}
|
|
3832
|
-
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3833
|
-
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3834
|
-
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3835
|
-
recipientMintCommitment.transactionData,
|
|
3905
|
+
const buildResult = await this.buildSplitBundle(
|
|
3906
|
+
tokenToSplit,
|
|
3907
|
+
splitAmount,
|
|
3908
|
+
remainderAmount,
|
|
3909
|
+
coinIdHex,
|
|
3836
3910
|
recipientAddress,
|
|
3837
|
-
|
|
3838
|
-
this.signingService
|
|
3839
|
-
);
|
|
3840
|
-
const mintedPredicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
|
|
3841
|
-
recipientTokenId,
|
|
3842
|
-
tokenToSplit.type,
|
|
3843
|
-
this.signingService,
|
|
3844
|
-
import_HashAlgorithm3.HashAlgorithm.SHA256,
|
|
3845
|
-
recipientSalt
|
|
3911
|
+
options
|
|
3846
3912
|
);
|
|
3847
|
-
|
|
3848
|
-
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3913
|
+
console.log("[InstantSplit] Sending via transport...");
|
|
3849
3914
|
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3850
|
-
let nametagTokenJson;
|
|
3851
|
-
const recipientAddressStr = recipientAddress.toString();
|
|
3852
|
-
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3853
|
-
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3854
|
-
}
|
|
3855
|
-
const bundle = {
|
|
3856
|
-
version: "5.0",
|
|
3857
|
-
type: "INSTANT_SPLIT",
|
|
3858
|
-
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3859
|
-
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3860
|
-
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3861
|
-
amount: splitAmount.toString(),
|
|
3862
|
-
coinId: coinIdHex,
|
|
3863
|
-
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3864
|
-
splitGroupId,
|
|
3865
|
-
senderPubkey,
|
|
3866
|
-
recipientSaltHex: toHex2(recipientSalt),
|
|
3867
|
-
transferSaltHex: toHex2(transferSalt),
|
|
3868
|
-
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3869
|
-
finalRecipientStateJson: "",
|
|
3870
|
-
// Recipient creates their own
|
|
3871
|
-
recipientAddressJson: recipientAddressStr,
|
|
3872
|
-
nametagTokenJson
|
|
3873
|
-
};
|
|
3874
|
-
console.log("[InstantSplit] Step 6: Sending via transport...");
|
|
3875
3915
|
const nostrEventId = await transport.sendTokenTransfer(recipientPubkey, {
|
|
3876
|
-
token: JSON.stringify(bundle),
|
|
3916
|
+
token: JSON.stringify(buildResult.bundle),
|
|
3877
3917
|
proof: null,
|
|
3878
3918
|
// Proof is included in the bundle
|
|
3879
3919
|
memo: options?.memo,
|
|
@@ -3884,25 +3924,13 @@ var InstantSplitExecutor = class {
|
|
|
3884
3924
|
const criticalPathDuration = performance.now() - startTime;
|
|
3885
3925
|
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3886
3926
|
options?.onNostrDelivered?.(nostrEventId);
|
|
3887
|
-
|
|
3888
|
-
if (!options?.skipBackground) {
|
|
3889
|
-
backgroundPromise = this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3890
|
-
signingService: this.signingService,
|
|
3891
|
-
tokenType: tokenToSplit.type,
|
|
3892
|
-
coinId,
|
|
3893
|
-
senderTokenId,
|
|
3894
|
-
senderSalt,
|
|
3895
|
-
onProgress: options?.onBackgroundProgress,
|
|
3896
|
-
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3897
|
-
onStorageSync: options?.onStorageSync
|
|
3898
|
-
});
|
|
3899
|
-
}
|
|
3927
|
+
const backgroundPromise = buildResult.startBackground();
|
|
3900
3928
|
return {
|
|
3901
3929
|
success: true,
|
|
3902
3930
|
nostrEventId,
|
|
3903
|
-
splitGroupId,
|
|
3931
|
+
splitGroupId: buildResult.splitGroupId,
|
|
3904
3932
|
criticalPathDurationMs: criticalPathDuration,
|
|
3905
|
-
backgroundStarted:
|
|
3933
|
+
backgroundStarted: true,
|
|
3906
3934
|
backgroundPromise
|
|
3907
3935
|
};
|
|
3908
3936
|
} catch (error) {
|
|
@@ -3911,7 +3939,6 @@ var InstantSplitExecutor = class {
|
|
|
3911
3939
|
console.error(`[InstantSplit] Failed after ${duration.toFixed(0)}ms:`, error);
|
|
3912
3940
|
return {
|
|
3913
3941
|
success: false,
|
|
3914
|
-
splitGroupId,
|
|
3915
3942
|
criticalPathDurationMs: duration,
|
|
3916
3943
|
error: errorMessage,
|
|
3917
3944
|
backgroundStarted: false
|
|
@@ -4116,6 +4143,11 @@ function isInstantSplitBundleV4(obj) {
|
|
|
4116
4143
|
function isInstantSplitBundleV5(obj) {
|
|
4117
4144
|
return isInstantSplitBundle(obj) && obj.version === "5.0";
|
|
4118
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
|
+
}
|
|
4119
4151
|
|
|
4120
4152
|
// modules/payments/InstantSplitProcessor.ts
|
|
4121
4153
|
function fromHex3(hex) {
|
|
@@ -4769,6 +4801,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4769
4801
|
// Survives page reloads via KV storage so Nostr re-deliveries are ignored
|
|
4770
4802
|
// even when the confirmed token's in-memory ID differs from v5split_{id}.
|
|
4771
4803
|
processedSplitGroupIds = /* @__PURE__ */ new Set();
|
|
4804
|
+
// Persistent dedup: tracks V6 combined transfer IDs that have been processed.
|
|
4805
|
+
processedCombinedTransferIds = /* @__PURE__ */ new Set();
|
|
4772
4806
|
// Storage event subscriptions (push-based sync)
|
|
4773
4807
|
storageEventUnsubscribers = [];
|
|
4774
4808
|
syncDebounceTimer = null;
|
|
@@ -4869,10 +4903,23 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4869
4903
|
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4870
4904
|
}
|
|
4871
4905
|
}
|
|
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
|
+
}
|
|
4917
|
+
}
|
|
4872
4918
|
const loadedTokens = Array.from(this.tokens.values()).map((t) => `${t.id.slice(0, 12)}(${t.status})`);
|
|
4873
4919
|
console.log(`[Payments][DEBUG] load(): from TXF providers: ${this.tokens.size} tokens [${loadedTokens.join(", ")}]`);
|
|
4874
4920
|
await this.loadPendingV5Tokens();
|
|
4875
4921
|
await this.loadProcessedSplitGroupIds();
|
|
4922
|
+
await this.loadProcessedCombinedTransferIds();
|
|
4876
4923
|
await this.loadHistory();
|
|
4877
4924
|
const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
|
|
4878
4925
|
if (pending2) {
|
|
@@ -4964,12 +5011,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4964
5011
|
token.status = "transferring";
|
|
4965
5012
|
this.tokens.set(token.id, token);
|
|
4966
5013
|
}
|
|
5014
|
+
await this.save();
|
|
4967
5015
|
await this.saveToOutbox(result, recipientPubkey);
|
|
4968
5016
|
result.status = "submitted";
|
|
4969
5017
|
const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
|
|
4970
5018
|
const transferMode = request.transferMode ?? "instant";
|
|
4971
|
-
if (
|
|
4972
|
-
if (
|
|
5019
|
+
if (transferMode === "conservative") {
|
|
5020
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4973
5021
|
this.log("Executing conservative split...");
|
|
4974
5022
|
const splitExecutor = new TokenSplitExecutor({
|
|
4975
5023
|
stateTransitionClient: stClient,
|
|
@@ -5013,27 +5061,59 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5013
5061
|
requestIdHex: splitRequestIdHex
|
|
5014
5062
|
});
|
|
5015
5063
|
this.log(`Conservative split transfer completed`);
|
|
5016
|
-
}
|
|
5017
|
-
|
|
5018
|
-
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...");
|
|
5019
5098
|
const executor = new InstantSplitExecutor({
|
|
5020
5099
|
stateTransitionClient: stClient,
|
|
5021
5100
|
trustBase,
|
|
5022
5101
|
signingService,
|
|
5023
5102
|
devMode
|
|
5024
5103
|
});
|
|
5025
|
-
|
|
5104
|
+
builtSplit = await executor.buildSplitBundle(
|
|
5026
5105
|
splitPlan.tokenToSplit.sdkToken,
|
|
5027
5106
|
splitPlan.splitAmount,
|
|
5028
5107
|
splitPlan.remainderAmount,
|
|
5029
5108
|
splitPlan.coinId,
|
|
5030
5109
|
recipientAddress,
|
|
5031
|
-
this.deps.transport,
|
|
5032
|
-
recipientPubkey,
|
|
5033
5110
|
{
|
|
5034
5111
|
memo: request.memo,
|
|
5035
5112
|
onChangeTokenCreated: async (changeToken) => {
|
|
5036
5113
|
const changeTokenData = changeToken.toJSON();
|
|
5114
|
+
if (changeTokenPlaceholderId && this.tokens.has(changeTokenPlaceholderId)) {
|
|
5115
|
+
this.tokens.delete(changeTokenPlaceholderId);
|
|
5116
|
+
}
|
|
5037
5117
|
const uiToken = {
|
|
5038
5118
|
id: crypto.randomUUID(),
|
|
5039
5119
|
coinId: request.coinId,
|
|
@@ -5056,65 +5136,103 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5056
5136
|
}
|
|
5057
5137
|
}
|
|
5058
5138
|
);
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
this.
|
|
5064
|
-
|
|
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) {
|
|
5065
5204
|
await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
|
|
5066
5205
|
result.tokenTransfers.push({
|
|
5067
5206
|
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
5068
5207
|
method: "split",
|
|
5069
|
-
splitGroupId:
|
|
5070
|
-
nostrEventId: instantResult.nostrEventId
|
|
5208
|
+
splitGroupId: builtSplit.splitGroupId
|
|
5071
5209
|
});
|
|
5072
|
-
this.log(`Instant split transfer completed`);
|
|
5073
5210
|
}
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
}
|
|
5084
|
-
const inclusionProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment);
|
|
5085
|
-
const transferTx = commitment.toTransaction(inclusionProof);
|
|
5086
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
5087
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
5088
|
-
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
5089
|
-
memo: request.memo
|
|
5090
|
-
});
|
|
5091
|
-
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
5092
|
-
} else {
|
|
5093
|
-
console.log(`[Payments] NOSTR-FIRST: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
5094
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
5095
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
5096
|
-
commitmentData: JSON.stringify(commitment.toJSON()),
|
|
5097
|
-
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
|
|
5098
5220
|
});
|
|
5099
|
-
|
|
5100
|
-
stClient.submitTransferCommitment(commitment).catch(
|
|
5101
|
-
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
5102
|
-
);
|
|
5221
|
+
await this.removeToken(token.id);
|
|
5103
5222
|
}
|
|
5104
|
-
|
|
5105
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
5106
|
-
result.tokenTransfers.push({
|
|
5107
|
-
sourceTokenId: token.id,
|
|
5108
|
-
method: "direct",
|
|
5109
|
-
requestIdHex
|
|
5110
|
-
});
|
|
5111
|
-
this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
|
|
5112
|
-
await this.removeToken(token.id);
|
|
5223
|
+
this.log(`V6 combined transfer completed`);
|
|
5113
5224
|
}
|
|
5114
5225
|
result.status = "delivered";
|
|
5115
5226
|
await this.save();
|
|
5116
5227
|
await this.removeFromOutbox(result.id);
|
|
5117
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
|
+
}));
|
|
5118
5236
|
const sentTokenId = result.tokens[0] ? extractTokenIdFromSdkData(result.tokens[0].sdkData) : void 0;
|
|
5119
5237
|
await this.addToHistory({
|
|
5120
5238
|
type: "SENT",
|
|
@@ -5127,7 +5245,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5127
5245
|
recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
|
|
5128
5246
|
memo: request.memo,
|
|
5129
5247
|
transferId: result.id,
|
|
5130
|
-
tokenId: sentTokenId || void 0
|
|
5248
|
+
tokenId: sentTokenId || void 0,
|
|
5249
|
+
tokenIds: sentTokenIds.length > 0 ? sentTokenIds : void 0
|
|
5131
5250
|
});
|
|
5132
5251
|
this.deps.emitEvent("transfer:confirmed", result);
|
|
5133
5252
|
return result;
|
|
@@ -5297,6 +5416,267 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5297
5416
|
};
|
|
5298
5417
|
}
|
|
5299
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
|
+
}
|
|
5300
5680
|
/**
|
|
5301
5681
|
* Process a received INSTANT_SPLIT bundle.
|
|
5302
5682
|
*
|
|
@@ -5320,36 +5700,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5320
5700
|
return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
|
|
5321
5701
|
}
|
|
5322
5702
|
try {
|
|
5323
|
-
const
|
|
5324
|
-
if (
|
|
5325
|
-
console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
|
|
5703
|
+
const uiToken = await this.saveUnconfirmedV5Token(bundle, senderPubkey);
|
|
5704
|
+
if (!uiToken) {
|
|
5326
5705
|
return { success: true, durationMs: 0 };
|
|
5327
5706
|
}
|
|
5328
|
-
const registry = TokenRegistry.getInstance();
|
|
5329
|
-
const pendingData = {
|
|
5330
|
-
type: "v5_bundle",
|
|
5331
|
-
stage: "RECEIVED",
|
|
5332
|
-
bundleJson: JSON.stringify(bundle),
|
|
5333
|
-
senderPubkey,
|
|
5334
|
-
savedAt: Date.now(),
|
|
5335
|
-
attemptCount: 0
|
|
5336
|
-
};
|
|
5337
|
-
const uiToken = {
|
|
5338
|
-
id: deterministicId,
|
|
5339
|
-
coinId: bundle.coinId,
|
|
5340
|
-
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
5341
|
-
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
5342
|
-
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
5343
|
-
amount: bundle.amount,
|
|
5344
|
-
status: "submitted",
|
|
5345
|
-
// UNCONFIRMED
|
|
5346
|
-
createdAt: Date.now(),
|
|
5347
|
-
updatedAt: Date.now(),
|
|
5348
|
-
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
5349
|
-
};
|
|
5350
|
-
await this.addToken(uiToken);
|
|
5351
|
-
this.processedSplitGroupIds.add(bundle.splitGroupId);
|
|
5352
|
-
await this.saveProcessedSplitGroupIds();
|
|
5353
5707
|
const senderInfo = await this.resolveSenderInfo(senderPubkey);
|
|
5354
5708
|
await this.addToHistory({
|
|
5355
5709
|
type: "RECEIVED",
|
|
@@ -5360,7 +5714,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5360
5714
|
senderPubkey,
|
|
5361
5715
|
...senderInfo,
|
|
5362
5716
|
memo,
|
|
5363
|
-
tokenId:
|
|
5717
|
+
tokenId: uiToken.id
|
|
5364
5718
|
});
|
|
5365
5719
|
this.deps.emitEvent("transfer:incoming", {
|
|
5366
5720
|
id: bundle.splitGroupId,
|
|
@@ -6010,16 +6364,18 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6010
6364
|
}
|
|
6011
6365
|
/**
|
|
6012
6366
|
* Aggregate tokens by coinId with confirmed/unconfirmed breakdown.
|
|
6013
|
-
* 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").
|
|
6014
6369
|
*/
|
|
6015
6370
|
aggregateTokens(coinId) {
|
|
6016
6371
|
const assetsMap = /* @__PURE__ */ new Map();
|
|
6017
6372
|
for (const token of this.tokens.values()) {
|
|
6018
|
-
if (token.status === "spent" || token.status === "invalid"
|
|
6373
|
+
if (token.status === "spent" || token.status === "invalid") continue;
|
|
6019
6374
|
if (coinId && token.coinId !== coinId) continue;
|
|
6020
6375
|
const key = token.coinId;
|
|
6021
6376
|
const amount = BigInt(token.amount);
|
|
6022
6377
|
const isConfirmed = token.status === "confirmed";
|
|
6378
|
+
const isTransferring = token.status === "transferring";
|
|
6023
6379
|
const existing = assetsMap.get(key);
|
|
6024
6380
|
if (existing) {
|
|
6025
6381
|
if (isConfirmed) {
|
|
@@ -6029,6 +6385,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6029
6385
|
existing.unconfirmedAmount += amount;
|
|
6030
6386
|
existing.unconfirmedTokenCount++;
|
|
6031
6387
|
}
|
|
6388
|
+
if (isTransferring) existing.transferringTokenCount++;
|
|
6032
6389
|
} else {
|
|
6033
6390
|
assetsMap.set(key, {
|
|
6034
6391
|
coinId: token.coinId,
|
|
@@ -6039,7 +6396,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6039
6396
|
confirmedAmount: isConfirmed ? amount : 0n,
|
|
6040
6397
|
unconfirmedAmount: isConfirmed ? 0n : amount,
|
|
6041
6398
|
confirmedTokenCount: isConfirmed ? 1 : 0,
|
|
6042
|
-
unconfirmedTokenCount: isConfirmed ? 0 : 1
|
|
6399
|
+
unconfirmedTokenCount: isConfirmed ? 0 : 1,
|
|
6400
|
+
transferringTokenCount: isTransferring ? 1 : 0
|
|
6043
6401
|
});
|
|
6044
6402
|
}
|
|
6045
6403
|
}
|
|
@@ -6057,6 +6415,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6057
6415
|
unconfirmedAmount: raw.unconfirmedAmount.toString(),
|
|
6058
6416
|
confirmedTokenCount: raw.confirmedTokenCount,
|
|
6059
6417
|
unconfirmedTokenCount: raw.unconfirmedTokenCount,
|
|
6418
|
+
transferringTokenCount: raw.transferringTokenCount,
|
|
6060
6419
|
priceUsd: null,
|
|
6061
6420
|
priceEur: null,
|
|
6062
6421
|
change24h: null,
|
|
@@ -7454,7 +7813,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7454
7813
|
/**
|
|
7455
7814
|
* Handle NOSTR-FIRST commitment-only transfer (recipient side)
|
|
7456
7815
|
* This is called when receiving a transfer with only commitmentData and no proof yet.
|
|
7457
|
-
*
|
|
7816
|
+
* Delegates to saveCommitmentOnlyToken() helper, then emits event + records history.
|
|
7458
7817
|
*/
|
|
7459
7818
|
async handleCommitmentOnlyTransfer(transfer, payload) {
|
|
7460
7819
|
try {
|
|
@@ -7464,41 +7823,22 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7464
7823
|
console.warn("[Payments] Invalid NOSTR-FIRST transfer format");
|
|
7465
7824
|
return;
|
|
7466
7825
|
}
|
|
7467
|
-
const
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
decimals: tokenInfo.decimals,
|
|
7474
|
-
iconUrl: tokenInfo.iconUrl,
|
|
7475
|
-
amount: tokenInfo.amount,
|
|
7476
|
-
status: "submitted",
|
|
7477
|
-
// NOSTR-FIRST: unconfirmed until proof
|
|
7478
|
-
createdAt: Date.now(),
|
|
7479
|
-
updatedAt: Date.now(),
|
|
7480
|
-
sdkData: typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput)
|
|
7481
|
-
};
|
|
7482
|
-
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7483
|
-
const nostrStateHash = extractStateHashFromSdkData(token.sdkData);
|
|
7484
|
-
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
7485
|
-
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
7486
|
-
return;
|
|
7487
|
-
}
|
|
7488
|
-
this.tokens.set(token.id, token);
|
|
7489
|
-
console.log(`[Payments][DEBUG] NOSTR-FIRST: saving token id=${token.id.slice(0, 16)} status=${token.status} sdkData.length=${token.sdkData?.length}`);
|
|
7490
|
-
await this.save();
|
|
7491
|
-
console.log(`[Payments][DEBUG] NOSTR-FIRST: save() completed, tokens.size=${this.tokens.size}`);
|
|
7826
|
+
const token = await this.saveCommitmentOnlyToken(
|
|
7827
|
+
sourceTokenInput,
|
|
7828
|
+
commitmentInput,
|
|
7829
|
+
transfer.senderTransportPubkey
|
|
7830
|
+
);
|
|
7831
|
+
if (!token) return;
|
|
7492
7832
|
const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
|
|
7493
|
-
|
|
7833
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
7494
7834
|
id: transfer.id,
|
|
7495
7835
|
senderPubkey: transfer.senderTransportPubkey,
|
|
7496
7836
|
senderNametag: senderInfo.senderNametag,
|
|
7497
7837
|
tokens: [token],
|
|
7498
7838
|
memo: payload.memo,
|
|
7499
7839
|
receivedAt: transfer.timestamp
|
|
7500
|
-
};
|
|
7501
|
-
|
|
7840
|
+
});
|
|
7841
|
+
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7502
7842
|
await this.addToHistory({
|
|
7503
7843
|
type: "RECEIVED",
|
|
7504
7844
|
amount: token.amount,
|
|
@@ -7510,29 +7850,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7510
7850
|
memo: payload.memo,
|
|
7511
7851
|
tokenId: nostrTokenId || token.id
|
|
7512
7852
|
});
|
|
7513
|
-
try {
|
|
7514
|
-
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
|
|
7515
|
-
const requestIdBytes = commitment.requestId;
|
|
7516
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
7517
|
-
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
7518
|
-
if (stClient) {
|
|
7519
|
-
const response = await stClient.submitTransferCommitment(commitment);
|
|
7520
|
-
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
7521
|
-
}
|
|
7522
|
-
this.addProofPollingJob({
|
|
7523
|
-
tokenId: token.id,
|
|
7524
|
-
requestIdHex,
|
|
7525
|
-
commitmentJson: JSON.stringify(commitmentInput),
|
|
7526
|
-
startedAt: Date.now(),
|
|
7527
|
-
attemptCount: 0,
|
|
7528
|
-
lastAttemptAt: 0,
|
|
7529
|
-
onProofReceived: async (tokenId) => {
|
|
7530
|
-
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
|
|
7531
|
-
}
|
|
7532
|
-
});
|
|
7533
|
-
} catch (err) {
|
|
7534
|
-
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
7535
|
-
}
|
|
7536
7853
|
} catch (error) {
|
|
7537
7854
|
console.error("[Payments] Failed to process NOSTR-FIRST transfer:", error);
|
|
7538
7855
|
}
|
|
@@ -7651,6 +7968,28 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7651
7968
|
try {
|
|
7652
7969
|
const payload = transfer.payload;
|
|
7653
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
|
+
}
|
|
7654
7993
|
let instantBundle = null;
|
|
7655
7994
|
if (isInstantSplitBundle(payload)) {
|
|
7656
7995
|
instantBundle = payload;
|
|
@@ -7802,17 +8141,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7802
8141
|
memo: payload.memo,
|
|
7803
8142
|
tokenId: incomingTokenId || token.id
|
|
7804
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}`);
|
|
7805
8156
|
}
|
|
7806
|
-
const incomingTransfer = {
|
|
7807
|
-
id: transfer.id,
|
|
7808
|
-
senderPubkey: transfer.senderTransportPubkey,
|
|
7809
|
-
senderNametag: senderInfo.senderNametag,
|
|
7810
|
-
tokens: [token],
|
|
7811
|
-
memo: payload.memo,
|
|
7812
|
-
receivedAt: transfer.timestamp
|
|
7813
|
-
};
|
|
7814
|
-
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
7815
|
-
this.log(`Incoming transfer processed: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7816
8157
|
} catch (error) {
|
|
7817
8158
|
console.error("[Payments] Failed to process incoming transfer:", error);
|
|
7818
8159
|
}
|
|
@@ -16925,6 +17266,7 @@ function createPriceProvider(config) {
|
|
|
16925
17266
|
identityFromMnemonicSync,
|
|
16926
17267
|
initSphere,
|
|
16927
17268
|
isArchivedKey,
|
|
17269
|
+
isCombinedTransferBundleV6,
|
|
16928
17270
|
isForkedKey,
|
|
16929
17271
|
isInstantSplitBundle,
|
|
16930
17272
|
isInstantSplitBundleV4,
|