@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.js
CHANGED
|
@@ -93,7 +93,9 @@ var init_constants = __esm({
|
|
|
93
93
|
/** Group chat: processed event IDs for deduplication */
|
|
94
94
|
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
|
|
95
95
|
/** Processed V5 split group IDs for Nostr re-delivery dedup */
|
|
96
|
-
PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids"
|
|
96
|
+
PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids",
|
|
97
|
+
/** Processed V6 combined transfer IDs for Nostr re-delivery dedup */
|
|
98
|
+
PROCESSED_COMBINED_TRANSFER_IDS: "processed_combined_transfer_ids"
|
|
97
99
|
};
|
|
98
100
|
STORAGE_KEYS = {
|
|
99
101
|
...STORAGE_KEYS_GLOBAL,
|
|
@@ -3581,14 +3583,149 @@ var InstantSplitExecutor = class {
|
|
|
3581
3583
|
this.devMode = config.devMode ?? false;
|
|
3582
3584
|
}
|
|
3583
3585
|
/**
|
|
3584
|
-
*
|
|
3586
|
+
* Build a V5 split bundle WITHOUT sending it via transport.
|
|
3585
3587
|
*
|
|
3586
|
-
*
|
|
3588
|
+
* Steps 1-5 of the V5 flow:
|
|
3587
3589
|
* 1. Create and submit burn commitment
|
|
3588
3590
|
* 2. Wait for burn proof
|
|
3589
3591
|
* 3. Create mint commitments with SplitMintReason
|
|
3590
3592
|
* 4. Create transfer commitment (no mint proof needed)
|
|
3591
|
-
* 5.
|
|
3593
|
+
* 5. Package V5 bundle
|
|
3594
|
+
*
|
|
3595
|
+
* The caller is responsible for sending the bundle and then calling
|
|
3596
|
+
* `startBackground()` on the result to begin mint proof + change token creation.
|
|
3597
|
+
*/
|
|
3598
|
+
async buildSplitBundle(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, options) {
|
|
3599
|
+
const splitGroupId = crypto.randomUUID();
|
|
3600
|
+
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
3601
|
+
console.log(`[InstantSplit] Building V5 bundle for token ${tokenIdHex.slice(0, 8)}...`);
|
|
3602
|
+
const coinId = new CoinId3(fromHex2(coinIdHex));
|
|
3603
|
+
const seedString = `${tokenIdHex}_${splitAmount.toString()}_${remainderAmount.toString()}_${Date.now()}`;
|
|
3604
|
+
const recipientTokenId = new TokenId3(await sha2563(seedString));
|
|
3605
|
+
const senderTokenId = new TokenId3(await sha2563(seedString + "_sender"));
|
|
3606
|
+
const recipientSalt = await sha2563(seedString + "_recipient_salt");
|
|
3607
|
+
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
3608
|
+
const senderAddressRef = await UnmaskedPredicateReference2.create(
|
|
3609
|
+
tokenToSplit.type,
|
|
3610
|
+
this.signingService.algorithm,
|
|
3611
|
+
this.signingService.publicKey,
|
|
3612
|
+
HashAlgorithm3.SHA256
|
|
3613
|
+
);
|
|
3614
|
+
const senderAddress = await senderAddressRef.toAddress();
|
|
3615
|
+
const builder = new TokenSplitBuilder2();
|
|
3616
|
+
const coinDataA = TokenCoinData2.create([[coinId, splitAmount]]);
|
|
3617
|
+
builder.createToken(
|
|
3618
|
+
recipientTokenId,
|
|
3619
|
+
tokenToSplit.type,
|
|
3620
|
+
new Uint8Array(0),
|
|
3621
|
+
coinDataA,
|
|
3622
|
+
senderAddress,
|
|
3623
|
+
// Mint to sender first, then transfer
|
|
3624
|
+
recipientSalt,
|
|
3625
|
+
null
|
|
3626
|
+
);
|
|
3627
|
+
const coinDataB = TokenCoinData2.create([[coinId, remainderAmount]]);
|
|
3628
|
+
builder.createToken(
|
|
3629
|
+
senderTokenId,
|
|
3630
|
+
tokenToSplit.type,
|
|
3631
|
+
new Uint8Array(0),
|
|
3632
|
+
coinDataB,
|
|
3633
|
+
senderAddress,
|
|
3634
|
+
senderSalt,
|
|
3635
|
+
null
|
|
3636
|
+
);
|
|
3637
|
+
const split = await builder.build(tokenToSplit);
|
|
3638
|
+
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
3639
|
+
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
3640
|
+
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
3641
|
+
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3642
|
+
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3643
|
+
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3644
|
+
}
|
|
3645
|
+
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3646
|
+
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await waitInclusionProof3(this.trustBase, this.client, burnCommitment);
|
|
3647
|
+
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3648
|
+
console.log(`[InstantSplit] Burn proof received`);
|
|
3649
|
+
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3650
|
+
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3651
|
+
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3652
|
+
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3653
|
+
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3654
|
+
const recipientMintCommitment = mintCommitments.find(
|
|
3655
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3656
|
+
);
|
|
3657
|
+
const senderMintCommitment = mintCommitments.find(
|
|
3658
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3659
|
+
);
|
|
3660
|
+
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3661
|
+
throw new Error("Failed to find expected mint commitments");
|
|
3662
|
+
}
|
|
3663
|
+
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3664
|
+
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3665
|
+
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3666
|
+
recipientMintCommitment.transactionData,
|
|
3667
|
+
recipientAddress,
|
|
3668
|
+
transferSalt,
|
|
3669
|
+
this.signingService
|
|
3670
|
+
);
|
|
3671
|
+
const mintedPredicate = await UnmaskedPredicate3.create(
|
|
3672
|
+
recipientTokenId,
|
|
3673
|
+
tokenToSplit.type,
|
|
3674
|
+
this.signingService,
|
|
3675
|
+
HashAlgorithm3.SHA256,
|
|
3676
|
+
recipientSalt
|
|
3677
|
+
);
|
|
3678
|
+
const mintedState = new TokenState3(mintedPredicate, null);
|
|
3679
|
+
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3680
|
+
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3681
|
+
let nametagTokenJson;
|
|
3682
|
+
const recipientAddressStr = recipientAddress.toString();
|
|
3683
|
+
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3684
|
+
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3685
|
+
}
|
|
3686
|
+
const bundle = {
|
|
3687
|
+
version: "5.0",
|
|
3688
|
+
type: "INSTANT_SPLIT",
|
|
3689
|
+
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3690
|
+
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3691
|
+
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3692
|
+
amount: splitAmount.toString(),
|
|
3693
|
+
coinId: coinIdHex,
|
|
3694
|
+
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3695
|
+
splitGroupId,
|
|
3696
|
+
senderPubkey,
|
|
3697
|
+
recipientSaltHex: toHex2(recipientSalt),
|
|
3698
|
+
transferSaltHex: toHex2(transferSalt),
|
|
3699
|
+
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3700
|
+
finalRecipientStateJson: "",
|
|
3701
|
+
// Recipient creates their own
|
|
3702
|
+
recipientAddressJson: recipientAddressStr,
|
|
3703
|
+
nametagTokenJson
|
|
3704
|
+
};
|
|
3705
|
+
return {
|
|
3706
|
+
bundle,
|
|
3707
|
+
splitGroupId,
|
|
3708
|
+
startBackground: async () => {
|
|
3709
|
+
if (!options?.skipBackground) {
|
|
3710
|
+
await this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3711
|
+
signingService: this.signingService,
|
|
3712
|
+
tokenType: tokenToSplit.type,
|
|
3713
|
+
coinId,
|
|
3714
|
+
senderTokenId,
|
|
3715
|
+
senderSalt,
|
|
3716
|
+
onProgress: options?.onBackgroundProgress,
|
|
3717
|
+
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3718
|
+
onStorageSync: options?.onStorageSync
|
|
3719
|
+
});
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
};
|
|
3723
|
+
}
|
|
3724
|
+
/**
|
|
3725
|
+
* Execute an instant split transfer with V5 optimized flow.
|
|
3726
|
+
*
|
|
3727
|
+
* Builds the bundle via buildSplitBundle(), sends via transport,
|
|
3728
|
+
* and starts background processing.
|
|
3592
3729
|
*
|
|
3593
3730
|
* @param tokenToSplit - The SDK token to split
|
|
3594
3731
|
* @param splitAmount - Amount to send to recipient
|
|
@@ -3602,117 +3739,19 @@ var InstantSplitExecutor = class {
|
|
|
3602
3739
|
*/
|
|
3603
3740
|
async executeSplitInstant(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, transport, recipientPubkey, options) {
|
|
3604
3741
|
const startTime = performance.now();
|
|
3605
|
-
const splitGroupId = crypto.randomUUID();
|
|
3606
|
-
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
3607
|
-
console.log(`[InstantSplit] Starting V5 split for token ${tokenIdHex.slice(0, 8)}...`);
|
|
3608
3742
|
try {
|
|
3609
|
-
const
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
3615
|
-
const senderAddressRef = await UnmaskedPredicateReference2.create(
|
|
3616
|
-
tokenToSplit.type,
|
|
3617
|
-
this.signingService.algorithm,
|
|
3618
|
-
this.signingService.publicKey,
|
|
3619
|
-
HashAlgorithm3.SHA256
|
|
3620
|
-
);
|
|
3621
|
-
const senderAddress = await senderAddressRef.toAddress();
|
|
3622
|
-
const builder = new TokenSplitBuilder2();
|
|
3623
|
-
const coinDataA = TokenCoinData2.create([[coinId, splitAmount]]);
|
|
3624
|
-
builder.createToken(
|
|
3625
|
-
recipientTokenId,
|
|
3626
|
-
tokenToSplit.type,
|
|
3627
|
-
new Uint8Array(0),
|
|
3628
|
-
coinDataA,
|
|
3629
|
-
senderAddress,
|
|
3630
|
-
// Mint to sender first, then transfer
|
|
3631
|
-
recipientSalt,
|
|
3632
|
-
null
|
|
3633
|
-
);
|
|
3634
|
-
const coinDataB = TokenCoinData2.create([[coinId, remainderAmount]]);
|
|
3635
|
-
builder.createToken(
|
|
3636
|
-
senderTokenId,
|
|
3637
|
-
tokenToSplit.type,
|
|
3638
|
-
new Uint8Array(0),
|
|
3639
|
-
coinDataB,
|
|
3640
|
-
senderAddress,
|
|
3641
|
-
senderSalt,
|
|
3642
|
-
null
|
|
3643
|
-
);
|
|
3644
|
-
const split = await builder.build(tokenToSplit);
|
|
3645
|
-
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
3646
|
-
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
3647
|
-
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
3648
|
-
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3649
|
-
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3650
|
-
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3651
|
-
}
|
|
3652
|
-
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3653
|
-
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await waitInclusionProof3(this.trustBase, this.client, burnCommitment);
|
|
3654
|
-
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3655
|
-
const burnDuration = performance.now() - startTime;
|
|
3656
|
-
console.log(`[InstantSplit] Burn proof received in ${burnDuration.toFixed(0)}ms`);
|
|
3657
|
-
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3658
|
-
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3659
|
-
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3660
|
-
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3661
|
-
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3662
|
-
const recipientMintCommitment = mintCommitments.find(
|
|
3663
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3664
|
-
);
|
|
3665
|
-
const senderMintCommitment = mintCommitments.find(
|
|
3666
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3667
|
-
);
|
|
3668
|
-
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3669
|
-
throw new Error("Failed to find expected mint commitments");
|
|
3670
|
-
}
|
|
3671
|
-
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3672
|
-
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3673
|
-
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3674
|
-
recipientMintCommitment.transactionData,
|
|
3743
|
+
const buildResult = await this.buildSplitBundle(
|
|
3744
|
+
tokenToSplit,
|
|
3745
|
+
splitAmount,
|
|
3746
|
+
remainderAmount,
|
|
3747
|
+
coinIdHex,
|
|
3675
3748
|
recipientAddress,
|
|
3676
|
-
|
|
3677
|
-
this.signingService
|
|
3678
|
-
);
|
|
3679
|
-
const mintedPredicate = await UnmaskedPredicate3.create(
|
|
3680
|
-
recipientTokenId,
|
|
3681
|
-
tokenToSplit.type,
|
|
3682
|
-
this.signingService,
|
|
3683
|
-
HashAlgorithm3.SHA256,
|
|
3684
|
-
recipientSalt
|
|
3749
|
+
options
|
|
3685
3750
|
);
|
|
3686
|
-
|
|
3687
|
-
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3751
|
+
console.log("[InstantSplit] Sending via transport...");
|
|
3688
3752
|
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3689
|
-
let nametagTokenJson;
|
|
3690
|
-
const recipientAddressStr = recipientAddress.toString();
|
|
3691
|
-
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3692
|
-
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3693
|
-
}
|
|
3694
|
-
const bundle = {
|
|
3695
|
-
version: "5.0",
|
|
3696
|
-
type: "INSTANT_SPLIT",
|
|
3697
|
-
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3698
|
-
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3699
|
-
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3700
|
-
amount: splitAmount.toString(),
|
|
3701
|
-
coinId: coinIdHex,
|
|
3702
|
-
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3703
|
-
splitGroupId,
|
|
3704
|
-
senderPubkey,
|
|
3705
|
-
recipientSaltHex: toHex2(recipientSalt),
|
|
3706
|
-
transferSaltHex: toHex2(transferSalt),
|
|
3707
|
-
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3708
|
-
finalRecipientStateJson: "",
|
|
3709
|
-
// Recipient creates their own
|
|
3710
|
-
recipientAddressJson: recipientAddressStr,
|
|
3711
|
-
nametagTokenJson
|
|
3712
|
-
};
|
|
3713
|
-
console.log("[InstantSplit] Step 6: Sending via transport...");
|
|
3714
3753
|
const nostrEventId = await transport.sendTokenTransfer(recipientPubkey, {
|
|
3715
|
-
token: JSON.stringify(bundle),
|
|
3754
|
+
token: JSON.stringify(buildResult.bundle),
|
|
3716
3755
|
proof: null,
|
|
3717
3756
|
// Proof is included in the bundle
|
|
3718
3757
|
memo: options?.memo,
|
|
@@ -3723,25 +3762,13 @@ var InstantSplitExecutor = class {
|
|
|
3723
3762
|
const criticalPathDuration = performance.now() - startTime;
|
|
3724
3763
|
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3725
3764
|
options?.onNostrDelivered?.(nostrEventId);
|
|
3726
|
-
|
|
3727
|
-
if (!options?.skipBackground) {
|
|
3728
|
-
backgroundPromise = this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3729
|
-
signingService: this.signingService,
|
|
3730
|
-
tokenType: tokenToSplit.type,
|
|
3731
|
-
coinId,
|
|
3732
|
-
senderTokenId,
|
|
3733
|
-
senderSalt,
|
|
3734
|
-
onProgress: options?.onBackgroundProgress,
|
|
3735
|
-
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3736
|
-
onStorageSync: options?.onStorageSync
|
|
3737
|
-
});
|
|
3738
|
-
}
|
|
3765
|
+
const backgroundPromise = buildResult.startBackground();
|
|
3739
3766
|
return {
|
|
3740
3767
|
success: true,
|
|
3741
3768
|
nostrEventId,
|
|
3742
|
-
splitGroupId,
|
|
3769
|
+
splitGroupId: buildResult.splitGroupId,
|
|
3743
3770
|
criticalPathDurationMs: criticalPathDuration,
|
|
3744
|
-
backgroundStarted:
|
|
3771
|
+
backgroundStarted: true,
|
|
3745
3772
|
backgroundPromise
|
|
3746
3773
|
};
|
|
3747
3774
|
} catch (error) {
|
|
@@ -3750,7 +3777,6 @@ var InstantSplitExecutor = class {
|
|
|
3750
3777
|
console.error(`[InstantSplit] Failed after ${duration.toFixed(0)}ms:`, error);
|
|
3751
3778
|
return {
|
|
3752
3779
|
success: false,
|
|
3753
|
-
splitGroupId,
|
|
3754
3780
|
criticalPathDurationMs: duration,
|
|
3755
3781
|
error: errorMessage,
|
|
3756
3782
|
backgroundStarted: false
|
|
@@ -3955,6 +3981,11 @@ function isInstantSplitBundleV4(obj) {
|
|
|
3955
3981
|
function isInstantSplitBundleV5(obj) {
|
|
3956
3982
|
return isInstantSplitBundle(obj) && obj.version === "5.0";
|
|
3957
3983
|
}
|
|
3984
|
+
function isCombinedTransferBundleV6(obj) {
|
|
3985
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
3986
|
+
const b = obj;
|
|
3987
|
+
return b.version === "6.0" && b.type === "COMBINED_TRANSFER";
|
|
3988
|
+
}
|
|
3958
3989
|
|
|
3959
3990
|
// modules/payments/InstantSplitProcessor.ts
|
|
3960
3991
|
function fromHex3(hex) {
|
|
@@ -4608,6 +4639,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4608
4639
|
// Survives page reloads via KV storage so Nostr re-deliveries are ignored
|
|
4609
4640
|
// even when the confirmed token's in-memory ID differs from v5split_{id}.
|
|
4610
4641
|
processedSplitGroupIds = /* @__PURE__ */ new Set();
|
|
4642
|
+
// Persistent dedup: tracks V6 combined transfer IDs that have been processed.
|
|
4643
|
+
processedCombinedTransferIds = /* @__PURE__ */ new Set();
|
|
4611
4644
|
// Storage event subscriptions (push-based sync)
|
|
4612
4645
|
storageEventUnsubscribers = [];
|
|
4613
4646
|
syncDebounceTimer = null;
|
|
@@ -4708,10 +4741,23 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4708
4741
|
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4709
4742
|
}
|
|
4710
4743
|
}
|
|
4744
|
+
for (const [id, token] of this.tokens) {
|
|
4745
|
+
try {
|
|
4746
|
+
if (token.sdkData) {
|
|
4747
|
+
const data = JSON.parse(token.sdkData);
|
|
4748
|
+
if (data?._placeholder) {
|
|
4749
|
+
this.tokens.delete(id);
|
|
4750
|
+
console.log(`[Payments] Removed stale placeholder token: ${id}`);
|
|
4751
|
+
}
|
|
4752
|
+
}
|
|
4753
|
+
} catch {
|
|
4754
|
+
}
|
|
4755
|
+
}
|
|
4711
4756
|
const loadedTokens = Array.from(this.tokens.values()).map((t) => `${t.id.slice(0, 12)}(${t.status})`);
|
|
4712
4757
|
console.log(`[Payments][DEBUG] load(): from TXF providers: ${this.tokens.size} tokens [${loadedTokens.join(", ")}]`);
|
|
4713
4758
|
await this.loadPendingV5Tokens();
|
|
4714
4759
|
await this.loadProcessedSplitGroupIds();
|
|
4760
|
+
await this.loadProcessedCombinedTransferIds();
|
|
4715
4761
|
await this.loadHistory();
|
|
4716
4762
|
const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
|
|
4717
4763
|
if (pending2) {
|
|
@@ -4803,12 +4849,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4803
4849
|
token.status = "transferring";
|
|
4804
4850
|
this.tokens.set(token.id, token);
|
|
4805
4851
|
}
|
|
4852
|
+
await this.save();
|
|
4806
4853
|
await this.saveToOutbox(result, recipientPubkey);
|
|
4807
4854
|
result.status = "submitted";
|
|
4808
4855
|
const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
|
|
4809
4856
|
const transferMode = request.transferMode ?? "instant";
|
|
4810
|
-
if (
|
|
4811
|
-
if (
|
|
4857
|
+
if (transferMode === "conservative") {
|
|
4858
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4812
4859
|
this.log("Executing conservative split...");
|
|
4813
4860
|
const splitExecutor = new TokenSplitExecutor({
|
|
4814
4861
|
stateTransitionClient: stClient,
|
|
@@ -4852,27 +4899,59 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4852
4899
|
requestIdHex: splitRequestIdHex
|
|
4853
4900
|
});
|
|
4854
4901
|
this.log(`Conservative split transfer completed`);
|
|
4855
|
-
}
|
|
4856
|
-
|
|
4857
|
-
const
|
|
4902
|
+
}
|
|
4903
|
+
for (const tokenWithAmount of splitPlan.tokensToTransferDirectly) {
|
|
4904
|
+
const token = tokenWithAmount.uiToken;
|
|
4905
|
+
const commitment = await this.createSdkCommitment(token, recipientAddress, signingService);
|
|
4906
|
+
console.log(`[Payments] CONSERVATIVE: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4907
|
+
const submitResponse = await stClient.submitTransferCommitment(commitment);
|
|
4908
|
+
if (submitResponse.status !== "SUCCESS" && submitResponse.status !== "REQUEST_ID_EXISTS") {
|
|
4909
|
+
throw new Error(`Transfer commitment failed: ${submitResponse.status}`);
|
|
4910
|
+
}
|
|
4911
|
+
const inclusionProof = await waitInclusionProof5(trustBase, stClient, commitment);
|
|
4912
|
+
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4913
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4914
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4915
|
+
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
4916
|
+
memo: request.memo
|
|
4917
|
+
});
|
|
4918
|
+
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
4919
|
+
const requestIdBytes = commitment.requestId;
|
|
4920
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4921
|
+
result.tokenTransfers.push({
|
|
4922
|
+
sourceTokenId: token.id,
|
|
4923
|
+
method: "direct",
|
|
4924
|
+
requestIdHex
|
|
4925
|
+
});
|
|
4926
|
+
this.log(`Token ${token.id} sent via CONSERVATIVE, requestId: ${requestIdHex}`);
|
|
4927
|
+
await this.removeToken(token.id);
|
|
4928
|
+
}
|
|
4929
|
+
} else {
|
|
4930
|
+
const devMode = this.deps.oracle.isDevMode?.() ?? false;
|
|
4931
|
+
const senderPubkey = this.deps.identity.chainPubkey;
|
|
4932
|
+
let changeTokenPlaceholderId = null;
|
|
4933
|
+
let builtSplit = null;
|
|
4934
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4935
|
+
this.log("Building instant split bundle...");
|
|
4858
4936
|
const executor = new InstantSplitExecutor({
|
|
4859
4937
|
stateTransitionClient: stClient,
|
|
4860
4938
|
trustBase,
|
|
4861
4939
|
signingService,
|
|
4862
4940
|
devMode
|
|
4863
4941
|
});
|
|
4864
|
-
|
|
4942
|
+
builtSplit = await executor.buildSplitBundle(
|
|
4865
4943
|
splitPlan.tokenToSplit.sdkToken,
|
|
4866
4944
|
splitPlan.splitAmount,
|
|
4867
4945
|
splitPlan.remainderAmount,
|
|
4868
4946
|
splitPlan.coinId,
|
|
4869
4947
|
recipientAddress,
|
|
4870
|
-
this.deps.transport,
|
|
4871
|
-
recipientPubkey,
|
|
4872
4948
|
{
|
|
4873
4949
|
memo: request.memo,
|
|
4874
4950
|
onChangeTokenCreated: async (changeToken) => {
|
|
4875
4951
|
const changeTokenData = changeToken.toJSON();
|
|
4952
|
+
if (changeTokenPlaceholderId && this.tokens.has(changeTokenPlaceholderId)) {
|
|
4953
|
+
this.tokens.delete(changeTokenPlaceholderId);
|
|
4954
|
+
}
|
|
4876
4955
|
const uiToken = {
|
|
4877
4956
|
id: crypto.randomUUID(),
|
|
4878
4957
|
coinId: request.coinId,
|
|
@@ -4895,65 +4974,103 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4895
4974
|
}
|
|
4896
4975
|
}
|
|
4897
4976
|
);
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
this.
|
|
4903
|
-
|
|
4977
|
+
this.log(`Split bundle built: splitGroupId=${builtSplit.splitGroupId}`);
|
|
4978
|
+
}
|
|
4979
|
+
const directCommitments = await Promise.all(
|
|
4980
|
+
splitPlan.tokensToTransferDirectly.map(
|
|
4981
|
+
(tw) => this.createSdkCommitment(tw.uiToken, recipientAddress, signingService)
|
|
4982
|
+
)
|
|
4983
|
+
);
|
|
4984
|
+
const directTokenEntries = splitPlan.tokensToTransferDirectly.map(
|
|
4985
|
+
(tw, i) => ({
|
|
4986
|
+
sourceToken: JSON.stringify(tw.sdkToken.toJSON()),
|
|
4987
|
+
commitmentData: JSON.stringify(directCommitments[i].toJSON()),
|
|
4988
|
+
amount: tw.uiToken.amount,
|
|
4989
|
+
coinId: tw.uiToken.coinId,
|
|
4990
|
+
tokenId: extractTokenIdFromSdkData(tw.uiToken.sdkData) || void 0
|
|
4991
|
+
})
|
|
4992
|
+
);
|
|
4993
|
+
const combinedBundle = {
|
|
4994
|
+
version: "6.0",
|
|
4995
|
+
type: "COMBINED_TRANSFER",
|
|
4996
|
+
transferId: result.id,
|
|
4997
|
+
splitBundle: builtSplit?.bundle ?? null,
|
|
4998
|
+
directTokens: directTokenEntries,
|
|
4999
|
+
totalAmount: request.amount.toString(),
|
|
5000
|
+
coinId: request.coinId,
|
|
5001
|
+
senderPubkey,
|
|
5002
|
+
memo: request.memo
|
|
5003
|
+
};
|
|
5004
|
+
console.log(
|
|
5005
|
+
`[Payments] Sending V6 combined bundle: transfer=${result.id.slice(0, 8)}... split=${!!builtSplit} direct=${directTokenEntries.length}`
|
|
5006
|
+
);
|
|
5007
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
5008
|
+
token: JSON.stringify(combinedBundle),
|
|
5009
|
+
proof: null,
|
|
5010
|
+
memo: request.memo,
|
|
5011
|
+
sender: { transportPubkey: senderPubkey }
|
|
5012
|
+
});
|
|
5013
|
+
console.log(`[Payments] V6 combined bundle sent successfully`);
|
|
5014
|
+
if (builtSplit) {
|
|
5015
|
+
const bgPromise = builtSplit.startBackground();
|
|
5016
|
+
this.pendingBackgroundTasks.push(bgPromise);
|
|
5017
|
+
}
|
|
5018
|
+
if (builtSplit && splitPlan.remainderAmount) {
|
|
5019
|
+
changeTokenPlaceholderId = crypto.randomUUID();
|
|
5020
|
+
const placeholder = {
|
|
5021
|
+
id: changeTokenPlaceholderId,
|
|
5022
|
+
coinId: request.coinId,
|
|
5023
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
5024
|
+
name: this.getCoinName(request.coinId),
|
|
5025
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
5026
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
5027
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
5028
|
+
status: "transferring",
|
|
5029
|
+
createdAt: Date.now(),
|
|
5030
|
+
updatedAt: Date.now(),
|
|
5031
|
+
sdkData: JSON.stringify({ _placeholder: true })
|
|
5032
|
+
};
|
|
5033
|
+
this.tokens.set(placeholder.id, placeholder);
|
|
5034
|
+
this.log(`Placeholder change token created: ${placeholder.id} (${placeholder.amount})`);
|
|
5035
|
+
}
|
|
5036
|
+
for (const commitment of directCommitments) {
|
|
5037
|
+
stClient.submitTransferCommitment(commitment).catch(
|
|
5038
|
+
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
5039
|
+
);
|
|
5040
|
+
}
|
|
5041
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4904
5042
|
await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
|
|
4905
5043
|
result.tokenTransfers.push({
|
|
4906
5044
|
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4907
5045
|
method: "split",
|
|
4908
|
-
splitGroupId:
|
|
4909
|
-
nostrEventId: instantResult.nostrEventId
|
|
5046
|
+
splitGroupId: builtSplit.splitGroupId
|
|
4910
5047
|
});
|
|
4911
|
-
this.log(`Instant split transfer completed`);
|
|
4912
5048
|
}
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
}
|
|
4923
|
-
const inclusionProof = await waitInclusionProof5(trustBase, stClient, commitment);
|
|
4924
|
-
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4925
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4926
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4927
|
-
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
4928
|
-
memo: request.memo
|
|
4929
|
-
});
|
|
4930
|
-
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
4931
|
-
} else {
|
|
4932
|
-
console.log(`[Payments] NOSTR-FIRST: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4933
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4934
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4935
|
-
commitmentData: JSON.stringify(commitment.toJSON()),
|
|
4936
|
-
memo: request.memo
|
|
5049
|
+
for (let i = 0; i < splitPlan.tokensToTransferDirectly.length; i++) {
|
|
5050
|
+
const token = splitPlan.tokensToTransferDirectly[i].uiToken;
|
|
5051
|
+
const commitment = directCommitments[i];
|
|
5052
|
+
const requestIdBytes = commitment.requestId;
|
|
5053
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
5054
|
+
result.tokenTransfers.push({
|
|
5055
|
+
sourceTokenId: token.id,
|
|
5056
|
+
method: "direct",
|
|
5057
|
+
requestIdHex
|
|
4937
5058
|
});
|
|
4938
|
-
|
|
4939
|
-
stClient.submitTransferCommitment(commitment).catch(
|
|
4940
|
-
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
4941
|
-
);
|
|
5059
|
+
await this.removeToken(token.id);
|
|
4942
5060
|
}
|
|
4943
|
-
|
|
4944
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4945
|
-
result.tokenTransfers.push({
|
|
4946
|
-
sourceTokenId: token.id,
|
|
4947
|
-
method: "direct",
|
|
4948
|
-
requestIdHex
|
|
4949
|
-
});
|
|
4950
|
-
this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
|
|
4951
|
-
await this.removeToken(token.id);
|
|
5061
|
+
this.log(`V6 combined transfer completed`);
|
|
4952
5062
|
}
|
|
4953
5063
|
result.status = "delivered";
|
|
4954
5064
|
await this.save();
|
|
4955
5065
|
await this.removeFromOutbox(result.id);
|
|
4956
5066
|
result.status = "completed";
|
|
5067
|
+
const tokenMap = new Map(result.tokens.map((t) => [t.id, t]));
|
|
5068
|
+
const sentTokenIds = result.tokenTransfers.map((tt) => ({
|
|
5069
|
+
id: tt.sourceTokenId,
|
|
5070
|
+
// For split tokens, use splitAmount (the portion sent), not the original token amount
|
|
5071
|
+
amount: tt.method === "split" ? splitPlan.splitAmount?.toString() || "0" : tokenMap.get(tt.sourceTokenId)?.amount || "0",
|
|
5072
|
+
source: tt.method === "split" ? "split" : "direct"
|
|
5073
|
+
}));
|
|
4957
5074
|
const sentTokenId = result.tokens[0] ? extractTokenIdFromSdkData(result.tokens[0].sdkData) : void 0;
|
|
4958
5075
|
await this.addToHistory({
|
|
4959
5076
|
type: "SENT",
|
|
@@ -4966,7 +5083,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4966
5083
|
recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
|
|
4967
5084
|
memo: request.memo,
|
|
4968
5085
|
transferId: result.id,
|
|
4969
|
-
tokenId: sentTokenId || void 0
|
|
5086
|
+
tokenId: sentTokenId || void 0,
|
|
5087
|
+
tokenIds: sentTokenIds.length > 0 ? sentTokenIds : void 0
|
|
4970
5088
|
});
|
|
4971
5089
|
this.deps.emitEvent("transfer:confirmed", result);
|
|
4972
5090
|
return result;
|
|
@@ -5136,6 +5254,267 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5136
5254
|
};
|
|
5137
5255
|
}
|
|
5138
5256
|
}
|
|
5257
|
+
// ===========================================================================
|
|
5258
|
+
// Shared Helpers for V5 and V6 Receiver Processing
|
|
5259
|
+
// ===========================================================================
|
|
5260
|
+
/**
|
|
5261
|
+
* Save a V5 split bundle as an unconfirmed token (shared by V5 standalone and V6 combined).
|
|
5262
|
+
* Returns the created UI token, or null if deduped.
|
|
5263
|
+
*
|
|
5264
|
+
* @param deferPersistence - If true, skip addToken/save calls (caller batches them).
|
|
5265
|
+
* The token is still added to the in-memory map for dedup; caller must call save().
|
|
5266
|
+
*/
|
|
5267
|
+
async saveUnconfirmedV5Token(bundle, senderPubkey, deferPersistence = false) {
|
|
5268
|
+
const deterministicId = `v5split_${bundle.splitGroupId}`;
|
|
5269
|
+
if (this.tokens.has(deterministicId) || this.processedSplitGroupIds.has(bundle.splitGroupId)) {
|
|
5270
|
+
console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
|
|
5271
|
+
return null;
|
|
5272
|
+
}
|
|
5273
|
+
const registry = TokenRegistry.getInstance();
|
|
5274
|
+
const pendingData = {
|
|
5275
|
+
type: "v5_bundle",
|
|
5276
|
+
stage: "RECEIVED",
|
|
5277
|
+
bundleJson: JSON.stringify(bundle),
|
|
5278
|
+
senderPubkey,
|
|
5279
|
+
savedAt: Date.now(),
|
|
5280
|
+
attemptCount: 0
|
|
5281
|
+
};
|
|
5282
|
+
const uiToken = {
|
|
5283
|
+
id: deterministicId,
|
|
5284
|
+
coinId: bundle.coinId,
|
|
5285
|
+
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
5286
|
+
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
5287
|
+
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
5288
|
+
amount: bundle.amount,
|
|
5289
|
+
status: "submitted",
|
|
5290
|
+
// UNCONFIRMED
|
|
5291
|
+
createdAt: Date.now(),
|
|
5292
|
+
updatedAt: Date.now(),
|
|
5293
|
+
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
5294
|
+
};
|
|
5295
|
+
this.processedSplitGroupIds.add(bundle.splitGroupId);
|
|
5296
|
+
if (deferPersistence) {
|
|
5297
|
+
this.tokens.set(uiToken.id, uiToken);
|
|
5298
|
+
} else {
|
|
5299
|
+
await this.addToken(uiToken);
|
|
5300
|
+
await this.saveProcessedSplitGroupIds();
|
|
5301
|
+
}
|
|
5302
|
+
return uiToken;
|
|
5303
|
+
}
|
|
5304
|
+
/**
|
|
5305
|
+
* Save a commitment-only (NOSTR-FIRST) token and start proof polling.
|
|
5306
|
+
* Shared by standalone NOSTR-FIRST handler and V6 combined handler.
|
|
5307
|
+
* Returns the created UI token, or null if deduped/tombstoned.
|
|
5308
|
+
*
|
|
5309
|
+
* @param deferPersistence - If true, skip save() and commitment submission
|
|
5310
|
+
* (caller batches them). Token is added to in-memory map + proof polling is queued.
|
|
5311
|
+
* @param skipGenesisDedup - If true, skip genesis-ID-only dedup. V6 handler sets this
|
|
5312
|
+
* because bundle-level dedup protects against replays, and split children share genesis IDs.
|
|
5313
|
+
*/
|
|
5314
|
+
async saveCommitmentOnlyToken(sourceTokenInput, commitmentInput, senderPubkey, deferPersistence = false, skipGenesisDedup = false) {
|
|
5315
|
+
const tokenInfo = await parseTokenInfo(sourceTokenInput);
|
|
5316
|
+
const sdkData = typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput);
|
|
5317
|
+
const nostrTokenId = extractTokenIdFromSdkData(sdkData);
|
|
5318
|
+
const nostrStateHash = extractStateHashFromSdkData(sdkData);
|
|
5319
|
+
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
5320
|
+
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
5321
|
+
return null;
|
|
5322
|
+
}
|
|
5323
|
+
if (nostrTokenId) {
|
|
5324
|
+
for (const existing of this.tokens.values()) {
|
|
5325
|
+
const existingTokenId = extractTokenIdFromSdkData(existing.sdkData);
|
|
5326
|
+
if (existingTokenId !== nostrTokenId) continue;
|
|
5327
|
+
const existingStateHash = extractStateHashFromSdkData(existing.sdkData);
|
|
5328
|
+
if (nostrStateHash && existingStateHash === nostrStateHash) {
|
|
5329
|
+
console.log(
|
|
5330
|
+
`[Payments] NOSTR-FIRST: Skipping duplicate token state ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`
|
|
5331
|
+
);
|
|
5332
|
+
return null;
|
|
5333
|
+
}
|
|
5334
|
+
if (!skipGenesisDedup) {
|
|
5335
|
+
console.log(
|
|
5336
|
+
`[Payments] NOSTR-FIRST: Skipping replay of finalized token ${nostrTokenId.slice(0, 8)}...`
|
|
5337
|
+
);
|
|
5338
|
+
return null;
|
|
5339
|
+
}
|
|
5340
|
+
}
|
|
5341
|
+
}
|
|
5342
|
+
const token = {
|
|
5343
|
+
id: crypto.randomUUID(),
|
|
5344
|
+
coinId: tokenInfo.coinId,
|
|
5345
|
+
symbol: tokenInfo.symbol,
|
|
5346
|
+
name: tokenInfo.name,
|
|
5347
|
+
decimals: tokenInfo.decimals,
|
|
5348
|
+
iconUrl: tokenInfo.iconUrl,
|
|
5349
|
+
amount: tokenInfo.amount,
|
|
5350
|
+
status: "submitted",
|
|
5351
|
+
// NOSTR-FIRST: unconfirmed until proof
|
|
5352
|
+
createdAt: Date.now(),
|
|
5353
|
+
updatedAt: Date.now(),
|
|
5354
|
+
sdkData
|
|
5355
|
+
};
|
|
5356
|
+
this.tokens.set(token.id, token);
|
|
5357
|
+
if (!deferPersistence) {
|
|
5358
|
+
await this.save();
|
|
5359
|
+
}
|
|
5360
|
+
try {
|
|
5361
|
+
const commitment = await TransferCommitment4.fromJSON(commitmentInput);
|
|
5362
|
+
const requestIdBytes = commitment.requestId;
|
|
5363
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
5364
|
+
if (!deferPersistence) {
|
|
5365
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5366
|
+
if (stClient) {
|
|
5367
|
+
const response = await stClient.submitTransferCommitment(commitment);
|
|
5368
|
+
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
this.addProofPollingJob({
|
|
5372
|
+
tokenId: token.id,
|
|
5373
|
+
requestIdHex,
|
|
5374
|
+
commitmentJson: JSON.stringify(commitmentInput),
|
|
5375
|
+
startedAt: Date.now(),
|
|
5376
|
+
attemptCount: 0,
|
|
5377
|
+
lastAttemptAt: 0,
|
|
5378
|
+
onProofReceived: async (tokenId) => {
|
|
5379
|
+
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
|
|
5380
|
+
}
|
|
5381
|
+
});
|
|
5382
|
+
} catch (err) {
|
|
5383
|
+
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
5384
|
+
}
|
|
5385
|
+
return token;
|
|
5386
|
+
}
|
|
5387
|
+
// ===========================================================================
|
|
5388
|
+
// Combined Transfer V6 — Receiver
|
|
5389
|
+
// ===========================================================================
|
|
5390
|
+
/**
|
|
5391
|
+
* Process a received COMBINED_TRANSFER V6 bundle.
|
|
5392
|
+
*
|
|
5393
|
+
* Unpacks a single Nostr message into its component tokens:
|
|
5394
|
+
* - Optional V5 split bundle (saved as unconfirmed, resolved lazily)
|
|
5395
|
+
* - Zero or more direct tokens (saved as unconfirmed, proof-polled)
|
|
5396
|
+
*
|
|
5397
|
+
* Emits ONE transfer:incoming event and records ONE history entry.
|
|
5398
|
+
*/
|
|
5399
|
+
async processCombinedTransferBundle(bundle, senderPubkey) {
|
|
5400
|
+
this.ensureInitialized();
|
|
5401
|
+
if (!this.loaded && this.loadedPromise) {
|
|
5402
|
+
await this.loadedPromise;
|
|
5403
|
+
}
|
|
5404
|
+
if (this.processedCombinedTransferIds.has(bundle.transferId)) {
|
|
5405
|
+
console.log(`[Payments] V6 combined transfer ${bundle.transferId.slice(0, 12)}... already processed, skipping`);
|
|
5406
|
+
return;
|
|
5407
|
+
}
|
|
5408
|
+
console.log(
|
|
5409
|
+
`[Payments] Processing V6 combined transfer ${bundle.transferId.slice(0, 12)}... (split=${!!bundle.splitBundle}, direct=${bundle.directTokens.length})`
|
|
5410
|
+
);
|
|
5411
|
+
const allTokens = [];
|
|
5412
|
+
const tokenBreakdown = [];
|
|
5413
|
+
const parsedDirectEntries = bundle.directTokens.map((entry) => ({
|
|
5414
|
+
sourceToken: typeof entry.sourceToken === "string" ? JSON.parse(entry.sourceToken) : entry.sourceToken,
|
|
5415
|
+
commitment: typeof entry.commitmentData === "string" ? JSON.parse(entry.commitmentData) : entry.commitmentData
|
|
5416
|
+
}));
|
|
5417
|
+
if (bundle.splitBundle) {
|
|
5418
|
+
const splitToken = await this.saveUnconfirmedV5Token(bundle.splitBundle, senderPubkey, true);
|
|
5419
|
+
if (splitToken) {
|
|
5420
|
+
allTokens.push(splitToken);
|
|
5421
|
+
tokenBreakdown.push({ id: splitToken.id, amount: splitToken.amount, source: "split" });
|
|
5422
|
+
} else {
|
|
5423
|
+
console.warn(`[Payments] V6: split token was deduped/failed \u2014 amount=${bundle.splitBundle.amount}`);
|
|
5424
|
+
}
|
|
5425
|
+
}
|
|
5426
|
+
const directResults = await Promise.all(
|
|
5427
|
+
parsedDirectEntries.map(
|
|
5428
|
+
({ sourceToken, commitment }) => this.saveCommitmentOnlyToken(sourceToken, commitment, senderPubkey, true, true)
|
|
5429
|
+
)
|
|
5430
|
+
);
|
|
5431
|
+
for (let i = 0; i < directResults.length; i++) {
|
|
5432
|
+
const token = directResults[i];
|
|
5433
|
+
if (token) {
|
|
5434
|
+
allTokens.push(token);
|
|
5435
|
+
tokenBreakdown.push({ id: token.id, amount: token.amount, source: "direct" });
|
|
5436
|
+
} else {
|
|
5437
|
+
const entry = bundle.directTokens[i];
|
|
5438
|
+
console.warn(
|
|
5439
|
+
`[Payments] V6: direct token #${i} dropped (amount=${entry.amount}, tokenId=${entry.tokenId?.slice(0, 12) ?? "N/A"})`
|
|
5440
|
+
);
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5443
|
+
if (allTokens.length === 0) {
|
|
5444
|
+
console.log(`[Payments] V6 combined transfer: all tokens deduped, nothing to save`);
|
|
5445
|
+
return;
|
|
5446
|
+
}
|
|
5447
|
+
this.processedCombinedTransferIds.add(bundle.transferId);
|
|
5448
|
+
const [senderInfo] = await Promise.all([
|
|
5449
|
+
this.resolveSenderInfo(senderPubkey),
|
|
5450
|
+
this.save(),
|
|
5451
|
+
this.saveProcessedCombinedTransferIds(),
|
|
5452
|
+
...bundle.splitBundle ? [this.saveProcessedSplitGroupIds()] : []
|
|
5453
|
+
]);
|
|
5454
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5455
|
+
if (stClient) {
|
|
5456
|
+
for (const { commitment } of parsedDirectEntries) {
|
|
5457
|
+
TransferCommitment4.fromJSON(commitment).then(
|
|
5458
|
+
(c) => stClient.submitTransferCommitment(c)
|
|
5459
|
+
).catch(
|
|
5460
|
+
(err) => console.error("[Payments] V6 background commitment submit failed:", err)
|
|
5461
|
+
);
|
|
5462
|
+
}
|
|
5463
|
+
}
|
|
5464
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
5465
|
+
id: bundle.transferId,
|
|
5466
|
+
senderPubkey,
|
|
5467
|
+
senderNametag: senderInfo.senderNametag,
|
|
5468
|
+
tokens: allTokens,
|
|
5469
|
+
memo: bundle.memo,
|
|
5470
|
+
receivedAt: Date.now()
|
|
5471
|
+
});
|
|
5472
|
+
const actualAmount = allTokens.reduce((sum, t) => sum + BigInt(t.amount || "0"), 0n).toString();
|
|
5473
|
+
await this.addToHistory({
|
|
5474
|
+
type: "RECEIVED",
|
|
5475
|
+
amount: actualAmount,
|
|
5476
|
+
coinId: bundle.coinId,
|
|
5477
|
+
symbol: allTokens[0]?.symbol || bundle.coinId,
|
|
5478
|
+
timestamp: Date.now(),
|
|
5479
|
+
senderPubkey,
|
|
5480
|
+
...senderInfo,
|
|
5481
|
+
memo: bundle.memo,
|
|
5482
|
+
transferId: bundle.transferId,
|
|
5483
|
+
tokenId: allTokens[0]?.id,
|
|
5484
|
+
tokenIds: tokenBreakdown
|
|
5485
|
+
});
|
|
5486
|
+
if (bundle.splitBundle) {
|
|
5487
|
+
this.resolveUnconfirmed().catch(() => {
|
|
5488
|
+
});
|
|
5489
|
+
this.scheduleResolveUnconfirmed();
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
/**
|
|
5493
|
+
* Persist processed combined transfer IDs to KV storage.
|
|
5494
|
+
*/
|
|
5495
|
+
async saveProcessedCombinedTransferIds() {
|
|
5496
|
+
const ids = Array.from(this.processedCombinedTransferIds);
|
|
5497
|
+
if (ids.length > 0) {
|
|
5498
|
+
await this.deps.storage.set(
|
|
5499
|
+
STORAGE_KEYS_ADDRESS.PROCESSED_COMBINED_TRANSFER_IDS,
|
|
5500
|
+
JSON.stringify(ids)
|
|
5501
|
+
);
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
/**
|
|
5505
|
+
* Load processed combined transfer IDs from KV storage.
|
|
5506
|
+
*/
|
|
5507
|
+
async loadProcessedCombinedTransferIds() {
|
|
5508
|
+
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PROCESSED_COMBINED_TRANSFER_IDS);
|
|
5509
|
+
if (!data) return;
|
|
5510
|
+
try {
|
|
5511
|
+
const ids = JSON.parse(data);
|
|
5512
|
+
for (const id of ids) {
|
|
5513
|
+
this.processedCombinedTransferIds.add(id);
|
|
5514
|
+
}
|
|
5515
|
+
} catch {
|
|
5516
|
+
}
|
|
5517
|
+
}
|
|
5139
5518
|
/**
|
|
5140
5519
|
* Process a received INSTANT_SPLIT bundle.
|
|
5141
5520
|
*
|
|
@@ -5159,36 +5538,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5159
5538
|
return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
|
|
5160
5539
|
}
|
|
5161
5540
|
try {
|
|
5162
|
-
const
|
|
5163
|
-
if (
|
|
5164
|
-
console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
|
|
5541
|
+
const uiToken = await this.saveUnconfirmedV5Token(bundle, senderPubkey);
|
|
5542
|
+
if (!uiToken) {
|
|
5165
5543
|
return { success: true, durationMs: 0 };
|
|
5166
5544
|
}
|
|
5167
|
-
const registry = TokenRegistry.getInstance();
|
|
5168
|
-
const pendingData = {
|
|
5169
|
-
type: "v5_bundle",
|
|
5170
|
-
stage: "RECEIVED",
|
|
5171
|
-
bundleJson: JSON.stringify(bundle),
|
|
5172
|
-
senderPubkey,
|
|
5173
|
-
savedAt: Date.now(),
|
|
5174
|
-
attemptCount: 0
|
|
5175
|
-
};
|
|
5176
|
-
const uiToken = {
|
|
5177
|
-
id: deterministicId,
|
|
5178
|
-
coinId: bundle.coinId,
|
|
5179
|
-
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
5180
|
-
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
5181
|
-
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
5182
|
-
amount: bundle.amount,
|
|
5183
|
-
status: "submitted",
|
|
5184
|
-
// UNCONFIRMED
|
|
5185
|
-
createdAt: Date.now(),
|
|
5186
|
-
updatedAt: Date.now(),
|
|
5187
|
-
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
5188
|
-
};
|
|
5189
|
-
await this.addToken(uiToken);
|
|
5190
|
-
this.processedSplitGroupIds.add(bundle.splitGroupId);
|
|
5191
|
-
await this.saveProcessedSplitGroupIds();
|
|
5192
5545
|
const senderInfo = await this.resolveSenderInfo(senderPubkey);
|
|
5193
5546
|
await this.addToHistory({
|
|
5194
5547
|
type: "RECEIVED",
|
|
@@ -5199,7 +5552,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5199
5552
|
senderPubkey,
|
|
5200
5553
|
...senderInfo,
|
|
5201
5554
|
memo,
|
|
5202
|
-
tokenId:
|
|
5555
|
+
tokenId: uiToken.id
|
|
5203
5556
|
});
|
|
5204
5557
|
this.deps.emitEvent("transfer:incoming", {
|
|
5205
5558
|
id: bundle.splitGroupId,
|
|
@@ -5849,16 +6202,18 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5849
6202
|
}
|
|
5850
6203
|
/**
|
|
5851
6204
|
* Aggregate tokens by coinId with confirmed/unconfirmed breakdown.
|
|
5852
|
-
* Excludes tokens with status 'spent'
|
|
6205
|
+
* Excludes tokens with status 'spent' or 'invalid'.
|
|
6206
|
+
* Tokens with status 'transferring' are counted as unconfirmed (visible in UI as "Sending").
|
|
5853
6207
|
*/
|
|
5854
6208
|
aggregateTokens(coinId) {
|
|
5855
6209
|
const assetsMap = /* @__PURE__ */ new Map();
|
|
5856
6210
|
for (const token of this.tokens.values()) {
|
|
5857
|
-
if (token.status === "spent" || token.status === "invalid"
|
|
6211
|
+
if (token.status === "spent" || token.status === "invalid") continue;
|
|
5858
6212
|
if (coinId && token.coinId !== coinId) continue;
|
|
5859
6213
|
const key = token.coinId;
|
|
5860
6214
|
const amount = BigInt(token.amount);
|
|
5861
6215
|
const isConfirmed = token.status === "confirmed";
|
|
6216
|
+
const isTransferring = token.status === "transferring";
|
|
5862
6217
|
const existing = assetsMap.get(key);
|
|
5863
6218
|
if (existing) {
|
|
5864
6219
|
if (isConfirmed) {
|
|
@@ -5868,6 +6223,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5868
6223
|
existing.unconfirmedAmount += amount;
|
|
5869
6224
|
existing.unconfirmedTokenCount++;
|
|
5870
6225
|
}
|
|
6226
|
+
if (isTransferring) existing.transferringTokenCount++;
|
|
5871
6227
|
} else {
|
|
5872
6228
|
assetsMap.set(key, {
|
|
5873
6229
|
coinId: token.coinId,
|
|
@@ -5878,7 +6234,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5878
6234
|
confirmedAmount: isConfirmed ? amount : 0n,
|
|
5879
6235
|
unconfirmedAmount: isConfirmed ? 0n : amount,
|
|
5880
6236
|
confirmedTokenCount: isConfirmed ? 1 : 0,
|
|
5881
|
-
unconfirmedTokenCount: isConfirmed ? 0 : 1
|
|
6237
|
+
unconfirmedTokenCount: isConfirmed ? 0 : 1,
|
|
6238
|
+
transferringTokenCount: isTransferring ? 1 : 0
|
|
5882
6239
|
});
|
|
5883
6240
|
}
|
|
5884
6241
|
}
|
|
@@ -5896,6 +6253,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5896
6253
|
unconfirmedAmount: raw.unconfirmedAmount.toString(),
|
|
5897
6254
|
confirmedTokenCount: raw.confirmedTokenCount,
|
|
5898
6255
|
unconfirmedTokenCount: raw.unconfirmedTokenCount,
|
|
6256
|
+
transferringTokenCount: raw.transferringTokenCount,
|
|
5899
6257
|
priceUsd: null,
|
|
5900
6258
|
priceEur: null,
|
|
5901
6259
|
change24h: null,
|
|
@@ -7293,7 +7651,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7293
7651
|
/**
|
|
7294
7652
|
* Handle NOSTR-FIRST commitment-only transfer (recipient side)
|
|
7295
7653
|
* This is called when receiving a transfer with only commitmentData and no proof yet.
|
|
7296
|
-
*
|
|
7654
|
+
* Delegates to saveCommitmentOnlyToken() helper, then emits event + records history.
|
|
7297
7655
|
*/
|
|
7298
7656
|
async handleCommitmentOnlyTransfer(transfer, payload) {
|
|
7299
7657
|
try {
|
|
@@ -7303,41 +7661,22 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7303
7661
|
console.warn("[Payments] Invalid NOSTR-FIRST transfer format");
|
|
7304
7662
|
return;
|
|
7305
7663
|
}
|
|
7306
|
-
const
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
decimals: tokenInfo.decimals,
|
|
7313
|
-
iconUrl: tokenInfo.iconUrl,
|
|
7314
|
-
amount: tokenInfo.amount,
|
|
7315
|
-
status: "submitted",
|
|
7316
|
-
// NOSTR-FIRST: unconfirmed until proof
|
|
7317
|
-
createdAt: Date.now(),
|
|
7318
|
-
updatedAt: Date.now(),
|
|
7319
|
-
sdkData: typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput)
|
|
7320
|
-
};
|
|
7321
|
-
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7322
|
-
const nostrStateHash = extractStateHashFromSdkData(token.sdkData);
|
|
7323
|
-
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
7324
|
-
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
7325
|
-
return;
|
|
7326
|
-
}
|
|
7327
|
-
this.tokens.set(token.id, token);
|
|
7328
|
-
console.log(`[Payments][DEBUG] NOSTR-FIRST: saving token id=${token.id.slice(0, 16)} status=${token.status} sdkData.length=${token.sdkData?.length}`);
|
|
7329
|
-
await this.save();
|
|
7330
|
-
console.log(`[Payments][DEBUG] NOSTR-FIRST: save() completed, tokens.size=${this.tokens.size}`);
|
|
7664
|
+
const token = await this.saveCommitmentOnlyToken(
|
|
7665
|
+
sourceTokenInput,
|
|
7666
|
+
commitmentInput,
|
|
7667
|
+
transfer.senderTransportPubkey
|
|
7668
|
+
);
|
|
7669
|
+
if (!token) return;
|
|
7331
7670
|
const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
|
|
7332
|
-
|
|
7671
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
7333
7672
|
id: transfer.id,
|
|
7334
7673
|
senderPubkey: transfer.senderTransportPubkey,
|
|
7335
7674
|
senderNametag: senderInfo.senderNametag,
|
|
7336
7675
|
tokens: [token],
|
|
7337
7676
|
memo: payload.memo,
|
|
7338
7677
|
receivedAt: transfer.timestamp
|
|
7339
|
-
};
|
|
7340
|
-
|
|
7678
|
+
});
|
|
7679
|
+
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7341
7680
|
await this.addToHistory({
|
|
7342
7681
|
type: "RECEIVED",
|
|
7343
7682
|
amount: token.amount,
|
|
@@ -7349,29 +7688,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7349
7688
|
memo: payload.memo,
|
|
7350
7689
|
tokenId: nostrTokenId || token.id
|
|
7351
7690
|
});
|
|
7352
|
-
try {
|
|
7353
|
-
const commitment = await TransferCommitment4.fromJSON(commitmentInput);
|
|
7354
|
-
const requestIdBytes = commitment.requestId;
|
|
7355
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
7356
|
-
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
7357
|
-
if (stClient) {
|
|
7358
|
-
const response = await stClient.submitTransferCommitment(commitment);
|
|
7359
|
-
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
7360
|
-
}
|
|
7361
|
-
this.addProofPollingJob({
|
|
7362
|
-
tokenId: token.id,
|
|
7363
|
-
requestIdHex,
|
|
7364
|
-
commitmentJson: JSON.stringify(commitmentInput),
|
|
7365
|
-
startedAt: Date.now(),
|
|
7366
|
-
attemptCount: 0,
|
|
7367
|
-
lastAttemptAt: 0,
|
|
7368
|
-
onProofReceived: async (tokenId) => {
|
|
7369
|
-
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
|
|
7370
|
-
}
|
|
7371
|
-
});
|
|
7372
|
-
} catch (err) {
|
|
7373
|
-
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
7374
|
-
}
|
|
7375
7691
|
} catch (error) {
|
|
7376
7692
|
console.error("[Payments] Failed to process NOSTR-FIRST transfer:", error);
|
|
7377
7693
|
}
|
|
@@ -7490,6 +7806,28 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7490
7806
|
try {
|
|
7491
7807
|
const payload = transfer.payload;
|
|
7492
7808
|
console.log("[Payments][DEBUG] handleIncomingTransfer: keys=", Object.keys(payload).join(","));
|
|
7809
|
+
let combinedBundle = null;
|
|
7810
|
+
if (isCombinedTransferBundleV6(payload)) {
|
|
7811
|
+
combinedBundle = payload;
|
|
7812
|
+
} else if (payload.token) {
|
|
7813
|
+
try {
|
|
7814
|
+
const inner = typeof payload.token === "string" ? JSON.parse(payload.token) : payload.token;
|
|
7815
|
+
if (isCombinedTransferBundleV6(inner)) {
|
|
7816
|
+
combinedBundle = inner;
|
|
7817
|
+
}
|
|
7818
|
+
} catch {
|
|
7819
|
+
}
|
|
7820
|
+
}
|
|
7821
|
+
if (combinedBundle) {
|
|
7822
|
+
this.log("Processing COMBINED_TRANSFER V6 bundle...");
|
|
7823
|
+
try {
|
|
7824
|
+
await this.processCombinedTransferBundle(combinedBundle, transfer.senderTransportPubkey);
|
|
7825
|
+
this.log("COMBINED_TRANSFER V6 processed successfully");
|
|
7826
|
+
} catch (err) {
|
|
7827
|
+
console.error("[Payments] COMBINED_TRANSFER V6 processing error:", err);
|
|
7828
|
+
}
|
|
7829
|
+
return;
|
|
7830
|
+
}
|
|
7493
7831
|
let instantBundle = null;
|
|
7494
7832
|
if (isInstantSplitBundle(payload)) {
|
|
7495
7833
|
instantBundle = payload;
|
|
@@ -7641,17 +7979,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7641
7979
|
memo: payload.memo,
|
|
7642
7980
|
tokenId: incomingTokenId || token.id
|
|
7643
7981
|
});
|
|
7982
|
+
const incomingTransfer = {
|
|
7983
|
+
id: transfer.id,
|
|
7984
|
+
senderPubkey: transfer.senderTransportPubkey,
|
|
7985
|
+
senderNametag: senderInfo.senderNametag,
|
|
7986
|
+
tokens: [token],
|
|
7987
|
+
memo: payload.memo,
|
|
7988
|
+
receivedAt: transfer.timestamp
|
|
7989
|
+
};
|
|
7990
|
+
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
7991
|
+
this.log(`Incoming transfer processed: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7992
|
+
} else {
|
|
7993
|
+
this.log(`Duplicate transfer ignored: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7644
7994
|
}
|
|
7645
|
-
const incomingTransfer = {
|
|
7646
|
-
id: transfer.id,
|
|
7647
|
-
senderPubkey: transfer.senderTransportPubkey,
|
|
7648
|
-
senderNametag: senderInfo.senderNametag,
|
|
7649
|
-
tokens: [token],
|
|
7650
|
-
memo: payload.memo,
|
|
7651
|
-
receivedAt: transfer.timestamp
|
|
7652
|
-
};
|
|
7653
|
-
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
7654
|
-
this.log(`Incoming transfer processed: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7655
7995
|
} catch (error) {
|
|
7656
7996
|
console.error("[Payments] Failed to process incoming transfer:", error);
|
|
7657
7997
|
}
|
|
@@ -16772,6 +17112,7 @@ export {
|
|
|
16772
17112
|
identityFromMnemonicSync,
|
|
16773
17113
|
initSphere,
|
|
16774
17114
|
isArchivedKey,
|
|
17115
|
+
isCombinedTransferBundleV6,
|
|
16775
17116
|
isForkedKey,
|
|
16776
17117
|
isInstantSplitBundle,
|
|
16777
17118
|
isInstantSplitBundleV4,
|