@unicitylabs/sphere-sdk 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connect/index.cjs +5 -1
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/index.js +5 -1
- package/dist/connect/index.js.map +1 -1
- package/dist/core/index.cjs +813 -309
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +71 -2
- package/dist/core/index.d.ts +71 -2
- package/dist/core/index.js +813 -309
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/connect/index.cjs +5 -1
- package/dist/impl/browser/connect/index.cjs.map +1 -1
- package/dist/impl/browser/connect/index.js +5 -1
- package/dist/impl/browser/connect/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +7 -2
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +7 -2
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/browser/ipfs.cjs +5 -1
- package/dist/impl/browser/ipfs.cjs.map +1 -1
- package/dist/impl/browser/ipfs.js +5 -1
- package/dist/impl/browser/ipfs.js.map +1 -1
- package/dist/impl/nodejs/connect/index.cjs +5 -1
- package/dist/impl/nodejs/connect/index.cjs.map +1 -1
- package/dist/impl/nodejs/connect/index.js +5 -1
- package/dist/impl/nodejs/connect/index.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +7 -2
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +6 -0
- package/dist/impl/nodejs/index.d.ts +6 -0
- package/dist/impl/nodejs/index.js +7 -2
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +815 -309
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +144 -3
- package/dist/index.d.ts +144 -3
- package/dist/index.js +814 -309
- package/dist/index.js.map +1 -1
- package/dist/l1/index.cjs +5 -1
- package/dist/l1/index.cjs.map +1 -1
- package/dist/l1/index.js +5 -1
- package/dist/l1/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -91,7 +91,11 @@ var init_constants = __esm({
|
|
|
91
91
|
/** Group chat: members for this address */
|
|
92
92
|
GROUP_CHAT_MEMBERS: "group_chat_members",
|
|
93
93
|
/** Group chat: processed event IDs for deduplication */
|
|
94
|
-
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events"
|
|
94
|
+
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
|
|
95
|
+
/** Processed V5 split group IDs for Nostr re-delivery dedup */
|
|
96
|
+
PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids",
|
|
97
|
+
/** Processed V6 combined transfer IDs for Nostr re-delivery dedup */
|
|
98
|
+
PROCESSED_COMBINED_TRANSFER_IDS: "processed_combined_transfer_ids"
|
|
95
99
|
};
|
|
96
100
|
STORAGE_KEYS = {
|
|
97
101
|
...STORAGE_KEYS_GLOBAL,
|
|
@@ -3579,14 +3583,149 @@ var InstantSplitExecutor = class {
|
|
|
3579
3583
|
this.devMode = config.devMode ?? false;
|
|
3580
3584
|
}
|
|
3581
3585
|
/**
|
|
3582
|
-
*
|
|
3586
|
+
* Build a V5 split bundle WITHOUT sending it via transport.
|
|
3583
3587
|
*
|
|
3584
|
-
*
|
|
3588
|
+
* Steps 1-5 of the V5 flow:
|
|
3585
3589
|
* 1. Create and submit burn commitment
|
|
3586
3590
|
* 2. Wait for burn proof
|
|
3587
3591
|
* 3. Create mint commitments with SplitMintReason
|
|
3588
3592
|
* 4. Create transfer commitment (no mint proof needed)
|
|
3589
|
-
* 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.
|
|
3590
3729
|
*
|
|
3591
3730
|
* @param tokenToSplit - The SDK token to split
|
|
3592
3731
|
* @param splitAmount - Amount to send to recipient
|
|
@@ -3600,117 +3739,19 @@ var InstantSplitExecutor = class {
|
|
|
3600
3739
|
*/
|
|
3601
3740
|
async executeSplitInstant(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, transport, recipientPubkey, options) {
|
|
3602
3741
|
const startTime = performance.now();
|
|
3603
|
-
const splitGroupId = crypto.randomUUID();
|
|
3604
|
-
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
3605
|
-
console.log(`[InstantSplit] Starting V5 split for token ${tokenIdHex.slice(0, 8)}...`);
|
|
3606
3742
|
try {
|
|
3607
|
-
const
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
3613
|
-
const senderAddressRef = await UnmaskedPredicateReference2.create(
|
|
3614
|
-
tokenToSplit.type,
|
|
3615
|
-
this.signingService.algorithm,
|
|
3616
|
-
this.signingService.publicKey,
|
|
3617
|
-
HashAlgorithm3.SHA256
|
|
3618
|
-
);
|
|
3619
|
-
const senderAddress = await senderAddressRef.toAddress();
|
|
3620
|
-
const builder = new TokenSplitBuilder2();
|
|
3621
|
-
const coinDataA = TokenCoinData2.create([[coinId, splitAmount]]);
|
|
3622
|
-
builder.createToken(
|
|
3623
|
-
recipientTokenId,
|
|
3624
|
-
tokenToSplit.type,
|
|
3625
|
-
new Uint8Array(0),
|
|
3626
|
-
coinDataA,
|
|
3627
|
-
senderAddress,
|
|
3628
|
-
// Mint to sender first, then transfer
|
|
3629
|
-
recipientSalt,
|
|
3630
|
-
null
|
|
3631
|
-
);
|
|
3632
|
-
const coinDataB = TokenCoinData2.create([[coinId, remainderAmount]]);
|
|
3633
|
-
builder.createToken(
|
|
3634
|
-
senderTokenId,
|
|
3635
|
-
tokenToSplit.type,
|
|
3636
|
-
new Uint8Array(0),
|
|
3637
|
-
coinDataB,
|
|
3638
|
-
senderAddress,
|
|
3639
|
-
senderSalt,
|
|
3640
|
-
null
|
|
3641
|
-
);
|
|
3642
|
-
const split = await builder.build(tokenToSplit);
|
|
3643
|
-
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
3644
|
-
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
3645
|
-
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
3646
|
-
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3647
|
-
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3648
|
-
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3649
|
-
}
|
|
3650
|
-
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3651
|
-
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await waitInclusionProof3(this.trustBase, this.client, burnCommitment);
|
|
3652
|
-
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3653
|
-
const burnDuration = performance.now() - startTime;
|
|
3654
|
-
console.log(`[InstantSplit] Burn proof received in ${burnDuration.toFixed(0)}ms`);
|
|
3655
|
-
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3656
|
-
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3657
|
-
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3658
|
-
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3659
|
-
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3660
|
-
const recipientMintCommitment = mintCommitments.find(
|
|
3661
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3662
|
-
);
|
|
3663
|
-
const senderMintCommitment = mintCommitments.find(
|
|
3664
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3665
|
-
);
|
|
3666
|
-
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3667
|
-
throw new Error("Failed to find expected mint commitments");
|
|
3668
|
-
}
|
|
3669
|
-
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3670
|
-
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3671
|
-
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3672
|
-
recipientMintCommitment.transactionData,
|
|
3743
|
+
const buildResult = await this.buildSplitBundle(
|
|
3744
|
+
tokenToSplit,
|
|
3745
|
+
splitAmount,
|
|
3746
|
+
remainderAmount,
|
|
3747
|
+
coinIdHex,
|
|
3673
3748
|
recipientAddress,
|
|
3674
|
-
|
|
3675
|
-
this.signingService
|
|
3749
|
+
options
|
|
3676
3750
|
);
|
|
3677
|
-
|
|
3678
|
-
recipientTokenId,
|
|
3679
|
-
tokenToSplit.type,
|
|
3680
|
-
this.signingService,
|
|
3681
|
-
HashAlgorithm3.SHA256,
|
|
3682
|
-
recipientSalt
|
|
3683
|
-
);
|
|
3684
|
-
const mintedState = new TokenState3(mintedPredicate, null);
|
|
3685
|
-
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3751
|
+
console.log("[InstantSplit] Sending via transport...");
|
|
3686
3752
|
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3687
|
-
let nametagTokenJson;
|
|
3688
|
-
const recipientAddressStr = recipientAddress.toString();
|
|
3689
|
-
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3690
|
-
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3691
|
-
}
|
|
3692
|
-
const bundle = {
|
|
3693
|
-
version: "5.0",
|
|
3694
|
-
type: "INSTANT_SPLIT",
|
|
3695
|
-
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3696
|
-
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3697
|
-
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3698
|
-
amount: splitAmount.toString(),
|
|
3699
|
-
coinId: coinIdHex,
|
|
3700
|
-
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3701
|
-
splitGroupId,
|
|
3702
|
-
senderPubkey,
|
|
3703
|
-
recipientSaltHex: toHex2(recipientSalt),
|
|
3704
|
-
transferSaltHex: toHex2(transferSalt),
|
|
3705
|
-
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3706
|
-
finalRecipientStateJson: "",
|
|
3707
|
-
// Recipient creates their own
|
|
3708
|
-
recipientAddressJson: recipientAddressStr,
|
|
3709
|
-
nametagTokenJson
|
|
3710
|
-
};
|
|
3711
|
-
console.log("[InstantSplit] Step 6: Sending via transport...");
|
|
3712
3753
|
const nostrEventId = await transport.sendTokenTransfer(recipientPubkey, {
|
|
3713
|
-
token: JSON.stringify(bundle),
|
|
3754
|
+
token: JSON.stringify(buildResult.bundle),
|
|
3714
3755
|
proof: null,
|
|
3715
3756
|
// Proof is included in the bundle
|
|
3716
3757
|
memo: options?.memo,
|
|
@@ -3721,25 +3762,13 @@ var InstantSplitExecutor = class {
|
|
|
3721
3762
|
const criticalPathDuration = performance.now() - startTime;
|
|
3722
3763
|
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3723
3764
|
options?.onNostrDelivered?.(nostrEventId);
|
|
3724
|
-
|
|
3725
|
-
if (!options?.skipBackground) {
|
|
3726
|
-
backgroundPromise = this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3727
|
-
signingService: this.signingService,
|
|
3728
|
-
tokenType: tokenToSplit.type,
|
|
3729
|
-
coinId,
|
|
3730
|
-
senderTokenId,
|
|
3731
|
-
senderSalt,
|
|
3732
|
-
onProgress: options?.onBackgroundProgress,
|
|
3733
|
-
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3734
|
-
onStorageSync: options?.onStorageSync
|
|
3735
|
-
});
|
|
3736
|
-
}
|
|
3765
|
+
const backgroundPromise = buildResult.startBackground();
|
|
3737
3766
|
return {
|
|
3738
3767
|
success: true,
|
|
3739
3768
|
nostrEventId,
|
|
3740
|
-
splitGroupId,
|
|
3769
|
+
splitGroupId: buildResult.splitGroupId,
|
|
3741
3770
|
criticalPathDurationMs: criticalPathDuration,
|
|
3742
|
-
backgroundStarted:
|
|
3771
|
+
backgroundStarted: true,
|
|
3743
3772
|
backgroundPromise
|
|
3744
3773
|
};
|
|
3745
3774
|
} catch (error) {
|
|
@@ -3748,7 +3777,6 @@ var InstantSplitExecutor = class {
|
|
|
3748
3777
|
console.error(`[InstantSplit] Failed after ${duration.toFixed(0)}ms:`, error);
|
|
3749
3778
|
return {
|
|
3750
3779
|
success: false,
|
|
3751
|
-
splitGroupId,
|
|
3752
3780
|
criticalPathDurationMs: duration,
|
|
3753
3781
|
error: errorMessage,
|
|
3754
3782
|
backgroundStarted: false
|
|
@@ -3953,6 +3981,11 @@ function isInstantSplitBundleV4(obj) {
|
|
|
3953
3981
|
function isInstantSplitBundleV5(obj) {
|
|
3954
3982
|
return isInstantSplitBundle(obj) && obj.version === "5.0";
|
|
3955
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
|
+
}
|
|
3956
3989
|
|
|
3957
3990
|
// modules/payments/InstantSplitProcessor.ts
|
|
3958
3991
|
function fromHex3(hex) {
|
|
@@ -4595,6 +4628,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4595
4628
|
// Poll every 2s
|
|
4596
4629
|
static PROOF_POLLING_MAX_ATTEMPTS = 30;
|
|
4597
4630
|
// Max 30 attempts (~60s)
|
|
4631
|
+
// Periodic retry for resolveUnconfirmed (V5 lazy finalization)
|
|
4632
|
+
resolveUnconfirmedTimer = null;
|
|
4633
|
+
static RESOLVE_UNCONFIRMED_INTERVAL_MS = 1e4;
|
|
4634
|
+
// Retry every 10s
|
|
4635
|
+
// Guard: ensure load() completes before processing incoming bundles
|
|
4636
|
+
loadedPromise = null;
|
|
4637
|
+
loaded = false;
|
|
4638
|
+
// Persistent dedup: tracks splitGroupIds that have been fully processed.
|
|
4639
|
+
// Survives page reloads via KV storage so Nostr re-deliveries are ignored
|
|
4640
|
+
// even when the confirmed token's in-memory ID differs from v5split_{id}.
|
|
4641
|
+
processedSplitGroupIds = /* @__PURE__ */ new Set();
|
|
4642
|
+
// Persistent dedup: tracks V6 combined transfer IDs that have been processed.
|
|
4643
|
+
processedCombinedTransferIds = /* @__PURE__ */ new Set();
|
|
4598
4644
|
// Storage event subscriptions (push-based sync)
|
|
4599
4645
|
storageEventUnsubscribers = [];
|
|
4600
4646
|
syncDebounceTimer = null;
|
|
@@ -4680,31 +4726,53 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4680
4726
|
*/
|
|
4681
4727
|
async load() {
|
|
4682
4728
|
this.ensureInitialized();
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4729
|
+
const doLoad = async () => {
|
|
4730
|
+
await TokenRegistry.waitForReady();
|
|
4731
|
+
const providers = this.getTokenStorageProviders();
|
|
4732
|
+
for (const [id, provider] of providers) {
|
|
4733
|
+
try {
|
|
4734
|
+
const result = await provider.load();
|
|
4735
|
+
if (result.success && result.data) {
|
|
4736
|
+
this.loadFromStorageData(result.data);
|
|
4737
|
+
this.log(`Loaded metadata from provider ${id}`);
|
|
4738
|
+
break;
|
|
4739
|
+
}
|
|
4740
|
+
} catch (err) {
|
|
4741
|
+
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4692
4742
|
}
|
|
4693
|
-
} catch (err) {
|
|
4694
|
-
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4695
4743
|
}
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
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
|
+
}
|
|
4704
4755
|
}
|
|
4705
|
-
|
|
4756
|
+
const loadedTokens = Array.from(this.tokens.values()).map((t) => `${t.id.slice(0, 12)}(${t.status})`);
|
|
4757
|
+
console.log(`[Payments][DEBUG] load(): from TXF providers: ${this.tokens.size} tokens [${loadedTokens.join(", ")}]`);
|
|
4758
|
+
await this.loadPendingV5Tokens();
|
|
4759
|
+
await this.loadProcessedSplitGroupIds();
|
|
4760
|
+
await this.loadProcessedCombinedTransferIds();
|
|
4761
|
+
await this.loadHistory();
|
|
4762
|
+
const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
|
|
4763
|
+
if (pending2) {
|
|
4764
|
+
const transfers = JSON.parse(pending2);
|
|
4765
|
+
for (const transfer of transfers) {
|
|
4766
|
+
this.pendingTransfers.set(transfer.id, transfer);
|
|
4767
|
+
}
|
|
4768
|
+
}
|
|
4769
|
+
this.loaded = true;
|
|
4770
|
+
};
|
|
4771
|
+
this.loadedPromise = doLoad();
|
|
4772
|
+
await this.loadedPromise;
|
|
4706
4773
|
this.resolveUnconfirmed().catch(() => {
|
|
4707
4774
|
});
|
|
4775
|
+
this.scheduleResolveUnconfirmed();
|
|
4708
4776
|
}
|
|
4709
4777
|
/**
|
|
4710
4778
|
* Cleanup all subscriptions, polling jobs, and pending resolvers.
|
|
@@ -4723,6 +4791,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4723
4791
|
this.paymentRequestResponseHandlers.clear();
|
|
4724
4792
|
this.stopProofPolling();
|
|
4725
4793
|
this.proofPollingJobs.clear();
|
|
4794
|
+
this.stopResolveUnconfirmedPolling();
|
|
4726
4795
|
for (const [, resolver] of this.pendingResponseResolvers) {
|
|
4727
4796
|
clearTimeout(resolver.timeout);
|
|
4728
4797
|
resolver.reject(new Error("Module destroyed"));
|
|
@@ -4780,12 +4849,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4780
4849
|
token.status = "transferring";
|
|
4781
4850
|
this.tokens.set(token.id, token);
|
|
4782
4851
|
}
|
|
4852
|
+
await this.save();
|
|
4783
4853
|
await this.saveToOutbox(result, recipientPubkey);
|
|
4784
4854
|
result.status = "submitted";
|
|
4785
4855
|
const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
|
|
4786
4856
|
const transferMode = request.transferMode ?? "instant";
|
|
4787
|
-
if (
|
|
4788
|
-
if (
|
|
4857
|
+
if (transferMode === "conservative") {
|
|
4858
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4789
4859
|
this.log("Executing conservative split...");
|
|
4790
4860
|
const splitExecutor = new TokenSplitExecutor({
|
|
4791
4861
|
stateTransitionClient: stClient,
|
|
@@ -4829,27 +4899,59 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4829
4899
|
requestIdHex: splitRequestIdHex
|
|
4830
4900
|
});
|
|
4831
4901
|
this.log(`Conservative split transfer completed`);
|
|
4832
|
-
}
|
|
4833
|
-
|
|
4834
|
-
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...");
|
|
4835
4936
|
const executor = new InstantSplitExecutor({
|
|
4836
4937
|
stateTransitionClient: stClient,
|
|
4837
4938
|
trustBase,
|
|
4838
4939
|
signingService,
|
|
4839
4940
|
devMode
|
|
4840
4941
|
});
|
|
4841
|
-
|
|
4942
|
+
builtSplit = await executor.buildSplitBundle(
|
|
4842
4943
|
splitPlan.tokenToSplit.sdkToken,
|
|
4843
4944
|
splitPlan.splitAmount,
|
|
4844
4945
|
splitPlan.remainderAmount,
|
|
4845
4946
|
splitPlan.coinId,
|
|
4846
4947
|
recipientAddress,
|
|
4847
|
-
this.deps.transport,
|
|
4848
|
-
recipientPubkey,
|
|
4849
4948
|
{
|
|
4850
4949
|
memo: request.memo,
|
|
4851
4950
|
onChangeTokenCreated: async (changeToken) => {
|
|
4852
4951
|
const changeTokenData = changeToken.toJSON();
|
|
4952
|
+
if (changeTokenPlaceholderId && this.tokens.has(changeTokenPlaceholderId)) {
|
|
4953
|
+
this.tokens.delete(changeTokenPlaceholderId);
|
|
4954
|
+
}
|
|
4853
4955
|
const uiToken = {
|
|
4854
4956
|
id: crypto.randomUUID(),
|
|
4855
4957
|
coinId: request.coinId,
|
|
@@ -4872,65 +4974,103 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4872
4974
|
}
|
|
4873
4975
|
}
|
|
4874
4976
|
);
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
this.
|
|
4880
|
-
|
|
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) {
|
|
4881
5042
|
await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
|
|
4882
5043
|
result.tokenTransfers.push({
|
|
4883
5044
|
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4884
5045
|
method: "split",
|
|
4885
|
-
splitGroupId:
|
|
4886
|
-
nostrEventId: instantResult.nostrEventId
|
|
5046
|
+
splitGroupId: builtSplit.splitGroupId
|
|
4887
5047
|
});
|
|
4888
|
-
this.log(`Instant split transfer completed`);
|
|
4889
5048
|
}
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
}
|
|
4900
|
-
const inclusionProof = await waitInclusionProof5(trustBase, stClient, commitment);
|
|
4901
|
-
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4902
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4903
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4904
|
-
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
4905
|
-
memo: request.memo
|
|
4906
|
-
});
|
|
4907
|
-
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
4908
|
-
} else {
|
|
4909
|
-
console.log(`[Payments] NOSTR-FIRST: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4910
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4911
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4912
|
-
commitmentData: JSON.stringify(commitment.toJSON()),
|
|
4913
|
-
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
|
|
4914
5058
|
});
|
|
4915
|
-
|
|
4916
|
-
stClient.submitTransferCommitment(commitment).catch(
|
|
4917
|
-
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
4918
|
-
);
|
|
5059
|
+
await this.removeToken(token.id);
|
|
4919
5060
|
}
|
|
4920
|
-
|
|
4921
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4922
|
-
result.tokenTransfers.push({
|
|
4923
|
-
sourceTokenId: token.id,
|
|
4924
|
-
method: "direct",
|
|
4925
|
-
requestIdHex
|
|
4926
|
-
});
|
|
4927
|
-
this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
|
|
4928
|
-
await this.removeToken(token.id);
|
|
5061
|
+
this.log(`V6 combined transfer completed`);
|
|
4929
5062
|
}
|
|
4930
5063
|
result.status = "delivered";
|
|
4931
5064
|
await this.save();
|
|
4932
5065
|
await this.removeFromOutbox(result.id);
|
|
4933
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
|
+
}));
|
|
4934
5074
|
const sentTokenId = result.tokens[0] ? extractTokenIdFromSdkData(result.tokens[0].sdkData) : void 0;
|
|
4935
5075
|
await this.addToHistory({
|
|
4936
5076
|
type: "SENT",
|
|
@@ -4943,7 +5083,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4943
5083
|
recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
|
|
4944
5084
|
memo: request.memo,
|
|
4945
5085
|
transferId: result.id,
|
|
4946
|
-
tokenId: sentTokenId || void 0
|
|
5086
|
+
tokenId: sentTokenId || void 0,
|
|
5087
|
+
tokenIds: sentTokenIds.length > 0 ? sentTokenIds : void 0
|
|
4947
5088
|
});
|
|
4948
5089
|
this.deps.emitEvent("transfer:confirmed", result);
|
|
4949
5090
|
return result;
|
|
@@ -5113,6 +5254,267 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5113
5254
|
};
|
|
5114
5255
|
}
|
|
5115
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
|
+
}
|
|
5116
5518
|
/**
|
|
5117
5519
|
* Process a received INSTANT_SPLIT bundle.
|
|
5118
5520
|
*
|
|
@@ -5129,39 +5531,17 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5129
5531
|
*/
|
|
5130
5532
|
async processInstantSplitBundle(bundle, senderPubkey, memo) {
|
|
5131
5533
|
this.ensureInitialized();
|
|
5534
|
+
if (!this.loaded && this.loadedPromise) {
|
|
5535
|
+
await this.loadedPromise;
|
|
5536
|
+
}
|
|
5132
5537
|
if (!isInstantSplitBundleV5(bundle)) {
|
|
5133
5538
|
return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
|
|
5134
5539
|
}
|
|
5135
5540
|
try {
|
|
5136
|
-
const
|
|
5137
|
-
if (
|
|
5138
|
-
this.log(`V5 bundle ${deterministicId.slice(0, 16)}... already exists, skipping duplicate`);
|
|
5541
|
+
const uiToken = await this.saveUnconfirmedV5Token(bundle, senderPubkey);
|
|
5542
|
+
if (!uiToken) {
|
|
5139
5543
|
return { success: true, durationMs: 0 };
|
|
5140
5544
|
}
|
|
5141
|
-
const registry = TokenRegistry.getInstance();
|
|
5142
|
-
const pendingData = {
|
|
5143
|
-
type: "v5_bundle",
|
|
5144
|
-
stage: "RECEIVED",
|
|
5145
|
-
bundleJson: JSON.stringify(bundle),
|
|
5146
|
-
senderPubkey,
|
|
5147
|
-
savedAt: Date.now(),
|
|
5148
|
-
attemptCount: 0
|
|
5149
|
-
};
|
|
5150
|
-
const uiToken = {
|
|
5151
|
-
id: deterministicId,
|
|
5152
|
-
coinId: bundle.coinId,
|
|
5153
|
-
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
5154
|
-
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
5155
|
-
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
5156
|
-
amount: bundle.amount,
|
|
5157
|
-
status: "submitted",
|
|
5158
|
-
// UNCONFIRMED
|
|
5159
|
-
createdAt: Date.now(),
|
|
5160
|
-
updatedAt: Date.now(),
|
|
5161
|
-
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
5162
|
-
};
|
|
5163
|
-
await this.addToken(uiToken);
|
|
5164
|
-
this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
|
|
5165
5545
|
const senderInfo = await this.resolveSenderInfo(senderPubkey);
|
|
5166
5546
|
await this.addToHistory({
|
|
5167
5547
|
type: "RECEIVED",
|
|
@@ -5172,7 +5552,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5172
5552
|
senderPubkey,
|
|
5173
5553
|
...senderInfo,
|
|
5174
5554
|
memo,
|
|
5175
|
-
tokenId:
|
|
5555
|
+
tokenId: uiToken.id
|
|
5176
5556
|
});
|
|
5177
5557
|
this.deps.emitEvent("transfer:incoming", {
|
|
5178
5558
|
id: bundle.splitGroupId,
|
|
@@ -5185,6 +5565,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5185
5565
|
await this.save();
|
|
5186
5566
|
this.resolveUnconfirmed().catch(() => {
|
|
5187
5567
|
});
|
|
5568
|
+
this.scheduleResolveUnconfirmed();
|
|
5188
5569
|
return { success: true, durationMs: 0 };
|
|
5189
5570
|
} catch (error) {
|
|
5190
5571
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -5821,16 +6202,18 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5821
6202
|
}
|
|
5822
6203
|
/**
|
|
5823
6204
|
* Aggregate tokens by coinId with confirmed/unconfirmed breakdown.
|
|
5824
|
-
* 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").
|
|
5825
6207
|
*/
|
|
5826
6208
|
aggregateTokens(coinId) {
|
|
5827
6209
|
const assetsMap = /* @__PURE__ */ new Map();
|
|
5828
6210
|
for (const token of this.tokens.values()) {
|
|
5829
|
-
if (token.status === "spent" || token.status === "invalid"
|
|
6211
|
+
if (token.status === "spent" || token.status === "invalid") continue;
|
|
5830
6212
|
if (coinId && token.coinId !== coinId) continue;
|
|
5831
6213
|
const key = token.coinId;
|
|
5832
6214
|
const amount = BigInt(token.amount);
|
|
5833
6215
|
const isConfirmed = token.status === "confirmed";
|
|
6216
|
+
const isTransferring = token.status === "transferring";
|
|
5834
6217
|
const existing = assetsMap.get(key);
|
|
5835
6218
|
if (existing) {
|
|
5836
6219
|
if (isConfirmed) {
|
|
@@ -5840,6 +6223,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5840
6223
|
existing.unconfirmedAmount += amount;
|
|
5841
6224
|
existing.unconfirmedTokenCount++;
|
|
5842
6225
|
}
|
|
6226
|
+
if (isTransferring) existing.transferringTokenCount++;
|
|
5843
6227
|
} else {
|
|
5844
6228
|
assetsMap.set(key, {
|
|
5845
6229
|
coinId: token.coinId,
|
|
@@ -5850,7 +6234,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5850
6234
|
confirmedAmount: isConfirmed ? amount : 0n,
|
|
5851
6235
|
unconfirmedAmount: isConfirmed ? 0n : amount,
|
|
5852
6236
|
confirmedTokenCount: isConfirmed ? 1 : 0,
|
|
5853
|
-
unconfirmedTokenCount: isConfirmed ? 0 : 1
|
|
6237
|
+
unconfirmedTokenCount: isConfirmed ? 0 : 1,
|
|
6238
|
+
transferringTokenCount: isTransferring ? 1 : 0
|
|
5854
6239
|
});
|
|
5855
6240
|
}
|
|
5856
6241
|
}
|
|
@@ -5868,6 +6253,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5868
6253
|
unconfirmedAmount: raw.unconfirmedAmount.toString(),
|
|
5869
6254
|
confirmedTokenCount: raw.confirmedTokenCount,
|
|
5870
6255
|
unconfirmedTokenCount: raw.unconfirmedTokenCount,
|
|
6256
|
+
transferringTokenCount: raw.transferringTokenCount,
|
|
5871
6257
|
priceUsd: null,
|
|
5872
6258
|
priceEur: null,
|
|
5873
6259
|
change24h: null,
|
|
@@ -5931,28 +6317,70 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5931
6317
|
};
|
|
5932
6318
|
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5933
6319
|
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
5934
|
-
if (!stClient || !trustBase)
|
|
6320
|
+
if (!stClient || !trustBase) {
|
|
6321
|
+
console.log(`[V5-RESOLVE] resolveUnconfirmed: EARLY EXIT \u2014 stClient=${!!stClient} trustBase=${!!trustBase}`);
|
|
6322
|
+
return result;
|
|
6323
|
+
}
|
|
5935
6324
|
const signingService = await this.createSigningService();
|
|
6325
|
+
const submittedCount = Array.from(this.tokens.values()).filter((t) => t.status === "submitted").length;
|
|
6326
|
+
console.log(`[V5-RESOLVE] resolveUnconfirmed: ${submittedCount} submitted token(s) to process`);
|
|
5936
6327
|
for (const [tokenId, token] of this.tokens) {
|
|
5937
6328
|
if (token.status !== "submitted") continue;
|
|
5938
6329
|
const pending2 = this.parsePendingFinalization(token.sdkData);
|
|
5939
6330
|
if (!pending2) {
|
|
6331
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 16)}: no pending finalization metadata, skipping`);
|
|
5940
6332
|
result.stillPending++;
|
|
5941
6333
|
continue;
|
|
5942
6334
|
}
|
|
5943
6335
|
if (pending2.type === "v5_bundle") {
|
|
6336
|
+
console.log(`[V5-RESOLVE] Processing ${tokenId.slice(0, 16)}... stage=${pending2.stage} attempt=${pending2.attemptCount}`);
|
|
5944
6337
|
const progress = await this.resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService);
|
|
6338
|
+
console.log(`[V5-RESOLVE] Result for ${tokenId.slice(0, 16)}...: ${progress} (stage now: ${pending2.stage})`);
|
|
5945
6339
|
result.details.push({ tokenId, stage: pending2.stage, status: progress });
|
|
5946
6340
|
if (progress === "resolved") result.resolved++;
|
|
5947
6341
|
else if (progress === "failed") result.failed++;
|
|
5948
6342
|
else result.stillPending++;
|
|
5949
6343
|
}
|
|
5950
6344
|
}
|
|
5951
|
-
if (result.resolved > 0 || result.failed > 0) {
|
|
6345
|
+
if (result.resolved > 0 || result.failed > 0 || result.stillPending > 0) {
|
|
6346
|
+
console.log(`[V5-RESOLVE] Saving: resolved=${result.resolved} failed=${result.failed} stillPending=${result.stillPending}`);
|
|
5952
6347
|
await this.save();
|
|
5953
6348
|
}
|
|
5954
6349
|
return result;
|
|
5955
6350
|
}
|
|
6351
|
+
/**
|
|
6352
|
+
* Start a periodic interval that retries resolveUnconfirmed() until all
|
|
6353
|
+
* tokens are confirmed or failed. Stops automatically when nothing is
|
|
6354
|
+
* pending and is cleaned up by destroy().
|
|
6355
|
+
*/
|
|
6356
|
+
scheduleResolveUnconfirmed() {
|
|
6357
|
+
if (this.resolveUnconfirmedTimer) return;
|
|
6358
|
+
const hasUnconfirmed = Array.from(this.tokens.values()).some(
|
|
6359
|
+
(t) => t.status === "submitted"
|
|
6360
|
+
);
|
|
6361
|
+
if (!hasUnconfirmed) {
|
|
6362
|
+
console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: no submitted tokens, not starting timer`);
|
|
6363
|
+
return;
|
|
6364
|
+
}
|
|
6365
|
+
console.log(`[V5-RESOLVE] scheduleResolveUnconfirmed: starting periodic retry (every ${_PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS}ms)`);
|
|
6366
|
+
this.resolveUnconfirmedTimer = setInterval(async () => {
|
|
6367
|
+
try {
|
|
6368
|
+
const result = await this.resolveUnconfirmed();
|
|
6369
|
+
if (result.stillPending === 0) {
|
|
6370
|
+
console.log(`[V5-RESOLVE] All tokens resolved, stopping periodic retry`);
|
|
6371
|
+
this.stopResolveUnconfirmedPolling();
|
|
6372
|
+
}
|
|
6373
|
+
} catch (err) {
|
|
6374
|
+
console.log(`[V5-RESOLVE] Periodic retry error:`, err);
|
|
6375
|
+
}
|
|
6376
|
+
}, _PaymentsModule.RESOLVE_UNCONFIRMED_INTERVAL_MS);
|
|
6377
|
+
}
|
|
6378
|
+
stopResolveUnconfirmedPolling() {
|
|
6379
|
+
if (this.resolveUnconfirmedTimer) {
|
|
6380
|
+
clearInterval(this.resolveUnconfirmedTimer);
|
|
6381
|
+
this.resolveUnconfirmedTimer = null;
|
|
6382
|
+
}
|
|
6383
|
+
}
|
|
5956
6384
|
// ===========================================================================
|
|
5957
6385
|
// Private - V5 Lazy Resolution Helpers
|
|
5958
6386
|
// ===========================================================================
|
|
@@ -5965,10 +6393,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5965
6393
|
pending2.lastAttemptAt = Date.now();
|
|
5966
6394
|
try {
|
|
5967
6395
|
if (pending2.stage === "RECEIVED") {
|
|
6396
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: RECEIVED \u2192 submitting mint commitment...`);
|
|
5968
6397
|
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5969
6398
|
const mintData = await MintTransactionData3.fromJSON(mintDataJson);
|
|
5970
6399
|
const mintCommitment = await MintCommitment3.create(mintData);
|
|
5971
6400
|
const mintResponse = await stClient.submitMintCommitment(mintCommitment);
|
|
6401
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint response status=${mintResponse.status}`);
|
|
5972
6402
|
if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5973
6403
|
throw new Error(`Mint submission failed: ${mintResponse.status}`);
|
|
5974
6404
|
}
|
|
@@ -5976,22 +6406,27 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5976
6406
|
this.updatePendingFinalization(token, pending2);
|
|
5977
6407
|
}
|
|
5978
6408
|
if (pending2.stage === "MINT_SUBMITTED") {
|
|
6409
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_SUBMITTED \u2192 checking mint proof...`);
|
|
5979
6410
|
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5980
6411
|
const mintData = await MintTransactionData3.fromJSON(mintDataJson);
|
|
5981
6412
|
const mintCommitment = await MintCommitment3.create(mintData);
|
|
5982
6413
|
const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
|
|
5983
6414
|
if (!proof) {
|
|
6415
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof not yet available, staying MINT_SUBMITTED`);
|
|
5984
6416
|
this.updatePendingFinalization(token, pending2);
|
|
5985
6417
|
return "pending";
|
|
5986
6418
|
}
|
|
6419
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: mint proof obtained!`);
|
|
5987
6420
|
pending2.mintProofJson = JSON.stringify(proof);
|
|
5988
6421
|
pending2.stage = "MINT_PROVEN";
|
|
5989
6422
|
this.updatePendingFinalization(token, pending2);
|
|
5990
6423
|
}
|
|
5991
6424
|
if (pending2.stage === "MINT_PROVEN") {
|
|
6425
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: MINT_PROVEN \u2192 submitting transfer commitment...`);
|
|
5992
6426
|
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5993
6427
|
const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
|
|
5994
6428
|
const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
|
|
6429
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer response status=${transferResponse.status}`);
|
|
5995
6430
|
if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5996
6431
|
throw new Error(`Transfer submission failed: ${transferResponse.status}`);
|
|
5997
6432
|
}
|
|
@@ -5999,13 +6434,16 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5999
6434
|
this.updatePendingFinalization(token, pending2);
|
|
6000
6435
|
}
|
|
6001
6436
|
if (pending2.stage === "TRANSFER_SUBMITTED") {
|
|
6437
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: TRANSFER_SUBMITTED \u2192 checking transfer proof...`);
|
|
6002
6438
|
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
6003
6439
|
const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
|
|
6004
6440
|
const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
|
|
6005
6441
|
if (!proof) {
|
|
6442
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof not yet available, staying TRANSFER_SUBMITTED`);
|
|
6006
6443
|
this.updatePendingFinalization(token, pending2);
|
|
6007
6444
|
return "pending";
|
|
6008
6445
|
}
|
|
6446
|
+
console.log(`[V5-RESOLVE] ${tokenId.slice(0, 12)}: transfer proof obtained! Finalizing...`);
|
|
6009
6447
|
const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
|
|
6010
6448
|
const confirmedToken = {
|
|
6011
6449
|
id: token.id,
|
|
@@ -6021,6 +6459,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6021
6459
|
sdkData: JSON.stringify(finalizedToken.toJSON())
|
|
6022
6460
|
};
|
|
6023
6461
|
this.tokens.set(tokenId, confirmedToken);
|
|
6462
|
+
this.deps.emitEvent("transfer:confirmed", {
|
|
6463
|
+
id: crypto.randomUUID(),
|
|
6464
|
+
status: "completed",
|
|
6465
|
+
tokens: [confirmedToken],
|
|
6466
|
+
tokenTransfers: []
|
|
6467
|
+
});
|
|
6024
6468
|
this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
|
|
6025
6469
|
return "resolved";
|
|
6026
6470
|
}
|
|
@@ -6162,11 +6606,20 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6162
6606
|
}
|
|
6163
6607
|
}
|
|
6164
6608
|
if (pendingTokens.length > 0) {
|
|
6609
|
+
const json = JSON.stringify(pendingTokens);
|
|
6610
|
+
this.log(`[V5-PERSIST] Saving ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")} (${json.length} bytes)`);
|
|
6165
6611
|
await this.deps.storage.set(
|
|
6166
6612
|
STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
|
|
6167
|
-
|
|
6613
|
+
json
|
|
6168
6614
|
);
|
|
6615
|
+
const verify = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
|
|
6616
|
+
if (!verify) {
|
|
6617
|
+
console.error("[Payments][V5-PERSIST] CRITICAL: KV write succeeded but read-back is empty!");
|
|
6618
|
+
} else {
|
|
6619
|
+
this.log(`[V5-PERSIST] Verified: read-back ${verify.length} bytes`);
|
|
6620
|
+
}
|
|
6169
6621
|
} else {
|
|
6622
|
+
this.log(`[V5-PERSIST] No pending V5 tokens to save (total tokens: ${this.tokens.size}), clearing KV`);
|
|
6170
6623
|
await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
|
|
6171
6624
|
}
|
|
6172
6625
|
}
|
|
@@ -6176,16 +6629,47 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6176
6629
|
*/
|
|
6177
6630
|
async loadPendingV5Tokens() {
|
|
6178
6631
|
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
|
|
6632
|
+
this.log(`[V5-PERSIST] loadPendingV5Tokens: KV data = ${data ? `${data.length} bytes` : "null/empty"}`);
|
|
6179
6633
|
if (!data) return;
|
|
6180
6634
|
try {
|
|
6181
6635
|
const pendingTokens = JSON.parse(data);
|
|
6636
|
+
this.log(`[V5-PERSIST] Parsed ${pendingTokens.length} pending V5 token(s): ${pendingTokens.map((t) => t.id.slice(0, 16)).join(", ")}`);
|
|
6182
6637
|
for (const token of pendingTokens) {
|
|
6183
6638
|
if (!this.tokens.has(token.id)) {
|
|
6184
6639
|
this.tokens.set(token.id, token);
|
|
6640
|
+
this.log(`[V5-PERSIST] Restored token ${token.id.slice(0, 16)} (status=${token.status})`);
|
|
6641
|
+
} else {
|
|
6642
|
+
this.log(`[V5-PERSIST] Token ${token.id.slice(0, 16)} already in map, skipping`);
|
|
6185
6643
|
}
|
|
6186
6644
|
}
|
|
6187
|
-
|
|
6188
|
-
|
|
6645
|
+
} catch (err) {
|
|
6646
|
+
console.error("[Payments][V5-PERSIST] Failed to parse pending V5 tokens:", err);
|
|
6647
|
+
}
|
|
6648
|
+
}
|
|
6649
|
+
/**
|
|
6650
|
+
* Persist the set of processed splitGroupIds to KV storage.
|
|
6651
|
+
* This ensures Nostr re-deliveries are ignored across page reloads,
|
|
6652
|
+
* even when the confirmed token's in-memory ID differs from v5split_{id}.
|
|
6653
|
+
*/
|
|
6654
|
+
async saveProcessedSplitGroupIds() {
|
|
6655
|
+
const ids = Array.from(this.processedSplitGroupIds);
|
|
6656
|
+
if (ids.length > 0) {
|
|
6657
|
+
await this.deps.storage.set(
|
|
6658
|
+
STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS,
|
|
6659
|
+
JSON.stringify(ids)
|
|
6660
|
+
);
|
|
6661
|
+
}
|
|
6662
|
+
}
|
|
6663
|
+
/**
|
|
6664
|
+
* Load processed splitGroupIds from KV storage.
|
|
6665
|
+
*/
|
|
6666
|
+
async loadProcessedSplitGroupIds() {
|
|
6667
|
+
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PROCESSED_SPLIT_GROUP_IDS);
|
|
6668
|
+
if (!data) return;
|
|
6669
|
+
try {
|
|
6670
|
+
const ids = JSON.parse(data);
|
|
6671
|
+
for (const id of ids) {
|
|
6672
|
+
this.processedSplitGroupIds.add(id);
|
|
6189
6673
|
}
|
|
6190
6674
|
} catch {
|
|
6191
6675
|
}
|
|
@@ -6840,7 +7324,32 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6840
7324
|
try {
|
|
6841
7325
|
const result = await provider.sync(localData);
|
|
6842
7326
|
if (result.success && result.merged) {
|
|
7327
|
+
const savedTokens = new Map(this.tokens);
|
|
6843
7328
|
this.loadFromStorageData(result.merged);
|
|
7329
|
+
let restoredCount = 0;
|
|
7330
|
+
for (const [tokenId, token] of savedTokens) {
|
|
7331
|
+
if (this.tokens.has(tokenId)) continue;
|
|
7332
|
+
const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7333
|
+
const stateHash = extractStateHashFromSdkData(token.sdkData);
|
|
7334
|
+
if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
|
|
7335
|
+
continue;
|
|
7336
|
+
}
|
|
7337
|
+
if (sdkTokenId) {
|
|
7338
|
+
let hasEquivalent = false;
|
|
7339
|
+
for (const existing of this.tokens.values()) {
|
|
7340
|
+
if (extractTokenIdFromSdkData(existing.sdkData) === sdkTokenId) {
|
|
7341
|
+
hasEquivalent = true;
|
|
7342
|
+
break;
|
|
7343
|
+
}
|
|
7344
|
+
}
|
|
7345
|
+
if (hasEquivalent) continue;
|
|
7346
|
+
}
|
|
7347
|
+
this.tokens.set(tokenId, token);
|
|
7348
|
+
restoredCount++;
|
|
7349
|
+
}
|
|
7350
|
+
if (restoredCount > 0) {
|
|
7351
|
+
console.log(`[Payments] Sync: restored ${restoredCount} token(s) lost by loadFromStorageData`);
|
|
7352
|
+
}
|
|
6844
7353
|
if (this.nametags.length === 0 && savedNametags.length > 0) {
|
|
6845
7354
|
this.nametags = savedNametags;
|
|
6846
7355
|
}
|
|
@@ -7142,7 +7651,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7142
7651
|
/**
|
|
7143
7652
|
* Handle NOSTR-FIRST commitment-only transfer (recipient side)
|
|
7144
7653
|
* This is called when receiving a transfer with only commitmentData and no proof yet.
|
|
7145
|
-
*
|
|
7654
|
+
* Delegates to saveCommitmentOnlyToken() helper, then emits event + records history.
|
|
7146
7655
|
*/
|
|
7147
7656
|
async handleCommitmentOnlyTransfer(transfer, payload) {
|
|
7148
7657
|
try {
|
|
@@ -7152,40 +7661,22 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7152
7661
|
console.warn("[Payments] Invalid NOSTR-FIRST transfer format");
|
|
7153
7662
|
return;
|
|
7154
7663
|
}
|
|
7155
|
-
const
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
decimals: tokenInfo.decimals,
|
|
7162
|
-
iconUrl: tokenInfo.iconUrl,
|
|
7163
|
-
amount: tokenInfo.amount,
|
|
7164
|
-
status: "submitted",
|
|
7165
|
-
// NOSTR-FIRST: unconfirmed until proof
|
|
7166
|
-
createdAt: Date.now(),
|
|
7167
|
-
updatedAt: Date.now(),
|
|
7168
|
-
sdkData: typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput)
|
|
7169
|
-
};
|
|
7170
|
-
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7171
|
-
const nostrStateHash = extractStateHashFromSdkData(token.sdkData);
|
|
7172
|
-
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
7173
|
-
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
7174
|
-
return;
|
|
7175
|
-
}
|
|
7176
|
-
this.tokens.set(token.id, token);
|
|
7177
|
-
await this.save();
|
|
7178
|
-
this.log(`NOSTR-FIRST: Token ${token.id.slice(0, 8)}... added as submitted (unconfirmed)`);
|
|
7664
|
+
const token = await this.saveCommitmentOnlyToken(
|
|
7665
|
+
sourceTokenInput,
|
|
7666
|
+
commitmentInput,
|
|
7667
|
+
transfer.senderTransportPubkey
|
|
7668
|
+
);
|
|
7669
|
+
if (!token) return;
|
|
7179
7670
|
const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
|
|
7180
|
-
|
|
7671
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
7181
7672
|
id: transfer.id,
|
|
7182
7673
|
senderPubkey: transfer.senderTransportPubkey,
|
|
7183
7674
|
senderNametag: senderInfo.senderNametag,
|
|
7184
7675
|
tokens: [token],
|
|
7185
7676
|
memo: payload.memo,
|
|
7186
7677
|
receivedAt: transfer.timestamp
|
|
7187
|
-
};
|
|
7188
|
-
|
|
7678
|
+
});
|
|
7679
|
+
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7189
7680
|
await this.addToHistory({
|
|
7190
7681
|
type: "RECEIVED",
|
|
7191
7682
|
amount: token.amount,
|
|
@@ -7197,29 +7688,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7197
7688
|
memo: payload.memo,
|
|
7198
7689
|
tokenId: nostrTokenId || token.id
|
|
7199
7690
|
});
|
|
7200
|
-
try {
|
|
7201
|
-
const commitment = await TransferCommitment4.fromJSON(commitmentInput);
|
|
7202
|
-
const requestIdBytes = commitment.requestId;
|
|
7203
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
7204
|
-
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
7205
|
-
if (stClient) {
|
|
7206
|
-
const response = await stClient.submitTransferCommitment(commitment);
|
|
7207
|
-
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
7208
|
-
}
|
|
7209
|
-
this.addProofPollingJob({
|
|
7210
|
-
tokenId: token.id,
|
|
7211
|
-
requestIdHex,
|
|
7212
|
-
commitmentJson: JSON.stringify(commitmentInput),
|
|
7213
|
-
startedAt: Date.now(),
|
|
7214
|
-
attemptCount: 0,
|
|
7215
|
-
lastAttemptAt: 0,
|
|
7216
|
-
onProofReceived: async (tokenId) => {
|
|
7217
|
-
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
|
|
7218
|
-
}
|
|
7219
|
-
});
|
|
7220
|
-
} catch (err) {
|
|
7221
|
-
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
7222
|
-
}
|
|
7223
7691
|
} catch (error) {
|
|
7224
7692
|
console.error("[Payments] Failed to process NOSTR-FIRST transfer:", error);
|
|
7225
7693
|
}
|
|
@@ -7332,8 +7800,34 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7332
7800
|
}
|
|
7333
7801
|
}
|
|
7334
7802
|
async handleIncomingTransfer(transfer) {
|
|
7803
|
+
if (!this.loaded && this.loadedPromise) {
|
|
7804
|
+
await this.loadedPromise;
|
|
7805
|
+
}
|
|
7335
7806
|
try {
|
|
7336
7807
|
const payload = transfer.payload;
|
|
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
|
+
}
|
|
7337
7831
|
let instantBundle = null;
|
|
7338
7832
|
if (isInstantSplitBundle(payload)) {
|
|
7339
7833
|
instantBundle = payload;
|
|
@@ -7365,7 +7859,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7365
7859
|
return;
|
|
7366
7860
|
}
|
|
7367
7861
|
if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
|
|
7368
|
-
|
|
7862
|
+
console.log("[Payments][DEBUG] >>> NOSTR-FIRST commitment-only transfer detected");
|
|
7369
7863
|
await this.handleCommitmentOnlyTransfer(transfer, payload);
|
|
7370
7864
|
return;
|
|
7371
7865
|
}
|
|
@@ -7485,17 +7979,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7485
7979
|
memo: payload.memo,
|
|
7486
7980
|
tokenId: incomingTokenId || token.id
|
|
7487
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}`);
|
|
7488
7994
|
}
|
|
7489
|
-
const incomingTransfer = {
|
|
7490
|
-
id: transfer.id,
|
|
7491
|
-
senderPubkey: transfer.senderTransportPubkey,
|
|
7492
|
-
senderNametag: senderInfo.senderNametag,
|
|
7493
|
-
tokens: [token],
|
|
7494
|
-
memo: payload.memo,
|
|
7495
|
-
receivedAt: transfer.timestamp
|
|
7496
|
-
};
|
|
7497
|
-
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
7498
|
-
this.log(`Incoming transfer processed: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7499
7995
|
} catch (error) {
|
|
7500
7996
|
console.error("[Payments] Failed to process incoming transfer:", error);
|
|
7501
7997
|
}
|
|
@@ -7528,17 +8024,24 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7528
8024
|
// ===========================================================================
|
|
7529
8025
|
async save() {
|
|
7530
8026
|
const providers = this.getTokenStorageProviders();
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
return
|
|
7534
|
-
}
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
8027
|
+
const tokenStats = Array.from(this.tokens.values()).map((t) => {
|
|
8028
|
+
const txf = tokenToTxf(t);
|
|
8029
|
+
return `${t.id.slice(0, 12)}(${t.status},txf=${!!txf})`;
|
|
8030
|
+
});
|
|
8031
|
+
console.log(`[Payments][DEBUG] save(): providers=${providers.size}, tokens=[${tokenStats.join(", ")}]`);
|
|
8032
|
+
if (providers.size > 0) {
|
|
8033
|
+
const data = await this.createStorageData();
|
|
8034
|
+
const dataKeys = Object.keys(data).filter((k) => k.startsWith("token-"));
|
|
8035
|
+
console.log(`[Payments][DEBUG] save(): TXF keys=${dataKeys.length} (${dataKeys.join(", ")})`);
|
|
8036
|
+
for (const [id, provider] of providers) {
|
|
8037
|
+
try {
|
|
8038
|
+
await provider.save(data);
|
|
8039
|
+
} catch (err) {
|
|
8040
|
+
console.error(`[Payments] Failed to save to provider ${id}:`, err);
|
|
8041
|
+
}
|
|
7541
8042
|
}
|
|
8043
|
+
} else {
|
|
8044
|
+
console.log("[Payments][DEBUG] save(): No token storage providers - TXF not persisted");
|
|
7542
8045
|
}
|
|
7543
8046
|
await this.savePendingV5Tokens();
|
|
7544
8047
|
}
|
|
@@ -7574,6 +8077,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7574
8077
|
}
|
|
7575
8078
|
loadFromStorageData(data) {
|
|
7576
8079
|
const parsed = parseTxfStorageData(data);
|
|
8080
|
+
console.log(`[Payments][DEBUG] loadFromStorageData: parsed ${parsed.tokens.length} tokens, ${parsed.tombstones.length} tombstones, errors=[${parsed.validationErrors.join("; ")}]`);
|
|
7577
8081
|
this.tombstones = parsed.tombstones;
|
|
7578
8082
|
this.tokens.clear();
|
|
7579
8083
|
for (const token of parsed.tokens) {
|
|
@@ -16608,6 +17112,7 @@ export {
|
|
|
16608
17112
|
identityFromMnemonicSync,
|
|
16609
17113
|
initSphere,
|
|
16610
17114
|
isArchivedKey,
|
|
17115
|
+
isCombinedTransferBundleV6,
|
|
16611
17116
|
isForkedKey,
|
|
16612
17117
|
isInstantSplitBundle,
|
|
16613
17118
|
isInstantSplitBundleV4,
|