@unicitylabs/sphere-sdk 0.5.1 → 0.5.3
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 +669 -277
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +57 -2
- package/dist/core/index.d.ts +57 -2
- package/dist/core/index.js +669 -277
- 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 +11 -3
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +11 -3
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/browser/ipfs.cjs +9 -2
- package/dist/impl/browser/ipfs.cjs.map +1 -1
- package/dist/impl/browser/ipfs.js +9 -2
- 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 +11 -3
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +7 -0
- package/dist/impl/nodejs/index.d.ts +7 -0
- package/dist/impl/nodejs/index.js +11 -3
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +671 -277
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +128 -3
- package/dist/index.d.ts +128 -3
- package/dist/index.js +670 -277
- 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,
|
|
@@ -2711,7 +2713,7 @@ init_constants();
|
|
|
2711
2713
|
// types/txf.ts
|
|
2712
2714
|
var ARCHIVED_PREFIX = "archived-";
|
|
2713
2715
|
var FORKED_PREFIX = "_forked_";
|
|
2714
|
-
var RESERVED_KEYS = ["_meta", "_nametag", "_nametags", "_tombstones", "_invalidatedNametags", "_outbox", "_mintOutbox", "_sent", "_invalid", "_integrity"];
|
|
2716
|
+
var RESERVED_KEYS = ["_meta", "_nametag", "_nametags", "_tombstones", "_invalidatedNametags", "_outbox", "_mintOutbox", "_sent", "_invalid", "_integrity", "_history"];
|
|
2715
2717
|
function isTokenKey(key) {
|
|
2716
2718
|
return key.startsWith("_") && !key.startsWith(ARCHIVED_PREFIX) && !key.startsWith(FORKED_PREFIX) && !RESERVED_KEYS.includes(key);
|
|
2717
2719
|
}
|
|
@@ -3333,6 +3335,9 @@ async function buildTxfStorageData(tokens, meta, options) {
|
|
|
3333
3335
|
if (options?.invalidatedNametags && options.invalidatedNametags.length > 0) {
|
|
3334
3336
|
storageData._invalidatedNametags = options.invalidatedNametags;
|
|
3335
3337
|
}
|
|
3338
|
+
if (options?.historyEntries && options.historyEntries.length > 0) {
|
|
3339
|
+
storageData._history = options.historyEntries;
|
|
3340
|
+
}
|
|
3336
3341
|
for (const token of tokens) {
|
|
3337
3342
|
const txf = tokenToTxf(token);
|
|
3338
3343
|
if (txf) {
|
|
@@ -3366,6 +3371,7 @@ function parseTxfStorageData(data) {
|
|
|
3366
3371
|
outboxEntries: [],
|
|
3367
3372
|
mintOutboxEntries: [],
|
|
3368
3373
|
invalidatedNametags: [],
|
|
3374
|
+
historyEntries: [],
|
|
3369
3375
|
validationErrors: []
|
|
3370
3376
|
};
|
|
3371
3377
|
if (!data || typeof data !== "object") {
|
|
@@ -3419,6 +3425,13 @@ function parseTxfStorageData(data) {
|
|
|
3419
3425
|
}
|
|
3420
3426
|
}
|
|
3421
3427
|
}
|
|
3428
|
+
if (Array.isArray(storageData._history)) {
|
|
3429
|
+
for (const entry of storageData._history) {
|
|
3430
|
+
if (typeof entry === "object" && entry !== null && typeof entry.dedupKey === "string" && typeof entry.type === "string") {
|
|
3431
|
+
result.historyEntries.push(entry);
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3422
3435
|
for (const key of Object.keys(storageData)) {
|
|
3423
3436
|
if (isTokenKey(key)) {
|
|
3424
3437
|
const tokenId = tokenIdFromKey(key);
|
|
@@ -3581,14 +3594,149 @@ var InstantSplitExecutor = class {
|
|
|
3581
3594
|
this.devMode = config.devMode ?? false;
|
|
3582
3595
|
}
|
|
3583
3596
|
/**
|
|
3584
|
-
*
|
|
3597
|
+
* Build a V5 split bundle WITHOUT sending it via transport.
|
|
3585
3598
|
*
|
|
3586
|
-
*
|
|
3599
|
+
* Steps 1-5 of the V5 flow:
|
|
3587
3600
|
* 1. Create and submit burn commitment
|
|
3588
3601
|
* 2. Wait for burn proof
|
|
3589
3602
|
* 3. Create mint commitments with SplitMintReason
|
|
3590
3603
|
* 4. Create transfer commitment (no mint proof needed)
|
|
3591
|
-
* 5.
|
|
3604
|
+
* 5. Package V5 bundle
|
|
3605
|
+
*
|
|
3606
|
+
* The caller is responsible for sending the bundle and then calling
|
|
3607
|
+
* `startBackground()` on the result to begin mint proof + change token creation.
|
|
3608
|
+
*/
|
|
3609
|
+
async buildSplitBundle(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, options) {
|
|
3610
|
+
const splitGroupId = crypto.randomUUID();
|
|
3611
|
+
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
3612
|
+
console.log(`[InstantSplit] Building V5 bundle for token ${tokenIdHex.slice(0, 8)}...`);
|
|
3613
|
+
const coinId = new CoinId3(fromHex2(coinIdHex));
|
|
3614
|
+
const seedString = `${tokenIdHex}_${splitAmount.toString()}_${remainderAmount.toString()}_${Date.now()}`;
|
|
3615
|
+
const recipientTokenId = new TokenId3(await sha2563(seedString));
|
|
3616
|
+
const senderTokenId = new TokenId3(await sha2563(seedString + "_sender"));
|
|
3617
|
+
const recipientSalt = await sha2563(seedString + "_recipient_salt");
|
|
3618
|
+
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
3619
|
+
const senderAddressRef = await UnmaskedPredicateReference2.create(
|
|
3620
|
+
tokenToSplit.type,
|
|
3621
|
+
this.signingService.algorithm,
|
|
3622
|
+
this.signingService.publicKey,
|
|
3623
|
+
HashAlgorithm3.SHA256
|
|
3624
|
+
);
|
|
3625
|
+
const senderAddress = await senderAddressRef.toAddress();
|
|
3626
|
+
const builder = new TokenSplitBuilder2();
|
|
3627
|
+
const coinDataA = TokenCoinData2.create([[coinId, splitAmount]]);
|
|
3628
|
+
builder.createToken(
|
|
3629
|
+
recipientTokenId,
|
|
3630
|
+
tokenToSplit.type,
|
|
3631
|
+
new Uint8Array(0),
|
|
3632
|
+
coinDataA,
|
|
3633
|
+
senderAddress,
|
|
3634
|
+
// Mint to sender first, then transfer
|
|
3635
|
+
recipientSalt,
|
|
3636
|
+
null
|
|
3637
|
+
);
|
|
3638
|
+
const coinDataB = TokenCoinData2.create([[coinId, remainderAmount]]);
|
|
3639
|
+
builder.createToken(
|
|
3640
|
+
senderTokenId,
|
|
3641
|
+
tokenToSplit.type,
|
|
3642
|
+
new Uint8Array(0),
|
|
3643
|
+
coinDataB,
|
|
3644
|
+
senderAddress,
|
|
3645
|
+
senderSalt,
|
|
3646
|
+
null
|
|
3647
|
+
);
|
|
3648
|
+
const split = await builder.build(tokenToSplit);
|
|
3649
|
+
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
3650
|
+
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
3651
|
+
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
3652
|
+
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3653
|
+
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3654
|
+
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3655
|
+
}
|
|
3656
|
+
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3657
|
+
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await waitInclusionProof3(this.trustBase, this.client, burnCommitment);
|
|
3658
|
+
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3659
|
+
console.log(`[InstantSplit] Burn proof received`);
|
|
3660
|
+
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3661
|
+
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3662
|
+
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3663
|
+
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3664
|
+
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3665
|
+
const recipientMintCommitment = mintCommitments.find(
|
|
3666
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3667
|
+
);
|
|
3668
|
+
const senderMintCommitment = mintCommitments.find(
|
|
3669
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3670
|
+
);
|
|
3671
|
+
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3672
|
+
throw new Error("Failed to find expected mint commitments");
|
|
3673
|
+
}
|
|
3674
|
+
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3675
|
+
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3676
|
+
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3677
|
+
recipientMintCommitment.transactionData,
|
|
3678
|
+
recipientAddress,
|
|
3679
|
+
transferSalt,
|
|
3680
|
+
this.signingService
|
|
3681
|
+
);
|
|
3682
|
+
const mintedPredicate = await UnmaskedPredicate3.create(
|
|
3683
|
+
recipientTokenId,
|
|
3684
|
+
tokenToSplit.type,
|
|
3685
|
+
this.signingService,
|
|
3686
|
+
HashAlgorithm3.SHA256,
|
|
3687
|
+
recipientSalt
|
|
3688
|
+
);
|
|
3689
|
+
const mintedState = new TokenState3(mintedPredicate, null);
|
|
3690
|
+
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3691
|
+
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3692
|
+
let nametagTokenJson;
|
|
3693
|
+
const recipientAddressStr = recipientAddress.toString();
|
|
3694
|
+
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3695
|
+
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3696
|
+
}
|
|
3697
|
+
const bundle = {
|
|
3698
|
+
version: "5.0",
|
|
3699
|
+
type: "INSTANT_SPLIT",
|
|
3700
|
+
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3701
|
+
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3702
|
+
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3703
|
+
amount: splitAmount.toString(),
|
|
3704
|
+
coinId: coinIdHex,
|
|
3705
|
+
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3706
|
+
splitGroupId,
|
|
3707
|
+
senderPubkey,
|
|
3708
|
+
recipientSaltHex: toHex2(recipientSalt),
|
|
3709
|
+
transferSaltHex: toHex2(transferSalt),
|
|
3710
|
+
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3711
|
+
finalRecipientStateJson: "",
|
|
3712
|
+
// Recipient creates their own
|
|
3713
|
+
recipientAddressJson: recipientAddressStr,
|
|
3714
|
+
nametagTokenJson
|
|
3715
|
+
};
|
|
3716
|
+
return {
|
|
3717
|
+
bundle,
|
|
3718
|
+
splitGroupId,
|
|
3719
|
+
startBackground: async () => {
|
|
3720
|
+
if (!options?.skipBackground) {
|
|
3721
|
+
await this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3722
|
+
signingService: this.signingService,
|
|
3723
|
+
tokenType: tokenToSplit.type,
|
|
3724
|
+
coinId,
|
|
3725
|
+
senderTokenId,
|
|
3726
|
+
senderSalt,
|
|
3727
|
+
onProgress: options?.onBackgroundProgress,
|
|
3728
|
+
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3729
|
+
onStorageSync: options?.onStorageSync
|
|
3730
|
+
});
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
};
|
|
3734
|
+
}
|
|
3735
|
+
/**
|
|
3736
|
+
* Execute an instant split transfer with V5 optimized flow.
|
|
3737
|
+
*
|
|
3738
|
+
* Builds the bundle via buildSplitBundle(), sends via transport,
|
|
3739
|
+
* and starts background processing.
|
|
3592
3740
|
*
|
|
3593
3741
|
* @param tokenToSplit - The SDK token to split
|
|
3594
3742
|
* @param splitAmount - Amount to send to recipient
|
|
@@ -3602,117 +3750,19 @@ var InstantSplitExecutor = class {
|
|
|
3602
3750
|
*/
|
|
3603
3751
|
async executeSplitInstant(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, transport, recipientPubkey, options) {
|
|
3604
3752
|
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
3753
|
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,
|
|
3754
|
+
const buildResult = await this.buildSplitBundle(
|
|
3755
|
+
tokenToSplit,
|
|
3756
|
+
splitAmount,
|
|
3757
|
+
remainderAmount,
|
|
3758
|
+
coinIdHex,
|
|
3675
3759
|
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
|
|
3760
|
+
options
|
|
3685
3761
|
);
|
|
3686
|
-
|
|
3687
|
-
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3762
|
+
console.log("[InstantSplit] Sending via transport...");
|
|
3688
3763
|
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
3764
|
const nostrEventId = await transport.sendTokenTransfer(recipientPubkey, {
|
|
3715
|
-
token: JSON.stringify(bundle),
|
|
3765
|
+
token: JSON.stringify(buildResult.bundle),
|
|
3716
3766
|
proof: null,
|
|
3717
3767
|
// Proof is included in the bundle
|
|
3718
3768
|
memo: options?.memo,
|
|
@@ -3723,25 +3773,13 @@ var InstantSplitExecutor = class {
|
|
|
3723
3773
|
const criticalPathDuration = performance.now() - startTime;
|
|
3724
3774
|
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3725
3775
|
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
|
-
}
|
|
3776
|
+
const backgroundPromise = buildResult.startBackground();
|
|
3739
3777
|
return {
|
|
3740
3778
|
success: true,
|
|
3741
3779
|
nostrEventId,
|
|
3742
|
-
splitGroupId,
|
|
3780
|
+
splitGroupId: buildResult.splitGroupId,
|
|
3743
3781
|
criticalPathDurationMs: criticalPathDuration,
|
|
3744
|
-
backgroundStarted:
|
|
3782
|
+
backgroundStarted: true,
|
|
3745
3783
|
backgroundPromise
|
|
3746
3784
|
};
|
|
3747
3785
|
} catch (error) {
|
|
@@ -3750,7 +3788,6 @@ var InstantSplitExecutor = class {
|
|
|
3750
3788
|
console.error(`[InstantSplit] Failed after ${duration.toFixed(0)}ms:`, error);
|
|
3751
3789
|
return {
|
|
3752
3790
|
success: false,
|
|
3753
|
-
splitGroupId,
|
|
3754
3791
|
criticalPathDurationMs: duration,
|
|
3755
3792
|
error: errorMessage,
|
|
3756
3793
|
backgroundStarted: false
|
|
@@ -3955,6 +3992,11 @@ function isInstantSplitBundleV4(obj) {
|
|
|
3955
3992
|
function isInstantSplitBundleV5(obj) {
|
|
3956
3993
|
return isInstantSplitBundle(obj) && obj.version === "5.0";
|
|
3957
3994
|
}
|
|
3995
|
+
function isCombinedTransferBundleV6(obj) {
|
|
3996
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
3997
|
+
const b = obj;
|
|
3998
|
+
return b.version === "6.0" && b.type === "COMBINED_TRANSFER";
|
|
3999
|
+
}
|
|
3958
4000
|
|
|
3959
4001
|
// modules/payments/InstantSplitProcessor.ts
|
|
3960
4002
|
function fromHex3(hex) {
|
|
@@ -4279,6 +4321,7 @@ function computeHistoryDedupKey(type, tokenId, transferId) {
|
|
|
4279
4321
|
if (tokenId) return `${type}_${tokenId}`;
|
|
4280
4322
|
return `${type}_${crypto.randomUUID()}`;
|
|
4281
4323
|
}
|
|
4324
|
+
var MAX_SYNCED_HISTORY_ENTRIES = 5e3;
|
|
4282
4325
|
function enrichWithRegistry(info) {
|
|
4283
4326
|
const registry = TokenRegistry.getInstance();
|
|
4284
4327
|
const def = registry.getDefinition(info.coinId);
|
|
@@ -4608,6 +4651,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4608
4651
|
// Survives page reloads via KV storage so Nostr re-deliveries are ignored
|
|
4609
4652
|
// even when the confirmed token's in-memory ID differs from v5split_{id}.
|
|
4610
4653
|
processedSplitGroupIds = /* @__PURE__ */ new Set();
|
|
4654
|
+
// Persistent dedup: tracks V6 combined transfer IDs that have been processed.
|
|
4655
|
+
processedCombinedTransferIds = /* @__PURE__ */ new Set();
|
|
4611
4656
|
// Storage event subscriptions (push-based sync)
|
|
4612
4657
|
storageEventUnsubscribers = [];
|
|
4613
4658
|
syncDebounceTimer = null;
|
|
@@ -4701,6 +4746,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4701
4746
|
const result = await provider.load();
|
|
4702
4747
|
if (result.success && result.data) {
|
|
4703
4748
|
this.loadFromStorageData(result.data);
|
|
4749
|
+
const txfData = result.data;
|
|
4750
|
+
if (txfData._history && txfData._history.length > 0) {
|
|
4751
|
+
await this.importRemoteHistoryEntries(txfData._history);
|
|
4752
|
+
}
|
|
4704
4753
|
this.log(`Loaded metadata from provider ${id}`);
|
|
4705
4754
|
break;
|
|
4706
4755
|
}
|
|
@@ -4708,10 +4757,23 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4708
4757
|
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4709
4758
|
}
|
|
4710
4759
|
}
|
|
4760
|
+
for (const [id, token] of this.tokens) {
|
|
4761
|
+
try {
|
|
4762
|
+
if (token.sdkData) {
|
|
4763
|
+
const data = JSON.parse(token.sdkData);
|
|
4764
|
+
if (data?._placeholder) {
|
|
4765
|
+
this.tokens.delete(id);
|
|
4766
|
+
console.log(`[Payments] Removed stale placeholder token: ${id}`);
|
|
4767
|
+
}
|
|
4768
|
+
}
|
|
4769
|
+
} catch {
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4711
4772
|
const loadedTokens = Array.from(this.tokens.values()).map((t) => `${t.id.slice(0, 12)}(${t.status})`);
|
|
4712
4773
|
console.log(`[Payments][DEBUG] load(): from TXF providers: ${this.tokens.size} tokens [${loadedTokens.join(", ")}]`);
|
|
4713
4774
|
await this.loadPendingV5Tokens();
|
|
4714
4775
|
await this.loadProcessedSplitGroupIds();
|
|
4776
|
+
await this.loadProcessedCombinedTransferIds();
|
|
4715
4777
|
await this.loadHistory();
|
|
4716
4778
|
const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
|
|
4717
4779
|
if (pending2) {
|
|
@@ -4803,12 +4865,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4803
4865
|
token.status = "transferring";
|
|
4804
4866
|
this.tokens.set(token.id, token);
|
|
4805
4867
|
}
|
|
4868
|
+
await this.save();
|
|
4806
4869
|
await this.saveToOutbox(result, recipientPubkey);
|
|
4807
4870
|
result.status = "submitted";
|
|
4808
4871
|
const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
|
|
4809
4872
|
const transferMode = request.transferMode ?? "instant";
|
|
4810
|
-
if (
|
|
4811
|
-
if (
|
|
4873
|
+
if (transferMode === "conservative") {
|
|
4874
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4812
4875
|
this.log("Executing conservative split...");
|
|
4813
4876
|
const splitExecutor = new TokenSplitExecutor({
|
|
4814
4877
|
stateTransitionClient: stClient,
|
|
@@ -4852,27 +4915,59 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4852
4915
|
requestIdHex: splitRequestIdHex
|
|
4853
4916
|
});
|
|
4854
4917
|
this.log(`Conservative split transfer completed`);
|
|
4855
|
-
}
|
|
4856
|
-
|
|
4857
|
-
const
|
|
4918
|
+
}
|
|
4919
|
+
for (const tokenWithAmount of splitPlan.tokensToTransferDirectly) {
|
|
4920
|
+
const token = tokenWithAmount.uiToken;
|
|
4921
|
+
const commitment = await this.createSdkCommitment(token, recipientAddress, signingService);
|
|
4922
|
+
console.log(`[Payments] CONSERVATIVE: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4923
|
+
const submitResponse = await stClient.submitTransferCommitment(commitment);
|
|
4924
|
+
if (submitResponse.status !== "SUCCESS" && submitResponse.status !== "REQUEST_ID_EXISTS") {
|
|
4925
|
+
throw new Error(`Transfer commitment failed: ${submitResponse.status}`);
|
|
4926
|
+
}
|
|
4927
|
+
const inclusionProof = await waitInclusionProof5(trustBase, stClient, commitment);
|
|
4928
|
+
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4929
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4930
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4931
|
+
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
4932
|
+
memo: request.memo
|
|
4933
|
+
});
|
|
4934
|
+
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
4935
|
+
const requestIdBytes = commitment.requestId;
|
|
4936
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4937
|
+
result.tokenTransfers.push({
|
|
4938
|
+
sourceTokenId: token.id,
|
|
4939
|
+
method: "direct",
|
|
4940
|
+
requestIdHex
|
|
4941
|
+
});
|
|
4942
|
+
this.log(`Token ${token.id} sent via CONSERVATIVE, requestId: ${requestIdHex}`);
|
|
4943
|
+
await this.removeToken(token.id);
|
|
4944
|
+
}
|
|
4945
|
+
} else {
|
|
4946
|
+
const devMode = this.deps.oracle.isDevMode?.() ?? false;
|
|
4947
|
+
const senderPubkey = this.deps.identity.chainPubkey;
|
|
4948
|
+
let changeTokenPlaceholderId = null;
|
|
4949
|
+
let builtSplit = null;
|
|
4950
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4951
|
+
this.log("Building instant split bundle...");
|
|
4858
4952
|
const executor = new InstantSplitExecutor({
|
|
4859
4953
|
stateTransitionClient: stClient,
|
|
4860
4954
|
trustBase,
|
|
4861
4955
|
signingService,
|
|
4862
4956
|
devMode
|
|
4863
4957
|
});
|
|
4864
|
-
|
|
4958
|
+
builtSplit = await executor.buildSplitBundle(
|
|
4865
4959
|
splitPlan.tokenToSplit.sdkToken,
|
|
4866
4960
|
splitPlan.splitAmount,
|
|
4867
4961
|
splitPlan.remainderAmount,
|
|
4868
4962
|
splitPlan.coinId,
|
|
4869
4963
|
recipientAddress,
|
|
4870
|
-
this.deps.transport,
|
|
4871
|
-
recipientPubkey,
|
|
4872
4964
|
{
|
|
4873
4965
|
memo: request.memo,
|
|
4874
4966
|
onChangeTokenCreated: async (changeToken) => {
|
|
4875
4967
|
const changeTokenData = changeToken.toJSON();
|
|
4968
|
+
if (changeTokenPlaceholderId && this.tokens.has(changeTokenPlaceholderId)) {
|
|
4969
|
+
this.tokens.delete(changeTokenPlaceholderId);
|
|
4970
|
+
}
|
|
4876
4971
|
const uiToken = {
|
|
4877
4972
|
id: crypto.randomUUID(),
|
|
4878
4973
|
coinId: request.coinId,
|
|
@@ -4895,65 +4990,103 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4895
4990
|
}
|
|
4896
4991
|
}
|
|
4897
4992
|
);
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
this.
|
|
4903
|
-
|
|
4993
|
+
this.log(`Split bundle built: splitGroupId=${builtSplit.splitGroupId}`);
|
|
4994
|
+
}
|
|
4995
|
+
const directCommitments = await Promise.all(
|
|
4996
|
+
splitPlan.tokensToTransferDirectly.map(
|
|
4997
|
+
(tw) => this.createSdkCommitment(tw.uiToken, recipientAddress, signingService)
|
|
4998
|
+
)
|
|
4999
|
+
);
|
|
5000
|
+
const directTokenEntries = splitPlan.tokensToTransferDirectly.map(
|
|
5001
|
+
(tw, i) => ({
|
|
5002
|
+
sourceToken: JSON.stringify(tw.sdkToken.toJSON()),
|
|
5003
|
+
commitmentData: JSON.stringify(directCommitments[i].toJSON()),
|
|
5004
|
+
amount: tw.uiToken.amount,
|
|
5005
|
+
coinId: tw.uiToken.coinId,
|
|
5006
|
+
tokenId: extractTokenIdFromSdkData(tw.uiToken.sdkData) || void 0
|
|
5007
|
+
})
|
|
5008
|
+
);
|
|
5009
|
+
const combinedBundle = {
|
|
5010
|
+
version: "6.0",
|
|
5011
|
+
type: "COMBINED_TRANSFER",
|
|
5012
|
+
transferId: result.id,
|
|
5013
|
+
splitBundle: builtSplit?.bundle ?? null,
|
|
5014
|
+
directTokens: directTokenEntries,
|
|
5015
|
+
totalAmount: request.amount.toString(),
|
|
5016
|
+
coinId: request.coinId,
|
|
5017
|
+
senderPubkey,
|
|
5018
|
+
memo: request.memo
|
|
5019
|
+
};
|
|
5020
|
+
console.log(
|
|
5021
|
+
`[Payments] Sending V6 combined bundle: transfer=${result.id.slice(0, 8)}... split=${!!builtSplit} direct=${directTokenEntries.length}`
|
|
5022
|
+
);
|
|
5023
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
5024
|
+
token: JSON.stringify(combinedBundle),
|
|
5025
|
+
proof: null,
|
|
5026
|
+
memo: request.memo,
|
|
5027
|
+
sender: { transportPubkey: senderPubkey }
|
|
5028
|
+
});
|
|
5029
|
+
console.log(`[Payments] V6 combined bundle sent successfully`);
|
|
5030
|
+
if (builtSplit) {
|
|
5031
|
+
const bgPromise = builtSplit.startBackground();
|
|
5032
|
+
this.pendingBackgroundTasks.push(bgPromise);
|
|
5033
|
+
}
|
|
5034
|
+
if (builtSplit && splitPlan.remainderAmount) {
|
|
5035
|
+
changeTokenPlaceholderId = crypto.randomUUID();
|
|
5036
|
+
const placeholder = {
|
|
5037
|
+
id: changeTokenPlaceholderId,
|
|
5038
|
+
coinId: request.coinId,
|
|
5039
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
5040
|
+
name: this.getCoinName(request.coinId),
|
|
5041
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
5042
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
5043
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
5044
|
+
status: "transferring",
|
|
5045
|
+
createdAt: Date.now(),
|
|
5046
|
+
updatedAt: Date.now(),
|
|
5047
|
+
sdkData: JSON.stringify({ _placeholder: true })
|
|
5048
|
+
};
|
|
5049
|
+
this.tokens.set(placeholder.id, placeholder);
|
|
5050
|
+
this.log(`Placeholder change token created: ${placeholder.id} (${placeholder.amount})`);
|
|
5051
|
+
}
|
|
5052
|
+
for (const commitment of directCommitments) {
|
|
5053
|
+
stClient.submitTransferCommitment(commitment).catch(
|
|
5054
|
+
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
5055
|
+
);
|
|
5056
|
+
}
|
|
5057
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4904
5058
|
await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
|
|
4905
5059
|
result.tokenTransfers.push({
|
|
4906
5060
|
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4907
5061
|
method: "split",
|
|
4908
|
-
splitGroupId:
|
|
4909
|
-
nostrEventId: instantResult.nostrEventId
|
|
5062
|
+
splitGroupId: builtSplit.splitGroupId
|
|
4910
5063
|
});
|
|
4911
|
-
this.log(`Instant split transfer completed`);
|
|
4912
5064
|
}
|
|
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
|
|
5065
|
+
for (let i = 0; i < splitPlan.tokensToTransferDirectly.length; i++) {
|
|
5066
|
+
const token = splitPlan.tokensToTransferDirectly[i].uiToken;
|
|
5067
|
+
const commitment = directCommitments[i];
|
|
5068
|
+
const requestIdBytes = commitment.requestId;
|
|
5069
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
5070
|
+
result.tokenTransfers.push({
|
|
5071
|
+
sourceTokenId: token.id,
|
|
5072
|
+
method: "direct",
|
|
5073
|
+
requestIdHex
|
|
4937
5074
|
});
|
|
4938
|
-
|
|
4939
|
-
stClient.submitTransferCommitment(commitment).catch(
|
|
4940
|
-
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
4941
|
-
);
|
|
5075
|
+
await this.removeToken(token.id);
|
|
4942
5076
|
}
|
|
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);
|
|
5077
|
+
this.log(`V6 combined transfer completed`);
|
|
4952
5078
|
}
|
|
4953
5079
|
result.status = "delivered";
|
|
4954
5080
|
await this.save();
|
|
4955
5081
|
await this.removeFromOutbox(result.id);
|
|
4956
5082
|
result.status = "completed";
|
|
5083
|
+
const tokenMap = new Map(result.tokens.map((t) => [t.id, t]));
|
|
5084
|
+
const sentTokenIds = result.tokenTransfers.map((tt) => ({
|
|
5085
|
+
id: tt.sourceTokenId,
|
|
5086
|
+
// For split tokens, use splitAmount (the portion sent), not the original token amount
|
|
5087
|
+
amount: tt.method === "split" ? splitPlan.splitAmount?.toString() || "0" : tokenMap.get(tt.sourceTokenId)?.amount || "0",
|
|
5088
|
+
source: tt.method === "split" ? "split" : "direct"
|
|
5089
|
+
}));
|
|
4957
5090
|
const sentTokenId = result.tokens[0] ? extractTokenIdFromSdkData(result.tokens[0].sdkData) : void 0;
|
|
4958
5091
|
await this.addToHistory({
|
|
4959
5092
|
type: "SENT",
|
|
@@ -4966,7 +5099,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4966
5099
|
recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
|
|
4967
5100
|
memo: request.memo,
|
|
4968
5101
|
transferId: result.id,
|
|
4969
|
-
tokenId: sentTokenId || void 0
|
|
5102
|
+
tokenId: sentTokenId || void 0,
|
|
5103
|
+
tokenIds: sentTokenIds.length > 0 ? sentTokenIds : void 0
|
|
4970
5104
|
});
|
|
4971
5105
|
this.deps.emitEvent("transfer:confirmed", result);
|
|
4972
5106
|
return result;
|
|
@@ -5136,6 +5270,267 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5136
5270
|
};
|
|
5137
5271
|
}
|
|
5138
5272
|
}
|
|
5273
|
+
// ===========================================================================
|
|
5274
|
+
// Shared Helpers for V5 and V6 Receiver Processing
|
|
5275
|
+
// ===========================================================================
|
|
5276
|
+
/**
|
|
5277
|
+
* Save a V5 split bundle as an unconfirmed token (shared by V5 standalone and V6 combined).
|
|
5278
|
+
* Returns the created UI token, or null if deduped.
|
|
5279
|
+
*
|
|
5280
|
+
* @param deferPersistence - If true, skip addToken/save calls (caller batches them).
|
|
5281
|
+
* The token is still added to the in-memory map for dedup; caller must call save().
|
|
5282
|
+
*/
|
|
5283
|
+
async saveUnconfirmedV5Token(bundle, senderPubkey, deferPersistence = false) {
|
|
5284
|
+
const deterministicId = `v5split_${bundle.splitGroupId}`;
|
|
5285
|
+
if (this.tokens.has(deterministicId) || this.processedSplitGroupIds.has(bundle.splitGroupId)) {
|
|
5286
|
+
console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
|
|
5287
|
+
return null;
|
|
5288
|
+
}
|
|
5289
|
+
const registry = TokenRegistry.getInstance();
|
|
5290
|
+
const pendingData = {
|
|
5291
|
+
type: "v5_bundle",
|
|
5292
|
+
stage: "RECEIVED",
|
|
5293
|
+
bundleJson: JSON.stringify(bundle),
|
|
5294
|
+
senderPubkey,
|
|
5295
|
+
savedAt: Date.now(),
|
|
5296
|
+
attemptCount: 0
|
|
5297
|
+
};
|
|
5298
|
+
const uiToken = {
|
|
5299
|
+
id: deterministicId,
|
|
5300
|
+
coinId: bundle.coinId,
|
|
5301
|
+
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
5302
|
+
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
5303
|
+
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
5304
|
+
amount: bundle.amount,
|
|
5305
|
+
status: "submitted",
|
|
5306
|
+
// UNCONFIRMED
|
|
5307
|
+
createdAt: Date.now(),
|
|
5308
|
+
updatedAt: Date.now(),
|
|
5309
|
+
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
5310
|
+
};
|
|
5311
|
+
this.processedSplitGroupIds.add(bundle.splitGroupId);
|
|
5312
|
+
if (deferPersistence) {
|
|
5313
|
+
this.tokens.set(uiToken.id, uiToken);
|
|
5314
|
+
} else {
|
|
5315
|
+
await this.addToken(uiToken);
|
|
5316
|
+
await this.saveProcessedSplitGroupIds();
|
|
5317
|
+
}
|
|
5318
|
+
return uiToken;
|
|
5319
|
+
}
|
|
5320
|
+
/**
|
|
5321
|
+
* Save a commitment-only (NOSTR-FIRST) token and start proof polling.
|
|
5322
|
+
* Shared by standalone NOSTR-FIRST handler and V6 combined handler.
|
|
5323
|
+
* Returns the created UI token, or null if deduped/tombstoned.
|
|
5324
|
+
*
|
|
5325
|
+
* @param deferPersistence - If true, skip save() and commitment submission
|
|
5326
|
+
* (caller batches them). Token is added to in-memory map + proof polling is queued.
|
|
5327
|
+
* @param skipGenesisDedup - If true, skip genesis-ID-only dedup. V6 handler sets this
|
|
5328
|
+
* because bundle-level dedup protects against replays, and split children share genesis IDs.
|
|
5329
|
+
*/
|
|
5330
|
+
async saveCommitmentOnlyToken(sourceTokenInput, commitmentInput, senderPubkey, deferPersistence = false, skipGenesisDedup = false) {
|
|
5331
|
+
const tokenInfo = await parseTokenInfo(sourceTokenInput);
|
|
5332
|
+
const sdkData = typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput);
|
|
5333
|
+
const nostrTokenId = extractTokenIdFromSdkData(sdkData);
|
|
5334
|
+
const nostrStateHash = extractStateHashFromSdkData(sdkData);
|
|
5335
|
+
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
5336
|
+
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
5337
|
+
return null;
|
|
5338
|
+
}
|
|
5339
|
+
if (nostrTokenId) {
|
|
5340
|
+
for (const existing of this.tokens.values()) {
|
|
5341
|
+
const existingTokenId = extractTokenIdFromSdkData(existing.sdkData);
|
|
5342
|
+
if (existingTokenId !== nostrTokenId) continue;
|
|
5343
|
+
const existingStateHash = extractStateHashFromSdkData(existing.sdkData);
|
|
5344
|
+
if (nostrStateHash && existingStateHash === nostrStateHash) {
|
|
5345
|
+
console.log(
|
|
5346
|
+
`[Payments] NOSTR-FIRST: Skipping duplicate token state ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`
|
|
5347
|
+
);
|
|
5348
|
+
return null;
|
|
5349
|
+
}
|
|
5350
|
+
if (!skipGenesisDedup) {
|
|
5351
|
+
console.log(
|
|
5352
|
+
`[Payments] NOSTR-FIRST: Skipping replay of finalized token ${nostrTokenId.slice(0, 8)}...`
|
|
5353
|
+
);
|
|
5354
|
+
return null;
|
|
5355
|
+
}
|
|
5356
|
+
}
|
|
5357
|
+
}
|
|
5358
|
+
const token = {
|
|
5359
|
+
id: crypto.randomUUID(),
|
|
5360
|
+
coinId: tokenInfo.coinId,
|
|
5361
|
+
symbol: tokenInfo.symbol,
|
|
5362
|
+
name: tokenInfo.name,
|
|
5363
|
+
decimals: tokenInfo.decimals,
|
|
5364
|
+
iconUrl: tokenInfo.iconUrl,
|
|
5365
|
+
amount: tokenInfo.amount,
|
|
5366
|
+
status: "submitted",
|
|
5367
|
+
// NOSTR-FIRST: unconfirmed until proof
|
|
5368
|
+
createdAt: Date.now(),
|
|
5369
|
+
updatedAt: Date.now(),
|
|
5370
|
+
sdkData
|
|
5371
|
+
};
|
|
5372
|
+
this.tokens.set(token.id, token);
|
|
5373
|
+
if (!deferPersistence) {
|
|
5374
|
+
await this.save();
|
|
5375
|
+
}
|
|
5376
|
+
try {
|
|
5377
|
+
const commitment = await TransferCommitment4.fromJSON(commitmentInput);
|
|
5378
|
+
const requestIdBytes = commitment.requestId;
|
|
5379
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
5380
|
+
if (!deferPersistence) {
|
|
5381
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5382
|
+
if (stClient) {
|
|
5383
|
+
const response = await stClient.submitTransferCommitment(commitment);
|
|
5384
|
+
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5387
|
+
this.addProofPollingJob({
|
|
5388
|
+
tokenId: token.id,
|
|
5389
|
+
requestIdHex,
|
|
5390
|
+
commitmentJson: JSON.stringify(commitmentInput),
|
|
5391
|
+
startedAt: Date.now(),
|
|
5392
|
+
attemptCount: 0,
|
|
5393
|
+
lastAttemptAt: 0,
|
|
5394
|
+
onProofReceived: async (tokenId) => {
|
|
5395
|
+
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
|
|
5396
|
+
}
|
|
5397
|
+
});
|
|
5398
|
+
} catch (err) {
|
|
5399
|
+
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
5400
|
+
}
|
|
5401
|
+
return token;
|
|
5402
|
+
}
|
|
5403
|
+
// ===========================================================================
|
|
5404
|
+
// Combined Transfer V6 — Receiver
|
|
5405
|
+
// ===========================================================================
|
|
5406
|
+
/**
|
|
5407
|
+
* Process a received COMBINED_TRANSFER V6 bundle.
|
|
5408
|
+
*
|
|
5409
|
+
* Unpacks a single Nostr message into its component tokens:
|
|
5410
|
+
* - Optional V5 split bundle (saved as unconfirmed, resolved lazily)
|
|
5411
|
+
* - Zero or more direct tokens (saved as unconfirmed, proof-polled)
|
|
5412
|
+
*
|
|
5413
|
+
* Emits ONE transfer:incoming event and records ONE history entry.
|
|
5414
|
+
*/
|
|
5415
|
+
async processCombinedTransferBundle(bundle, senderPubkey) {
|
|
5416
|
+
this.ensureInitialized();
|
|
5417
|
+
if (!this.loaded && this.loadedPromise) {
|
|
5418
|
+
await this.loadedPromise;
|
|
5419
|
+
}
|
|
5420
|
+
if (this.processedCombinedTransferIds.has(bundle.transferId)) {
|
|
5421
|
+
console.log(`[Payments] V6 combined transfer ${bundle.transferId.slice(0, 12)}... already processed, skipping`);
|
|
5422
|
+
return;
|
|
5423
|
+
}
|
|
5424
|
+
console.log(
|
|
5425
|
+
`[Payments] Processing V6 combined transfer ${bundle.transferId.slice(0, 12)}... (split=${!!bundle.splitBundle}, direct=${bundle.directTokens.length})`
|
|
5426
|
+
);
|
|
5427
|
+
const allTokens = [];
|
|
5428
|
+
const tokenBreakdown = [];
|
|
5429
|
+
const parsedDirectEntries = bundle.directTokens.map((entry) => ({
|
|
5430
|
+
sourceToken: typeof entry.sourceToken === "string" ? JSON.parse(entry.sourceToken) : entry.sourceToken,
|
|
5431
|
+
commitment: typeof entry.commitmentData === "string" ? JSON.parse(entry.commitmentData) : entry.commitmentData
|
|
5432
|
+
}));
|
|
5433
|
+
if (bundle.splitBundle) {
|
|
5434
|
+
const splitToken = await this.saveUnconfirmedV5Token(bundle.splitBundle, senderPubkey, true);
|
|
5435
|
+
if (splitToken) {
|
|
5436
|
+
allTokens.push(splitToken);
|
|
5437
|
+
tokenBreakdown.push({ id: splitToken.id, amount: splitToken.amount, source: "split" });
|
|
5438
|
+
} else {
|
|
5439
|
+
console.warn(`[Payments] V6: split token was deduped/failed \u2014 amount=${bundle.splitBundle.amount}`);
|
|
5440
|
+
}
|
|
5441
|
+
}
|
|
5442
|
+
const directResults = await Promise.all(
|
|
5443
|
+
parsedDirectEntries.map(
|
|
5444
|
+
({ sourceToken, commitment }) => this.saveCommitmentOnlyToken(sourceToken, commitment, senderPubkey, true, true)
|
|
5445
|
+
)
|
|
5446
|
+
);
|
|
5447
|
+
for (let i = 0; i < directResults.length; i++) {
|
|
5448
|
+
const token = directResults[i];
|
|
5449
|
+
if (token) {
|
|
5450
|
+
allTokens.push(token);
|
|
5451
|
+
tokenBreakdown.push({ id: token.id, amount: token.amount, source: "direct" });
|
|
5452
|
+
} else {
|
|
5453
|
+
const entry = bundle.directTokens[i];
|
|
5454
|
+
console.warn(
|
|
5455
|
+
`[Payments] V6: direct token #${i} dropped (amount=${entry.amount}, tokenId=${entry.tokenId?.slice(0, 12) ?? "N/A"})`
|
|
5456
|
+
);
|
|
5457
|
+
}
|
|
5458
|
+
}
|
|
5459
|
+
if (allTokens.length === 0) {
|
|
5460
|
+
console.log(`[Payments] V6 combined transfer: all tokens deduped, nothing to save`);
|
|
5461
|
+
return;
|
|
5462
|
+
}
|
|
5463
|
+
this.processedCombinedTransferIds.add(bundle.transferId);
|
|
5464
|
+
const [senderInfo] = await Promise.all([
|
|
5465
|
+
this.resolveSenderInfo(senderPubkey),
|
|
5466
|
+
this.save(),
|
|
5467
|
+
this.saveProcessedCombinedTransferIds(),
|
|
5468
|
+
...bundle.splitBundle ? [this.saveProcessedSplitGroupIds()] : []
|
|
5469
|
+
]);
|
|
5470
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5471
|
+
if (stClient) {
|
|
5472
|
+
for (const { commitment } of parsedDirectEntries) {
|
|
5473
|
+
TransferCommitment4.fromJSON(commitment).then(
|
|
5474
|
+
(c) => stClient.submitTransferCommitment(c)
|
|
5475
|
+
).catch(
|
|
5476
|
+
(err) => console.error("[Payments] V6 background commitment submit failed:", err)
|
|
5477
|
+
);
|
|
5478
|
+
}
|
|
5479
|
+
}
|
|
5480
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
5481
|
+
id: bundle.transferId,
|
|
5482
|
+
senderPubkey,
|
|
5483
|
+
senderNametag: senderInfo.senderNametag,
|
|
5484
|
+
tokens: allTokens,
|
|
5485
|
+
memo: bundle.memo,
|
|
5486
|
+
receivedAt: Date.now()
|
|
5487
|
+
});
|
|
5488
|
+
const actualAmount = allTokens.reduce((sum, t) => sum + BigInt(t.amount || "0"), 0n).toString();
|
|
5489
|
+
await this.addToHistory({
|
|
5490
|
+
type: "RECEIVED",
|
|
5491
|
+
amount: actualAmount,
|
|
5492
|
+
coinId: bundle.coinId,
|
|
5493
|
+
symbol: allTokens[0]?.symbol || bundle.coinId,
|
|
5494
|
+
timestamp: Date.now(),
|
|
5495
|
+
senderPubkey,
|
|
5496
|
+
...senderInfo,
|
|
5497
|
+
memo: bundle.memo,
|
|
5498
|
+
transferId: bundle.transferId,
|
|
5499
|
+
tokenId: allTokens[0]?.id,
|
|
5500
|
+
tokenIds: tokenBreakdown
|
|
5501
|
+
});
|
|
5502
|
+
if (bundle.splitBundle) {
|
|
5503
|
+
this.resolveUnconfirmed().catch(() => {
|
|
5504
|
+
});
|
|
5505
|
+
this.scheduleResolveUnconfirmed();
|
|
5506
|
+
}
|
|
5507
|
+
}
|
|
5508
|
+
/**
|
|
5509
|
+
* Persist processed combined transfer IDs to KV storage.
|
|
5510
|
+
*/
|
|
5511
|
+
async saveProcessedCombinedTransferIds() {
|
|
5512
|
+
const ids = Array.from(this.processedCombinedTransferIds);
|
|
5513
|
+
if (ids.length > 0) {
|
|
5514
|
+
await this.deps.storage.set(
|
|
5515
|
+
STORAGE_KEYS_ADDRESS.PROCESSED_COMBINED_TRANSFER_IDS,
|
|
5516
|
+
JSON.stringify(ids)
|
|
5517
|
+
);
|
|
5518
|
+
}
|
|
5519
|
+
}
|
|
5520
|
+
/**
|
|
5521
|
+
* Load processed combined transfer IDs from KV storage.
|
|
5522
|
+
*/
|
|
5523
|
+
async loadProcessedCombinedTransferIds() {
|
|
5524
|
+
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PROCESSED_COMBINED_TRANSFER_IDS);
|
|
5525
|
+
if (!data) return;
|
|
5526
|
+
try {
|
|
5527
|
+
const ids = JSON.parse(data);
|
|
5528
|
+
for (const id of ids) {
|
|
5529
|
+
this.processedCombinedTransferIds.add(id);
|
|
5530
|
+
}
|
|
5531
|
+
} catch {
|
|
5532
|
+
}
|
|
5533
|
+
}
|
|
5139
5534
|
/**
|
|
5140
5535
|
* Process a received INSTANT_SPLIT bundle.
|
|
5141
5536
|
*
|
|
@@ -5159,36 +5554,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5159
5554
|
return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
|
|
5160
5555
|
}
|
|
5161
5556
|
try {
|
|
5162
|
-
const
|
|
5163
|
-
if (
|
|
5164
|
-
console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
|
|
5557
|
+
const uiToken = await this.saveUnconfirmedV5Token(bundle, senderPubkey);
|
|
5558
|
+
if (!uiToken) {
|
|
5165
5559
|
return { success: true, durationMs: 0 };
|
|
5166
5560
|
}
|
|
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
5561
|
const senderInfo = await this.resolveSenderInfo(senderPubkey);
|
|
5193
5562
|
await this.addToHistory({
|
|
5194
5563
|
type: "RECEIVED",
|
|
@@ -5199,7 +5568,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5199
5568
|
senderPubkey,
|
|
5200
5569
|
...senderInfo,
|
|
5201
5570
|
memo,
|
|
5202
|
-
tokenId:
|
|
5571
|
+
tokenId: uiToken.id
|
|
5203
5572
|
});
|
|
5204
5573
|
this.deps.emitEvent("transfer:incoming", {
|
|
5205
5574
|
id: bundle.splitGroupId,
|
|
@@ -5849,16 +6218,18 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5849
6218
|
}
|
|
5850
6219
|
/**
|
|
5851
6220
|
* Aggregate tokens by coinId with confirmed/unconfirmed breakdown.
|
|
5852
|
-
* Excludes tokens with status 'spent'
|
|
6221
|
+
* Excludes tokens with status 'spent' or 'invalid'.
|
|
6222
|
+
* Tokens with status 'transferring' are counted as unconfirmed (visible in UI as "Sending").
|
|
5853
6223
|
*/
|
|
5854
6224
|
aggregateTokens(coinId) {
|
|
5855
6225
|
const assetsMap = /* @__PURE__ */ new Map();
|
|
5856
6226
|
for (const token of this.tokens.values()) {
|
|
5857
|
-
if (token.status === "spent" || token.status === "invalid"
|
|
6227
|
+
if (token.status === "spent" || token.status === "invalid") continue;
|
|
5858
6228
|
if (coinId && token.coinId !== coinId) continue;
|
|
5859
6229
|
const key = token.coinId;
|
|
5860
6230
|
const amount = BigInt(token.amount);
|
|
5861
6231
|
const isConfirmed = token.status === "confirmed";
|
|
6232
|
+
const isTransferring = token.status === "transferring";
|
|
5862
6233
|
const existing = assetsMap.get(key);
|
|
5863
6234
|
if (existing) {
|
|
5864
6235
|
if (isConfirmed) {
|
|
@@ -5868,6 +6239,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5868
6239
|
existing.unconfirmedAmount += amount;
|
|
5869
6240
|
existing.unconfirmedTokenCount++;
|
|
5870
6241
|
}
|
|
6242
|
+
if (isTransferring) existing.transferringTokenCount++;
|
|
5871
6243
|
} else {
|
|
5872
6244
|
assetsMap.set(key, {
|
|
5873
6245
|
coinId: token.coinId,
|
|
@@ -5878,7 +6250,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5878
6250
|
confirmedAmount: isConfirmed ? amount : 0n,
|
|
5879
6251
|
unconfirmedAmount: isConfirmed ? 0n : amount,
|
|
5880
6252
|
confirmedTokenCount: isConfirmed ? 1 : 0,
|
|
5881
|
-
unconfirmedTokenCount: isConfirmed ? 0 : 1
|
|
6253
|
+
unconfirmedTokenCount: isConfirmed ? 0 : 1,
|
|
6254
|
+
transferringTokenCount: isTransferring ? 1 : 0
|
|
5882
6255
|
});
|
|
5883
6256
|
}
|
|
5884
6257
|
}
|
|
@@ -5896,6 +6269,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5896
6269
|
unconfirmedAmount: raw.unconfirmedAmount.toString(),
|
|
5897
6270
|
confirmedTokenCount: raw.confirmedTokenCount,
|
|
5898
6271
|
unconfirmedTokenCount: raw.unconfirmedTokenCount,
|
|
6272
|
+
transferringTokenCount: raw.transferringTokenCount,
|
|
5899
6273
|
priceUsd: null,
|
|
5900
6274
|
priceEur: null,
|
|
5901
6275
|
change24h: null,
|
|
@@ -6748,6 +7122,33 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6748
7122
|
}
|
|
6749
7123
|
}
|
|
6750
7124
|
}
|
|
7125
|
+
/**
|
|
7126
|
+
* Import history entries from remote TXF data into local store.
|
|
7127
|
+
* Delegates to the local TokenStorageProvider's importHistoryEntries() for
|
|
7128
|
+
* persistent storage, with in-memory fallback.
|
|
7129
|
+
* Reused by both load() (initial IPFS fetch) and _doSync() (merge result).
|
|
7130
|
+
*/
|
|
7131
|
+
async importRemoteHistoryEntries(entries) {
|
|
7132
|
+
if (entries.length === 0) return 0;
|
|
7133
|
+
const provider = this.getLocalTokenStorageProvider();
|
|
7134
|
+
if (provider?.importHistoryEntries) {
|
|
7135
|
+
const imported2 = await provider.importHistoryEntries(entries);
|
|
7136
|
+
if (imported2 > 0) {
|
|
7137
|
+
this._historyCache = await provider.getHistoryEntries();
|
|
7138
|
+
}
|
|
7139
|
+
return imported2;
|
|
7140
|
+
}
|
|
7141
|
+
const existingKeys = new Set(this._historyCache.map((e) => e.dedupKey));
|
|
7142
|
+
let imported = 0;
|
|
7143
|
+
for (const entry of entries) {
|
|
7144
|
+
if (!existingKeys.has(entry.dedupKey)) {
|
|
7145
|
+
this._historyCache.push(entry);
|
|
7146
|
+
existingKeys.add(entry.dedupKey);
|
|
7147
|
+
imported++;
|
|
7148
|
+
}
|
|
7149
|
+
}
|
|
7150
|
+
return imported;
|
|
7151
|
+
}
|
|
6751
7152
|
/**
|
|
6752
7153
|
* Get the first local token storage provider (for history operations).
|
|
6753
7154
|
*/
|
|
@@ -6995,6 +7396,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6995
7396
|
if (this.nametags.length === 0 && savedNametags.length > 0) {
|
|
6996
7397
|
this.nametags = savedNametags;
|
|
6997
7398
|
}
|
|
7399
|
+
const txfData = result.merged;
|
|
7400
|
+
if (txfData._history && txfData._history.length > 0) {
|
|
7401
|
+
const imported = await this.importRemoteHistoryEntries(txfData._history);
|
|
7402
|
+
if (imported > 0) {
|
|
7403
|
+
this.log(`Imported ${imported} history entries from IPFS sync`);
|
|
7404
|
+
}
|
|
7405
|
+
}
|
|
6998
7406
|
totalAdded += result.added;
|
|
6999
7407
|
totalRemoved += result.removed;
|
|
7000
7408
|
}
|
|
@@ -7293,7 +7701,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7293
7701
|
/**
|
|
7294
7702
|
* Handle NOSTR-FIRST commitment-only transfer (recipient side)
|
|
7295
7703
|
* This is called when receiving a transfer with only commitmentData and no proof yet.
|
|
7296
|
-
*
|
|
7704
|
+
* Delegates to saveCommitmentOnlyToken() helper, then emits event + records history.
|
|
7297
7705
|
*/
|
|
7298
7706
|
async handleCommitmentOnlyTransfer(transfer, payload) {
|
|
7299
7707
|
try {
|
|
@@ -7303,41 +7711,22 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7303
7711
|
console.warn("[Payments] Invalid NOSTR-FIRST transfer format");
|
|
7304
7712
|
return;
|
|
7305
7713
|
}
|
|
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}`);
|
|
7714
|
+
const token = await this.saveCommitmentOnlyToken(
|
|
7715
|
+
sourceTokenInput,
|
|
7716
|
+
commitmentInput,
|
|
7717
|
+
transfer.senderTransportPubkey
|
|
7718
|
+
);
|
|
7719
|
+
if (!token) return;
|
|
7331
7720
|
const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
|
|
7332
|
-
|
|
7721
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
7333
7722
|
id: transfer.id,
|
|
7334
7723
|
senderPubkey: transfer.senderTransportPubkey,
|
|
7335
7724
|
senderNametag: senderInfo.senderNametag,
|
|
7336
7725
|
tokens: [token],
|
|
7337
7726
|
memo: payload.memo,
|
|
7338
7727
|
receivedAt: transfer.timestamp
|
|
7339
|
-
};
|
|
7340
|
-
|
|
7728
|
+
});
|
|
7729
|
+
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7341
7730
|
await this.addToHistory({
|
|
7342
7731
|
type: "RECEIVED",
|
|
7343
7732
|
amount: token.amount,
|
|
@@ -7349,29 +7738,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7349
7738
|
memo: payload.memo,
|
|
7350
7739
|
tokenId: nostrTokenId || token.id
|
|
7351
7740
|
});
|
|
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
7741
|
} catch (error) {
|
|
7376
7742
|
console.error("[Payments] Failed to process NOSTR-FIRST transfer:", error);
|
|
7377
7743
|
}
|
|
@@ -7490,6 +7856,28 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7490
7856
|
try {
|
|
7491
7857
|
const payload = transfer.payload;
|
|
7492
7858
|
console.log("[Payments][DEBUG] handleIncomingTransfer: keys=", Object.keys(payload).join(","));
|
|
7859
|
+
let combinedBundle = null;
|
|
7860
|
+
if (isCombinedTransferBundleV6(payload)) {
|
|
7861
|
+
combinedBundle = payload;
|
|
7862
|
+
} else if (payload.token) {
|
|
7863
|
+
try {
|
|
7864
|
+
const inner = typeof payload.token === "string" ? JSON.parse(payload.token) : payload.token;
|
|
7865
|
+
if (isCombinedTransferBundleV6(inner)) {
|
|
7866
|
+
combinedBundle = inner;
|
|
7867
|
+
}
|
|
7868
|
+
} catch {
|
|
7869
|
+
}
|
|
7870
|
+
}
|
|
7871
|
+
if (combinedBundle) {
|
|
7872
|
+
this.log("Processing COMBINED_TRANSFER V6 bundle...");
|
|
7873
|
+
try {
|
|
7874
|
+
await this.processCombinedTransferBundle(combinedBundle, transfer.senderTransportPubkey);
|
|
7875
|
+
this.log("COMBINED_TRANSFER V6 processed successfully");
|
|
7876
|
+
} catch (err) {
|
|
7877
|
+
console.error("[Payments] COMBINED_TRANSFER V6 processing error:", err);
|
|
7878
|
+
}
|
|
7879
|
+
return;
|
|
7880
|
+
}
|
|
7493
7881
|
let instantBundle = null;
|
|
7494
7882
|
if (isInstantSplitBundle(payload)) {
|
|
7495
7883
|
instantBundle = payload;
|
|
@@ -7641,17 +8029,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7641
8029
|
memo: payload.memo,
|
|
7642
8030
|
tokenId: incomingTokenId || token.id
|
|
7643
8031
|
});
|
|
8032
|
+
const incomingTransfer = {
|
|
8033
|
+
id: transfer.id,
|
|
8034
|
+
senderPubkey: transfer.senderTransportPubkey,
|
|
8035
|
+
senderNametag: senderInfo.senderNametag,
|
|
8036
|
+
tokens: [token],
|
|
8037
|
+
memo: payload.memo,
|
|
8038
|
+
receivedAt: transfer.timestamp
|
|
8039
|
+
};
|
|
8040
|
+
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
8041
|
+
this.log(`Incoming transfer processed: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
8042
|
+
} else {
|
|
8043
|
+
this.log(`Duplicate transfer ignored: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7644
8044
|
}
|
|
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
8045
|
} catch (error) {
|
|
7656
8046
|
console.error("[Payments] Failed to process incoming transfer:", error);
|
|
7657
8047
|
}
|
|
@@ -7720,6 +8110,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7720
8110
|
return data ? JSON.parse(data) : [];
|
|
7721
8111
|
}
|
|
7722
8112
|
async createStorageData() {
|
|
8113
|
+
const sorted = [...this._historyCache].sort((a, b) => b.timestamp - a.timestamp);
|
|
7723
8114
|
return await buildTxfStorageData(
|
|
7724
8115
|
Array.from(this.tokens.values()),
|
|
7725
8116
|
{
|
|
@@ -7731,7 +8122,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7731
8122
|
nametags: this.nametags,
|
|
7732
8123
|
tombstones: this.tombstones,
|
|
7733
8124
|
archivedTokens: this.archivedTokens,
|
|
7734
|
-
forkedTokens: this.forkedTokens
|
|
8125
|
+
forkedTokens: this.forkedTokens,
|
|
8126
|
+
historyEntries: sorted.slice(0, MAX_SYNCED_HISTORY_ENTRIES)
|
|
7735
8127
|
}
|
|
7736
8128
|
);
|
|
7737
8129
|
}
|
|
@@ -16772,6 +17164,7 @@ export {
|
|
|
16772
17164
|
identityFromMnemonicSync,
|
|
16773
17165
|
initSphere,
|
|
16774
17166
|
isArchivedKey,
|
|
17167
|
+
isCombinedTransferBundleV6,
|
|
16775
17168
|
isForkedKey,
|
|
16776
17169
|
isInstantSplitBundle,
|
|
16777
17170
|
isInstantSplitBundleV4,
|