@unicitylabs/sphere-sdk 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connect/index.cjs +3 -1
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/index.js +3 -1
- package/dist/connect/index.js.map +1 -1
- package/dist/core/index.cjs +615 -275
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +49 -2
- package/dist/core/index.d.ts +49 -2
- package/dist/core/index.js +615 -275
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/connect/index.cjs +3 -1
- package/dist/impl/browser/connect/index.cjs.map +1 -1
- package/dist/impl/browser/connect/index.js +3 -1
- package/dist/impl/browser/connect/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +5 -2
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +5 -2
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/browser/ipfs.cjs +3 -1
- package/dist/impl/browser/ipfs.cjs.map +1 -1
- package/dist/impl/browser/ipfs.js +3 -1
- package/dist/impl/browser/ipfs.js.map +1 -1
- package/dist/impl/nodejs/connect/index.cjs +3 -1
- package/dist/impl/nodejs/connect/index.cjs.map +1 -1
- package/dist/impl/nodejs/connect/index.js +3 -1
- package/dist/impl/nodejs/connect/index.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +5 -2
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +6 -0
- package/dist/impl/nodejs/index.d.ts +6 -0
- package/dist/impl/nodejs/index.js +5 -2
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +617 -275
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +118 -3
- package/dist/index.d.ts +118 -3
- package/dist/index.js +616 -275
- package/dist/index.js.map +1 -1
- package/dist/l1/index.cjs +3 -1
- package/dist/l1/index.cjs.map +1 -1
- package/dist/l1/index.js +3 -1
- package/dist/l1/index.js.map +1 -1
- package/package.json +1 -1
package/dist/core/index.cjs
CHANGED
|
@@ -105,7 +105,9 @@ var init_constants = __esm({
|
|
|
105
105
|
/** Group chat: processed event IDs for deduplication */
|
|
106
106
|
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
|
|
107
107
|
/** Processed V5 split group IDs for Nostr re-delivery dedup */
|
|
108
|
-
PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids"
|
|
108
|
+
PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids",
|
|
109
|
+
/** Processed V6 combined transfer IDs for Nostr re-delivery dedup */
|
|
110
|
+
PROCESSED_COMBINED_TRANSFER_IDS: "processed_combined_transfer_ids"
|
|
109
111
|
};
|
|
110
112
|
STORAGE_KEYS = {
|
|
111
113
|
...STORAGE_KEYS_GLOBAL,
|
|
@@ -3381,14 +3383,149 @@ var InstantSplitExecutor = class {
|
|
|
3381
3383
|
this.devMode = config.devMode ?? false;
|
|
3382
3384
|
}
|
|
3383
3385
|
/**
|
|
3384
|
-
*
|
|
3386
|
+
* Build a V5 split bundle WITHOUT sending it via transport.
|
|
3385
3387
|
*
|
|
3386
|
-
*
|
|
3388
|
+
* Steps 1-5 of the V5 flow:
|
|
3387
3389
|
* 1. Create and submit burn commitment
|
|
3388
3390
|
* 2. Wait for burn proof
|
|
3389
3391
|
* 3. Create mint commitments with SplitMintReason
|
|
3390
3392
|
* 4. Create transfer commitment (no mint proof needed)
|
|
3391
|
-
* 5.
|
|
3393
|
+
* 5. Package V5 bundle
|
|
3394
|
+
*
|
|
3395
|
+
* The caller is responsible for sending the bundle and then calling
|
|
3396
|
+
* `startBackground()` on the result to begin mint proof + change token creation.
|
|
3397
|
+
*/
|
|
3398
|
+
async buildSplitBundle(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, options) {
|
|
3399
|
+
const splitGroupId = crypto.randomUUID();
|
|
3400
|
+
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
3401
|
+
console.log(`[InstantSplit] Building V5 bundle for token ${tokenIdHex.slice(0, 8)}...`);
|
|
3402
|
+
const coinId = new import_CoinId3.CoinId(fromHex2(coinIdHex));
|
|
3403
|
+
const seedString = `${tokenIdHex}_${splitAmount.toString()}_${remainderAmount.toString()}_${Date.now()}`;
|
|
3404
|
+
const recipientTokenId = new import_TokenId3.TokenId(await sha2563(seedString));
|
|
3405
|
+
const senderTokenId = new import_TokenId3.TokenId(await sha2563(seedString + "_sender"));
|
|
3406
|
+
const recipientSalt = await sha2563(seedString + "_recipient_salt");
|
|
3407
|
+
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
3408
|
+
const senderAddressRef = await import_UnmaskedPredicateReference2.UnmaskedPredicateReference.create(
|
|
3409
|
+
tokenToSplit.type,
|
|
3410
|
+
this.signingService.algorithm,
|
|
3411
|
+
this.signingService.publicKey,
|
|
3412
|
+
import_HashAlgorithm3.HashAlgorithm.SHA256
|
|
3413
|
+
);
|
|
3414
|
+
const senderAddress = await senderAddressRef.toAddress();
|
|
3415
|
+
const builder = new import_TokenSplitBuilder2.TokenSplitBuilder();
|
|
3416
|
+
const coinDataA = import_TokenCoinData2.TokenCoinData.create([[coinId, splitAmount]]);
|
|
3417
|
+
builder.createToken(
|
|
3418
|
+
recipientTokenId,
|
|
3419
|
+
tokenToSplit.type,
|
|
3420
|
+
new Uint8Array(0),
|
|
3421
|
+
coinDataA,
|
|
3422
|
+
senderAddress,
|
|
3423
|
+
// Mint to sender first, then transfer
|
|
3424
|
+
recipientSalt,
|
|
3425
|
+
null
|
|
3426
|
+
);
|
|
3427
|
+
const coinDataB = import_TokenCoinData2.TokenCoinData.create([[coinId, remainderAmount]]);
|
|
3428
|
+
builder.createToken(
|
|
3429
|
+
senderTokenId,
|
|
3430
|
+
tokenToSplit.type,
|
|
3431
|
+
new Uint8Array(0),
|
|
3432
|
+
coinDataB,
|
|
3433
|
+
senderAddress,
|
|
3434
|
+
senderSalt,
|
|
3435
|
+
null
|
|
3436
|
+
);
|
|
3437
|
+
const split = await builder.build(tokenToSplit);
|
|
3438
|
+
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
3439
|
+
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
3440
|
+
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
3441
|
+
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3442
|
+
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3443
|
+
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3444
|
+
}
|
|
3445
|
+
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3446
|
+
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, burnCommitment);
|
|
3447
|
+
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3448
|
+
console.log(`[InstantSplit] Burn proof received`);
|
|
3449
|
+
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3450
|
+
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3451
|
+
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3452
|
+
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3453
|
+
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3454
|
+
const recipientMintCommitment = mintCommitments.find(
|
|
3455
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3456
|
+
);
|
|
3457
|
+
const senderMintCommitment = mintCommitments.find(
|
|
3458
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3459
|
+
);
|
|
3460
|
+
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3461
|
+
throw new Error("Failed to find expected mint commitments");
|
|
3462
|
+
}
|
|
3463
|
+
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3464
|
+
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3465
|
+
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3466
|
+
recipientMintCommitment.transactionData,
|
|
3467
|
+
recipientAddress,
|
|
3468
|
+
transferSalt,
|
|
3469
|
+
this.signingService
|
|
3470
|
+
);
|
|
3471
|
+
const mintedPredicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
|
|
3472
|
+
recipientTokenId,
|
|
3473
|
+
tokenToSplit.type,
|
|
3474
|
+
this.signingService,
|
|
3475
|
+
import_HashAlgorithm3.HashAlgorithm.SHA256,
|
|
3476
|
+
recipientSalt
|
|
3477
|
+
);
|
|
3478
|
+
const mintedState = new import_TokenState3.TokenState(mintedPredicate, null);
|
|
3479
|
+
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3480
|
+
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3481
|
+
let nametagTokenJson;
|
|
3482
|
+
const recipientAddressStr = recipientAddress.toString();
|
|
3483
|
+
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3484
|
+
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3485
|
+
}
|
|
3486
|
+
const bundle = {
|
|
3487
|
+
version: "5.0",
|
|
3488
|
+
type: "INSTANT_SPLIT",
|
|
3489
|
+
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3490
|
+
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3491
|
+
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3492
|
+
amount: splitAmount.toString(),
|
|
3493
|
+
coinId: coinIdHex,
|
|
3494
|
+
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3495
|
+
splitGroupId,
|
|
3496
|
+
senderPubkey,
|
|
3497
|
+
recipientSaltHex: toHex2(recipientSalt),
|
|
3498
|
+
transferSaltHex: toHex2(transferSalt),
|
|
3499
|
+
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3500
|
+
finalRecipientStateJson: "",
|
|
3501
|
+
// Recipient creates their own
|
|
3502
|
+
recipientAddressJson: recipientAddressStr,
|
|
3503
|
+
nametagTokenJson
|
|
3504
|
+
};
|
|
3505
|
+
return {
|
|
3506
|
+
bundle,
|
|
3507
|
+
splitGroupId,
|
|
3508
|
+
startBackground: async () => {
|
|
3509
|
+
if (!options?.skipBackground) {
|
|
3510
|
+
await this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3511
|
+
signingService: this.signingService,
|
|
3512
|
+
tokenType: tokenToSplit.type,
|
|
3513
|
+
coinId,
|
|
3514
|
+
senderTokenId,
|
|
3515
|
+
senderSalt,
|
|
3516
|
+
onProgress: options?.onBackgroundProgress,
|
|
3517
|
+
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3518
|
+
onStorageSync: options?.onStorageSync
|
|
3519
|
+
});
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
};
|
|
3523
|
+
}
|
|
3524
|
+
/**
|
|
3525
|
+
* Execute an instant split transfer with V5 optimized flow.
|
|
3526
|
+
*
|
|
3527
|
+
* Builds the bundle via buildSplitBundle(), sends via transport,
|
|
3528
|
+
* and starts background processing.
|
|
3392
3529
|
*
|
|
3393
3530
|
* @param tokenToSplit - The SDK token to split
|
|
3394
3531
|
* @param splitAmount - Amount to send to recipient
|
|
@@ -3402,117 +3539,19 @@ var InstantSplitExecutor = class {
|
|
|
3402
3539
|
*/
|
|
3403
3540
|
async executeSplitInstant(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, transport, recipientPubkey, options) {
|
|
3404
3541
|
const startTime = performance.now();
|
|
3405
|
-
const splitGroupId = crypto.randomUUID();
|
|
3406
|
-
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
3407
|
-
console.log(`[InstantSplit] Starting V5 split for token ${tokenIdHex.slice(0, 8)}...`);
|
|
3408
3542
|
try {
|
|
3409
|
-
const
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
3415
|
-
const senderAddressRef = await import_UnmaskedPredicateReference2.UnmaskedPredicateReference.create(
|
|
3416
|
-
tokenToSplit.type,
|
|
3417
|
-
this.signingService.algorithm,
|
|
3418
|
-
this.signingService.publicKey,
|
|
3419
|
-
import_HashAlgorithm3.HashAlgorithm.SHA256
|
|
3420
|
-
);
|
|
3421
|
-
const senderAddress = await senderAddressRef.toAddress();
|
|
3422
|
-
const builder = new import_TokenSplitBuilder2.TokenSplitBuilder();
|
|
3423
|
-
const coinDataA = import_TokenCoinData2.TokenCoinData.create([[coinId, splitAmount]]);
|
|
3424
|
-
builder.createToken(
|
|
3425
|
-
recipientTokenId,
|
|
3426
|
-
tokenToSplit.type,
|
|
3427
|
-
new Uint8Array(0),
|
|
3428
|
-
coinDataA,
|
|
3429
|
-
senderAddress,
|
|
3430
|
-
// Mint to sender first, then transfer
|
|
3431
|
-
recipientSalt,
|
|
3432
|
-
null
|
|
3433
|
-
);
|
|
3434
|
-
const coinDataB = import_TokenCoinData2.TokenCoinData.create([[coinId, remainderAmount]]);
|
|
3435
|
-
builder.createToken(
|
|
3436
|
-
senderTokenId,
|
|
3437
|
-
tokenToSplit.type,
|
|
3438
|
-
new Uint8Array(0),
|
|
3439
|
-
coinDataB,
|
|
3440
|
-
senderAddress,
|
|
3441
|
-
senderSalt,
|
|
3442
|
-
null
|
|
3443
|
-
);
|
|
3444
|
-
const split = await builder.build(tokenToSplit);
|
|
3445
|
-
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
3446
|
-
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
3447
|
-
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
3448
|
-
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3449
|
-
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3450
|
-
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3451
|
-
}
|
|
3452
|
-
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3453
|
-
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, burnCommitment);
|
|
3454
|
-
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3455
|
-
const burnDuration = performance.now() - startTime;
|
|
3456
|
-
console.log(`[InstantSplit] Burn proof received in ${burnDuration.toFixed(0)}ms`);
|
|
3457
|
-
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3458
|
-
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3459
|
-
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3460
|
-
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3461
|
-
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3462
|
-
const recipientMintCommitment = mintCommitments.find(
|
|
3463
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3464
|
-
);
|
|
3465
|
-
const senderMintCommitment = mintCommitments.find(
|
|
3466
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3467
|
-
);
|
|
3468
|
-
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3469
|
-
throw new Error("Failed to find expected mint commitments");
|
|
3470
|
-
}
|
|
3471
|
-
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3472
|
-
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3473
|
-
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3474
|
-
recipientMintCommitment.transactionData,
|
|
3543
|
+
const buildResult = await this.buildSplitBundle(
|
|
3544
|
+
tokenToSplit,
|
|
3545
|
+
splitAmount,
|
|
3546
|
+
remainderAmount,
|
|
3547
|
+
coinIdHex,
|
|
3475
3548
|
recipientAddress,
|
|
3476
|
-
|
|
3477
|
-
this.signingService
|
|
3478
|
-
);
|
|
3479
|
-
const mintedPredicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
|
|
3480
|
-
recipientTokenId,
|
|
3481
|
-
tokenToSplit.type,
|
|
3482
|
-
this.signingService,
|
|
3483
|
-
import_HashAlgorithm3.HashAlgorithm.SHA256,
|
|
3484
|
-
recipientSalt
|
|
3549
|
+
options
|
|
3485
3550
|
);
|
|
3486
|
-
|
|
3487
|
-
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3551
|
+
console.log("[InstantSplit] Sending via transport...");
|
|
3488
3552
|
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3489
|
-
let nametagTokenJson;
|
|
3490
|
-
const recipientAddressStr = recipientAddress.toString();
|
|
3491
|
-
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3492
|
-
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3493
|
-
}
|
|
3494
|
-
const bundle = {
|
|
3495
|
-
version: "5.0",
|
|
3496
|
-
type: "INSTANT_SPLIT",
|
|
3497
|
-
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3498
|
-
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3499
|
-
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3500
|
-
amount: splitAmount.toString(),
|
|
3501
|
-
coinId: coinIdHex,
|
|
3502
|
-
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3503
|
-
splitGroupId,
|
|
3504
|
-
senderPubkey,
|
|
3505
|
-
recipientSaltHex: toHex2(recipientSalt),
|
|
3506
|
-
transferSaltHex: toHex2(transferSalt),
|
|
3507
|
-
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3508
|
-
finalRecipientStateJson: "",
|
|
3509
|
-
// Recipient creates their own
|
|
3510
|
-
recipientAddressJson: recipientAddressStr,
|
|
3511
|
-
nametagTokenJson
|
|
3512
|
-
};
|
|
3513
|
-
console.log("[InstantSplit] Step 6: Sending via transport...");
|
|
3514
3553
|
const nostrEventId = await transport.sendTokenTransfer(recipientPubkey, {
|
|
3515
|
-
token: JSON.stringify(bundle),
|
|
3554
|
+
token: JSON.stringify(buildResult.bundle),
|
|
3516
3555
|
proof: null,
|
|
3517
3556
|
// Proof is included in the bundle
|
|
3518
3557
|
memo: options?.memo,
|
|
@@ -3523,25 +3562,13 @@ var InstantSplitExecutor = class {
|
|
|
3523
3562
|
const criticalPathDuration = performance.now() - startTime;
|
|
3524
3563
|
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3525
3564
|
options?.onNostrDelivered?.(nostrEventId);
|
|
3526
|
-
|
|
3527
|
-
if (!options?.skipBackground) {
|
|
3528
|
-
backgroundPromise = this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3529
|
-
signingService: this.signingService,
|
|
3530
|
-
tokenType: tokenToSplit.type,
|
|
3531
|
-
coinId,
|
|
3532
|
-
senderTokenId,
|
|
3533
|
-
senderSalt,
|
|
3534
|
-
onProgress: options?.onBackgroundProgress,
|
|
3535
|
-
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3536
|
-
onStorageSync: options?.onStorageSync
|
|
3537
|
-
});
|
|
3538
|
-
}
|
|
3565
|
+
const backgroundPromise = buildResult.startBackground();
|
|
3539
3566
|
return {
|
|
3540
3567
|
success: true,
|
|
3541
3568
|
nostrEventId,
|
|
3542
|
-
splitGroupId,
|
|
3569
|
+
splitGroupId: buildResult.splitGroupId,
|
|
3543
3570
|
criticalPathDurationMs: criticalPathDuration,
|
|
3544
|
-
backgroundStarted:
|
|
3571
|
+
backgroundStarted: true,
|
|
3545
3572
|
backgroundPromise
|
|
3546
3573
|
};
|
|
3547
3574
|
} catch (error) {
|
|
@@ -3550,7 +3577,6 @@ var InstantSplitExecutor = class {
|
|
|
3550
3577
|
console.error(`[InstantSplit] Failed after ${duration.toFixed(0)}ms:`, error);
|
|
3551
3578
|
return {
|
|
3552
3579
|
success: false,
|
|
3553
|
-
splitGroupId,
|
|
3554
3580
|
criticalPathDurationMs: duration,
|
|
3555
3581
|
error: errorMessage,
|
|
3556
3582
|
backgroundStarted: false
|
|
@@ -3755,6 +3781,11 @@ function isInstantSplitBundleV4(obj) {
|
|
|
3755
3781
|
function isInstantSplitBundleV5(obj) {
|
|
3756
3782
|
return isInstantSplitBundle(obj) && obj.version === "5.0";
|
|
3757
3783
|
}
|
|
3784
|
+
function isCombinedTransferBundleV6(obj) {
|
|
3785
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
3786
|
+
const b = obj;
|
|
3787
|
+
return b.version === "6.0" && b.type === "COMBINED_TRANSFER";
|
|
3788
|
+
}
|
|
3758
3789
|
|
|
3759
3790
|
// modules/payments/InstantSplitProcessor.ts
|
|
3760
3791
|
function fromHex3(hex) {
|
|
@@ -4408,6 +4439,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4408
4439
|
// Survives page reloads via KV storage so Nostr re-deliveries are ignored
|
|
4409
4440
|
// even when the confirmed token's in-memory ID differs from v5split_{id}.
|
|
4410
4441
|
processedSplitGroupIds = /* @__PURE__ */ new Set();
|
|
4442
|
+
// Persistent dedup: tracks V6 combined transfer IDs that have been processed.
|
|
4443
|
+
processedCombinedTransferIds = /* @__PURE__ */ new Set();
|
|
4411
4444
|
// Storage event subscriptions (push-based sync)
|
|
4412
4445
|
storageEventUnsubscribers = [];
|
|
4413
4446
|
syncDebounceTimer = null;
|
|
@@ -4508,10 +4541,23 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4508
4541
|
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4509
4542
|
}
|
|
4510
4543
|
}
|
|
4544
|
+
for (const [id, token] of this.tokens) {
|
|
4545
|
+
try {
|
|
4546
|
+
if (token.sdkData) {
|
|
4547
|
+
const data = JSON.parse(token.sdkData);
|
|
4548
|
+
if (data?._placeholder) {
|
|
4549
|
+
this.tokens.delete(id);
|
|
4550
|
+
console.log(`[Payments] Removed stale placeholder token: ${id}`);
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4553
|
+
} catch {
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4511
4556
|
const loadedTokens = Array.from(this.tokens.values()).map((t) => `${t.id.slice(0, 12)}(${t.status})`);
|
|
4512
4557
|
console.log(`[Payments][DEBUG] load(): from TXF providers: ${this.tokens.size} tokens [${loadedTokens.join(", ")}]`);
|
|
4513
4558
|
await this.loadPendingV5Tokens();
|
|
4514
4559
|
await this.loadProcessedSplitGroupIds();
|
|
4560
|
+
await this.loadProcessedCombinedTransferIds();
|
|
4515
4561
|
await this.loadHistory();
|
|
4516
4562
|
const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
|
|
4517
4563
|
if (pending2) {
|
|
@@ -4603,12 +4649,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4603
4649
|
token.status = "transferring";
|
|
4604
4650
|
this.tokens.set(token.id, token);
|
|
4605
4651
|
}
|
|
4652
|
+
await this.save();
|
|
4606
4653
|
await this.saveToOutbox(result, recipientPubkey);
|
|
4607
4654
|
result.status = "submitted";
|
|
4608
4655
|
const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
|
|
4609
4656
|
const transferMode = request.transferMode ?? "instant";
|
|
4610
|
-
if (
|
|
4611
|
-
if (
|
|
4657
|
+
if (transferMode === "conservative") {
|
|
4658
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4612
4659
|
this.log("Executing conservative split...");
|
|
4613
4660
|
const splitExecutor = new TokenSplitExecutor({
|
|
4614
4661
|
stateTransitionClient: stClient,
|
|
@@ -4652,27 +4699,59 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4652
4699
|
requestIdHex: splitRequestIdHex
|
|
4653
4700
|
});
|
|
4654
4701
|
this.log(`Conservative split transfer completed`);
|
|
4655
|
-
}
|
|
4656
|
-
|
|
4657
|
-
const
|
|
4702
|
+
}
|
|
4703
|
+
for (const tokenWithAmount of splitPlan.tokensToTransferDirectly) {
|
|
4704
|
+
const token = tokenWithAmount.uiToken;
|
|
4705
|
+
const commitment = await this.createSdkCommitment(token, recipientAddress, signingService);
|
|
4706
|
+
console.log(`[Payments] CONSERVATIVE: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4707
|
+
const submitResponse = await stClient.submitTransferCommitment(commitment);
|
|
4708
|
+
if (submitResponse.status !== "SUCCESS" && submitResponse.status !== "REQUEST_ID_EXISTS") {
|
|
4709
|
+
throw new Error(`Transfer commitment failed: ${submitResponse.status}`);
|
|
4710
|
+
}
|
|
4711
|
+
const inclusionProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment);
|
|
4712
|
+
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4713
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4714
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4715
|
+
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
4716
|
+
memo: request.memo
|
|
4717
|
+
});
|
|
4718
|
+
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
4719
|
+
const requestIdBytes = commitment.requestId;
|
|
4720
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4721
|
+
result.tokenTransfers.push({
|
|
4722
|
+
sourceTokenId: token.id,
|
|
4723
|
+
method: "direct",
|
|
4724
|
+
requestIdHex
|
|
4725
|
+
});
|
|
4726
|
+
this.log(`Token ${token.id} sent via CONSERVATIVE, requestId: ${requestIdHex}`);
|
|
4727
|
+
await this.removeToken(token.id);
|
|
4728
|
+
}
|
|
4729
|
+
} else {
|
|
4730
|
+
const devMode = this.deps.oracle.isDevMode?.() ?? false;
|
|
4731
|
+
const senderPubkey = this.deps.identity.chainPubkey;
|
|
4732
|
+
let changeTokenPlaceholderId = null;
|
|
4733
|
+
let builtSplit = null;
|
|
4734
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4735
|
+
this.log("Building instant split bundle...");
|
|
4658
4736
|
const executor = new InstantSplitExecutor({
|
|
4659
4737
|
stateTransitionClient: stClient,
|
|
4660
4738
|
trustBase,
|
|
4661
4739
|
signingService,
|
|
4662
4740
|
devMode
|
|
4663
4741
|
});
|
|
4664
|
-
|
|
4742
|
+
builtSplit = await executor.buildSplitBundle(
|
|
4665
4743
|
splitPlan.tokenToSplit.sdkToken,
|
|
4666
4744
|
splitPlan.splitAmount,
|
|
4667
4745
|
splitPlan.remainderAmount,
|
|
4668
4746
|
splitPlan.coinId,
|
|
4669
4747
|
recipientAddress,
|
|
4670
|
-
this.deps.transport,
|
|
4671
|
-
recipientPubkey,
|
|
4672
4748
|
{
|
|
4673
4749
|
memo: request.memo,
|
|
4674
4750
|
onChangeTokenCreated: async (changeToken) => {
|
|
4675
4751
|
const changeTokenData = changeToken.toJSON();
|
|
4752
|
+
if (changeTokenPlaceholderId && this.tokens.has(changeTokenPlaceholderId)) {
|
|
4753
|
+
this.tokens.delete(changeTokenPlaceholderId);
|
|
4754
|
+
}
|
|
4676
4755
|
const uiToken = {
|
|
4677
4756
|
id: crypto.randomUUID(),
|
|
4678
4757
|
coinId: request.coinId,
|
|
@@ -4695,65 +4774,103 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4695
4774
|
}
|
|
4696
4775
|
}
|
|
4697
4776
|
);
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
this.
|
|
4703
|
-
|
|
4777
|
+
this.log(`Split bundle built: splitGroupId=${builtSplit.splitGroupId}`);
|
|
4778
|
+
}
|
|
4779
|
+
const directCommitments = await Promise.all(
|
|
4780
|
+
splitPlan.tokensToTransferDirectly.map(
|
|
4781
|
+
(tw) => this.createSdkCommitment(tw.uiToken, recipientAddress, signingService)
|
|
4782
|
+
)
|
|
4783
|
+
);
|
|
4784
|
+
const directTokenEntries = splitPlan.tokensToTransferDirectly.map(
|
|
4785
|
+
(tw, i) => ({
|
|
4786
|
+
sourceToken: JSON.stringify(tw.sdkToken.toJSON()),
|
|
4787
|
+
commitmentData: JSON.stringify(directCommitments[i].toJSON()),
|
|
4788
|
+
amount: tw.uiToken.amount,
|
|
4789
|
+
coinId: tw.uiToken.coinId,
|
|
4790
|
+
tokenId: extractTokenIdFromSdkData(tw.uiToken.sdkData) || void 0
|
|
4791
|
+
})
|
|
4792
|
+
);
|
|
4793
|
+
const combinedBundle = {
|
|
4794
|
+
version: "6.0",
|
|
4795
|
+
type: "COMBINED_TRANSFER",
|
|
4796
|
+
transferId: result.id,
|
|
4797
|
+
splitBundle: builtSplit?.bundle ?? null,
|
|
4798
|
+
directTokens: directTokenEntries,
|
|
4799
|
+
totalAmount: request.amount.toString(),
|
|
4800
|
+
coinId: request.coinId,
|
|
4801
|
+
senderPubkey,
|
|
4802
|
+
memo: request.memo
|
|
4803
|
+
};
|
|
4804
|
+
console.log(
|
|
4805
|
+
`[Payments] Sending V6 combined bundle: transfer=${result.id.slice(0, 8)}... split=${!!builtSplit} direct=${directTokenEntries.length}`
|
|
4806
|
+
);
|
|
4807
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4808
|
+
token: JSON.stringify(combinedBundle),
|
|
4809
|
+
proof: null,
|
|
4810
|
+
memo: request.memo,
|
|
4811
|
+
sender: { transportPubkey: senderPubkey }
|
|
4812
|
+
});
|
|
4813
|
+
console.log(`[Payments] V6 combined bundle sent successfully`);
|
|
4814
|
+
if (builtSplit) {
|
|
4815
|
+
const bgPromise = builtSplit.startBackground();
|
|
4816
|
+
this.pendingBackgroundTasks.push(bgPromise);
|
|
4817
|
+
}
|
|
4818
|
+
if (builtSplit && splitPlan.remainderAmount) {
|
|
4819
|
+
changeTokenPlaceholderId = crypto.randomUUID();
|
|
4820
|
+
const placeholder = {
|
|
4821
|
+
id: changeTokenPlaceholderId,
|
|
4822
|
+
coinId: request.coinId,
|
|
4823
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4824
|
+
name: this.getCoinName(request.coinId),
|
|
4825
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4826
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4827
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4828
|
+
status: "transferring",
|
|
4829
|
+
createdAt: Date.now(),
|
|
4830
|
+
updatedAt: Date.now(),
|
|
4831
|
+
sdkData: JSON.stringify({ _placeholder: true })
|
|
4832
|
+
};
|
|
4833
|
+
this.tokens.set(placeholder.id, placeholder);
|
|
4834
|
+
this.log(`Placeholder change token created: ${placeholder.id} (${placeholder.amount})`);
|
|
4835
|
+
}
|
|
4836
|
+
for (const commitment of directCommitments) {
|
|
4837
|
+
stClient.submitTransferCommitment(commitment).catch(
|
|
4838
|
+
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
4839
|
+
);
|
|
4840
|
+
}
|
|
4841
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4704
4842
|
await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
|
|
4705
4843
|
result.tokenTransfers.push({
|
|
4706
4844
|
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4707
4845
|
method: "split",
|
|
4708
|
-
splitGroupId:
|
|
4709
|
-
nostrEventId: instantResult.nostrEventId
|
|
4846
|
+
splitGroupId: builtSplit.splitGroupId
|
|
4710
4847
|
});
|
|
4711
|
-
this.log(`Instant split transfer completed`);
|
|
4712
4848
|
}
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
}
|
|
4723
|
-
const inclusionProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment);
|
|
4724
|
-
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4725
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4726
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4727
|
-
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
4728
|
-
memo: request.memo
|
|
4729
|
-
});
|
|
4730
|
-
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
4731
|
-
} else {
|
|
4732
|
-
console.log(`[Payments] NOSTR-FIRST: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4733
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4734
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4735
|
-
commitmentData: JSON.stringify(commitment.toJSON()),
|
|
4736
|
-
memo: request.memo
|
|
4849
|
+
for (let i = 0; i < splitPlan.tokensToTransferDirectly.length; i++) {
|
|
4850
|
+
const token = splitPlan.tokensToTransferDirectly[i].uiToken;
|
|
4851
|
+
const commitment = directCommitments[i];
|
|
4852
|
+
const requestIdBytes = commitment.requestId;
|
|
4853
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4854
|
+
result.tokenTransfers.push({
|
|
4855
|
+
sourceTokenId: token.id,
|
|
4856
|
+
method: "direct",
|
|
4857
|
+
requestIdHex
|
|
4737
4858
|
});
|
|
4738
|
-
|
|
4739
|
-
stClient.submitTransferCommitment(commitment).catch(
|
|
4740
|
-
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
4741
|
-
);
|
|
4859
|
+
await this.removeToken(token.id);
|
|
4742
4860
|
}
|
|
4743
|
-
|
|
4744
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4745
|
-
result.tokenTransfers.push({
|
|
4746
|
-
sourceTokenId: token.id,
|
|
4747
|
-
method: "direct",
|
|
4748
|
-
requestIdHex
|
|
4749
|
-
});
|
|
4750
|
-
this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
|
|
4751
|
-
await this.removeToken(token.id);
|
|
4861
|
+
this.log(`V6 combined transfer completed`);
|
|
4752
4862
|
}
|
|
4753
4863
|
result.status = "delivered";
|
|
4754
4864
|
await this.save();
|
|
4755
4865
|
await this.removeFromOutbox(result.id);
|
|
4756
4866
|
result.status = "completed";
|
|
4867
|
+
const tokenMap = new Map(result.tokens.map((t) => [t.id, t]));
|
|
4868
|
+
const sentTokenIds = result.tokenTransfers.map((tt) => ({
|
|
4869
|
+
id: tt.sourceTokenId,
|
|
4870
|
+
// For split tokens, use splitAmount (the portion sent), not the original token amount
|
|
4871
|
+
amount: tt.method === "split" ? splitPlan.splitAmount?.toString() || "0" : tokenMap.get(tt.sourceTokenId)?.amount || "0",
|
|
4872
|
+
source: tt.method === "split" ? "split" : "direct"
|
|
4873
|
+
}));
|
|
4757
4874
|
const sentTokenId = result.tokens[0] ? extractTokenIdFromSdkData(result.tokens[0].sdkData) : void 0;
|
|
4758
4875
|
await this.addToHistory({
|
|
4759
4876
|
type: "SENT",
|
|
@@ -4766,7 +4883,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4766
4883
|
recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
|
|
4767
4884
|
memo: request.memo,
|
|
4768
4885
|
transferId: result.id,
|
|
4769
|
-
tokenId: sentTokenId || void 0
|
|
4886
|
+
tokenId: sentTokenId || void 0,
|
|
4887
|
+
tokenIds: sentTokenIds.length > 0 ? sentTokenIds : void 0
|
|
4770
4888
|
});
|
|
4771
4889
|
this.deps.emitEvent("transfer:confirmed", result);
|
|
4772
4890
|
return result;
|
|
@@ -4936,6 +5054,267 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4936
5054
|
};
|
|
4937
5055
|
}
|
|
4938
5056
|
}
|
|
5057
|
+
// ===========================================================================
|
|
5058
|
+
// Shared Helpers for V5 and V6 Receiver Processing
|
|
5059
|
+
// ===========================================================================
|
|
5060
|
+
/**
|
|
5061
|
+
* Save a V5 split bundle as an unconfirmed token (shared by V5 standalone and V6 combined).
|
|
5062
|
+
* Returns the created UI token, or null if deduped.
|
|
5063
|
+
*
|
|
5064
|
+
* @param deferPersistence - If true, skip addToken/save calls (caller batches them).
|
|
5065
|
+
* The token is still added to the in-memory map for dedup; caller must call save().
|
|
5066
|
+
*/
|
|
5067
|
+
async saveUnconfirmedV5Token(bundle, senderPubkey, deferPersistence = false) {
|
|
5068
|
+
const deterministicId = `v5split_${bundle.splitGroupId}`;
|
|
5069
|
+
if (this.tokens.has(deterministicId) || this.processedSplitGroupIds.has(bundle.splitGroupId)) {
|
|
5070
|
+
console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
|
|
5071
|
+
return null;
|
|
5072
|
+
}
|
|
5073
|
+
const registry = TokenRegistry.getInstance();
|
|
5074
|
+
const pendingData = {
|
|
5075
|
+
type: "v5_bundle",
|
|
5076
|
+
stage: "RECEIVED",
|
|
5077
|
+
bundleJson: JSON.stringify(bundle),
|
|
5078
|
+
senderPubkey,
|
|
5079
|
+
savedAt: Date.now(),
|
|
5080
|
+
attemptCount: 0
|
|
5081
|
+
};
|
|
5082
|
+
const uiToken = {
|
|
5083
|
+
id: deterministicId,
|
|
5084
|
+
coinId: bundle.coinId,
|
|
5085
|
+
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
5086
|
+
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
5087
|
+
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
5088
|
+
amount: bundle.amount,
|
|
5089
|
+
status: "submitted",
|
|
5090
|
+
// UNCONFIRMED
|
|
5091
|
+
createdAt: Date.now(),
|
|
5092
|
+
updatedAt: Date.now(),
|
|
5093
|
+
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
5094
|
+
};
|
|
5095
|
+
this.processedSplitGroupIds.add(bundle.splitGroupId);
|
|
5096
|
+
if (deferPersistence) {
|
|
5097
|
+
this.tokens.set(uiToken.id, uiToken);
|
|
5098
|
+
} else {
|
|
5099
|
+
await this.addToken(uiToken);
|
|
5100
|
+
await this.saveProcessedSplitGroupIds();
|
|
5101
|
+
}
|
|
5102
|
+
return uiToken;
|
|
5103
|
+
}
|
|
5104
|
+
/**
|
|
5105
|
+
* Save a commitment-only (NOSTR-FIRST) token and start proof polling.
|
|
5106
|
+
* Shared by standalone NOSTR-FIRST handler and V6 combined handler.
|
|
5107
|
+
* Returns the created UI token, or null if deduped/tombstoned.
|
|
5108
|
+
*
|
|
5109
|
+
* @param deferPersistence - If true, skip save() and commitment submission
|
|
5110
|
+
* (caller batches them). Token is added to in-memory map + proof polling is queued.
|
|
5111
|
+
* @param skipGenesisDedup - If true, skip genesis-ID-only dedup. V6 handler sets this
|
|
5112
|
+
* because bundle-level dedup protects against replays, and split children share genesis IDs.
|
|
5113
|
+
*/
|
|
5114
|
+
async saveCommitmentOnlyToken(sourceTokenInput, commitmentInput, senderPubkey, deferPersistence = false, skipGenesisDedup = false) {
|
|
5115
|
+
const tokenInfo = await parseTokenInfo(sourceTokenInput);
|
|
5116
|
+
const sdkData = typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput);
|
|
5117
|
+
const nostrTokenId = extractTokenIdFromSdkData(sdkData);
|
|
5118
|
+
const nostrStateHash = extractStateHashFromSdkData(sdkData);
|
|
5119
|
+
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
5120
|
+
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
5121
|
+
return null;
|
|
5122
|
+
}
|
|
5123
|
+
if (nostrTokenId) {
|
|
5124
|
+
for (const existing of this.tokens.values()) {
|
|
5125
|
+
const existingTokenId = extractTokenIdFromSdkData(existing.sdkData);
|
|
5126
|
+
if (existingTokenId !== nostrTokenId) continue;
|
|
5127
|
+
const existingStateHash = extractStateHashFromSdkData(existing.sdkData);
|
|
5128
|
+
if (nostrStateHash && existingStateHash === nostrStateHash) {
|
|
5129
|
+
console.log(
|
|
5130
|
+
`[Payments] NOSTR-FIRST: Skipping duplicate token state ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`
|
|
5131
|
+
);
|
|
5132
|
+
return null;
|
|
5133
|
+
}
|
|
5134
|
+
if (!skipGenesisDedup) {
|
|
5135
|
+
console.log(
|
|
5136
|
+
`[Payments] NOSTR-FIRST: Skipping replay of finalized token ${nostrTokenId.slice(0, 8)}...`
|
|
5137
|
+
);
|
|
5138
|
+
return null;
|
|
5139
|
+
}
|
|
5140
|
+
}
|
|
5141
|
+
}
|
|
5142
|
+
const token = {
|
|
5143
|
+
id: crypto.randomUUID(),
|
|
5144
|
+
coinId: tokenInfo.coinId,
|
|
5145
|
+
symbol: tokenInfo.symbol,
|
|
5146
|
+
name: tokenInfo.name,
|
|
5147
|
+
decimals: tokenInfo.decimals,
|
|
5148
|
+
iconUrl: tokenInfo.iconUrl,
|
|
5149
|
+
amount: tokenInfo.amount,
|
|
5150
|
+
status: "submitted",
|
|
5151
|
+
// NOSTR-FIRST: unconfirmed until proof
|
|
5152
|
+
createdAt: Date.now(),
|
|
5153
|
+
updatedAt: Date.now(),
|
|
5154
|
+
sdkData
|
|
5155
|
+
};
|
|
5156
|
+
this.tokens.set(token.id, token);
|
|
5157
|
+
if (!deferPersistence) {
|
|
5158
|
+
await this.save();
|
|
5159
|
+
}
|
|
5160
|
+
try {
|
|
5161
|
+
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
|
|
5162
|
+
const requestIdBytes = commitment.requestId;
|
|
5163
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
5164
|
+
if (!deferPersistence) {
|
|
5165
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5166
|
+
if (stClient) {
|
|
5167
|
+
const response = await stClient.submitTransferCommitment(commitment);
|
|
5168
|
+
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
5169
|
+
}
|
|
5170
|
+
}
|
|
5171
|
+
this.addProofPollingJob({
|
|
5172
|
+
tokenId: token.id,
|
|
5173
|
+
requestIdHex,
|
|
5174
|
+
commitmentJson: JSON.stringify(commitmentInput),
|
|
5175
|
+
startedAt: Date.now(),
|
|
5176
|
+
attemptCount: 0,
|
|
5177
|
+
lastAttemptAt: 0,
|
|
5178
|
+
onProofReceived: async (tokenId) => {
|
|
5179
|
+
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
|
|
5180
|
+
}
|
|
5181
|
+
});
|
|
5182
|
+
} catch (err) {
|
|
5183
|
+
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
5184
|
+
}
|
|
5185
|
+
return token;
|
|
5186
|
+
}
|
|
5187
|
+
// ===========================================================================
|
|
5188
|
+
// Combined Transfer V6 — Receiver
|
|
5189
|
+
// ===========================================================================
|
|
5190
|
+
/**
|
|
5191
|
+
* Process a received COMBINED_TRANSFER V6 bundle.
|
|
5192
|
+
*
|
|
5193
|
+
* Unpacks a single Nostr message into its component tokens:
|
|
5194
|
+
* - Optional V5 split bundle (saved as unconfirmed, resolved lazily)
|
|
5195
|
+
* - Zero or more direct tokens (saved as unconfirmed, proof-polled)
|
|
5196
|
+
*
|
|
5197
|
+
* Emits ONE transfer:incoming event and records ONE history entry.
|
|
5198
|
+
*/
|
|
5199
|
+
async processCombinedTransferBundle(bundle, senderPubkey) {
|
|
5200
|
+
this.ensureInitialized();
|
|
5201
|
+
if (!this.loaded && this.loadedPromise) {
|
|
5202
|
+
await this.loadedPromise;
|
|
5203
|
+
}
|
|
5204
|
+
if (this.processedCombinedTransferIds.has(bundle.transferId)) {
|
|
5205
|
+
console.log(`[Payments] V6 combined transfer ${bundle.transferId.slice(0, 12)}... already processed, skipping`);
|
|
5206
|
+
return;
|
|
5207
|
+
}
|
|
5208
|
+
console.log(
|
|
5209
|
+
`[Payments] Processing V6 combined transfer ${bundle.transferId.slice(0, 12)}... (split=${!!bundle.splitBundle}, direct=${bundle.directTokens.length})`
|
|
5210
|
+
);
|
|
5211
|
+
const allTokens = [];
|
|
5212
|
+
const tokenBreakdown = [];
|
|
5213
|
+
const parsedDirectEntries = bundle.directTokens.map((entry) => ({
|
|
5214
|
+
sourceToken: typeof entry.sourceToken === "string" ? JSON.parse(entry.sourceToken) : entry.sourceToken,
|
|
5215
|
+
commitment: typeof entry.commitmentData === "string" ? JSON.parse(entry.commitmentData) : entry.commitmentData
|
|
5216
|
+
}));
|
|
5217
|
+
if (bundle.splitBundle) {
|
|
5218
|
+
const splitToken = await this.saveUnconfirmedV5Token(bundle.splitBundle, senderPubkey, true);
|
|
5219
|
+
if (splitToken) {
|
|
5220
|
+
allTokens.push(splitToken);
|
|
5221
|
+
tokenBreakdown.push({ id: splitToken.id, amount: splitToken.amount, source: "split" });
|
|
5222
|
+
} else {
|
|
5223
|
+
console.warn(`[Payments] V6: split token was deduped/failed \u2014 amount=${bundle.splitBundle.amount}`);
|
|
5224
|
+
}
|
|
5225
|
+
}
|
|
5226
|
+
const directResults = await Promise.all(
|
|
5227
|
+
parsedDirectEntries.map(
|
|
5228
|
+
({ sourceToken, commitment }) => this.saveCommitmentOnlyToken(sourceToken, commitment, senderPubkey, true, true)
|
|
5229
|
+
)
|
|
5230
|
+
);
|
|
5231
|
+
for (let i = 0; i < directResults.length; i++) {
|
|
5232
|
+
const token = directResults[i];
|
|
5233
|
+
if (token) {
|
|
5234
|
+
allTokens.push(token);
|
|
5235
|
+
tokenBreakdown.push({ id: token.id, amount: token.amount, source: "direct" });
|
|
5236
|
+
} else {
|
|
5237
|
+
const entry = bundle.directTokens[i];
|
|
5238
|
+
console.warn(
|
|
5239
|
+
`[Payments] V6: direct token #${i} dropped (amount=${entry.amount}, tokenId=${entry.tokenId?.slice(0, 12) ?? "N/A"})`
|
|
5240
|
+
);
|
|
5241
|
+
}
|
|
5242
|
+
}
|
|
5243
|
+
if (allTokens.length === 0) {
|
|
5244
|
+
console.log(`[Payments] V6 combined transfer: all tokens deduped, nothing to save`);
|
|
5245
|
+
return;
|
|
5246
|
+
}
|
|
5247
|
+
this.processedCombinedTransferIds.add(bundle.transferId);
|
|
5248
|
+
const [senderInfo] = await Promise.all([
|
|
5249
|
+
this.resolveSenderInfo(senderPubkey),
|
|
5250
|
+
this.save(),
|
|
5251
|
+
this.saveProcessedCombinedTransferIds(),
|
|
5252
|
+
...bundle.splitBundle ? [this.saveProcessedSplitGroupIds()] : []
|
|
5253
|
+
]);
|
|
5254
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5255
|
+
if (stClient) {
|
|
5256
|
+
for (const { commitment } of parsedDirectEntries) {
|
|
5257
|
+
import_TransferCommitment4.TransferCommitment.fromJSON(commitment).then(
|
|
5258
|
+
(c) => stClient.submitTransferCommitment(c)
|
|
5259
|
+
).catch(
|
|
5260
|
+
(err) => console.error("[Payments] V6 background commitment submit failed:", err)
|
|
5261
|
+
);
|
|
5262
|
+
}
|
|
5263
|
+
}
|
|
5264
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
5265
|
+
id: bundle.transferId,
|
|
5266
|
+
senderPubkey,
|
|
5267
|
+
senderNametag: senderInfo.senderNametag,
|
|
5268
|
+
tokens: allTokens,
|
|
5269
|
+
memo: bundle.memo,
|
|
5270
|
+
receivedAt: Date.now()
|
|
5271
|
+
});
|
|
5272
|
+
const actualAmount = allTokens.reduce((sum, t) => sum + BigInt(t.amount || "0"), 0n).toString();
|
|
5273
|
+
await this.addToHistory({
|
|
5274
|
+
type: "RECEIVED",
|
|
5275
|
+
amount: actualAmount,
|
|
5276
|
+
coinId: bundle.coinId,
|
|
5277
|
+
symbol: allTokens[0]?.symbol || bundle.coinId,
|
|
5278
|
+
timestamp: Date.now(),
|
|
5279
|
+
senderPubkey,
|
|
5280
|
+
...senderInfo,
|
|
5281
|
+
memo: bundle.memo,
|
|
5282
|
+
transferId: bundle.transferId,
|
|
5283
|
+
tokenId: allTokens[0]?.id,
|
|
5284
|
+
tokenIds: tokenBreakdown
|
|
5285
|
+
});
|
|
5286
|
+
if (bundle.splitBundle) {
|
|
5287
|
+
this.resolveUnconfirmed().catch(() => {
|
|
5288
|
+
});
|
|
5289
|
+
this.scheduleResolveUnconfirmed();
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
/**
|
|
5293
|
+
* Persist processed combined transfer IDs to KV storage.
|
|
5294
|
+
*/
|
|
5295
|
+
async saveProcessedCombinedTransferIds() {
|
|
5296
|
+
const ids = Array.from(this.processedCombinedTransferIds);
|
|
5297
|
+
if (ids.length > 0) {
|
|
5298
|
+
await this.deps.storage.set(
|
|
5299
|
+
STORAGE_KEYS_ADDRESS.PROCESSED_COMBINED_TRANSFER_IDS,
|
|
5300
|
+
JSON.stringify(ids)
|
|
5301
|
+
);
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5304
|
+
/**
|
|
5305
|
+
* Load processed combined transfer IDs from KV storage.
|
|
5306
|
+
*/
|
|
5307
|
+
async loadProcessedCombinedTransferIds() {
|
|
5308
|
+
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PROCESSED_COMBINED_TRANSFER_IDS);
|
|
5309
|
+
if (!data) return;
|
|
5310
|
+
try {
|
|
5311
|
+
const ids = JSON.parse(data);
|
|
5312
|
+
for (const id of ids) {
|
|
5313
|
+
this.processedCombinedTransferIds.add(id);
|
|
5314
|
+
}
|
|
5315
|
+
} catch {
|
|
5316
|
+
}
|
|
5317
|
+
}
|
|
4939
5318
|
/**
|
|
4940
5319
|
* Process a received INSTANT_SPLIT bundle.
|
|
4941
5320
|
*
|
|
@@ -4959,36 +5338,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4959
5338
|
return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
|
|
4960
5339
|
}
|
|
4961
5340
|
try {
|
|
4962
|
-
const
|
|
4963
|
-
if (
|
|
4964
|
-
console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
|
|
5341
|
+
const uiToken = await this.saveUnconfirmedV5Token(bundle, senderPubkey);
|
|
5342
|
+
if (!uiToken) {
|
|
4965
5343
|
return { success: true, durationMs: 0 };
|
|
4966
5344
|
}
|
|
4967
|
-
const registry = TokenRegistry.getInstance();
|
|
4968
|
-
const pendingData = {
|
|
4969
|
-
type: "v5_bundle",
|
|
4970
|
-
stage: "RECEIVED",
|
|
4971
|
-
bundleJson: JSON.stringify(bundle),
|
|
4972
|
-
senderPubkey,
|
|
4973
|
-
savedAt: Date.now(),
|
|
4974
|
-
attemptCount: 0
|
|
4975
|
-
};
|
|
4976
|
-
const uiToken = {
|
|
4977
|
-
id: deterministicId,
|
|
4978
|
-
coinId: bundle.coinId,
|
|
4979
|
-
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
4980
|
-
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
4981
|
-
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
4982
|
-
amount: bundle.amount,
|
|
4983
|
-
status: "submitted",
|
|
4984
|
-
// UNCONFIRMED
|
|
4985
|
-
createdAt: Date.now(),
|
|
4986
|
-
updatedAt: Date.now(),
|
|
4987
|
-
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
4988
|
-
};
|
|
4989
|
-
await this.addToken(uiToken);
|
|
4990
|
-
this.processedSplitGroupIds.add(bundle.splitGroupId);
|
|
4991
|
-
await this.saveProcessedSplitGroupIds();
|
|
4992
5345
|
const senderInfo = await this.resolveSenderInfo(senderPubkey);
|
|
4993
5346
|
await this.addToHistory({
|
|
4994
5347
|
type: "RECEIVED",
|
|
@@ -4999,7 +5352,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4999
5352
|
senderPubkey,
|
|
5000
5353
|
...senderInfo,
|
|
5001
5354
|
memo,
|
|
5002
|
-
tokenId:
|
|
5355
|
+
tokenId: uiToken.id
|
|
5003
5356
|
});
|
|
5004
5357
|
this.deps.emitEvent("transfer:incoming", {
|
|
5005
5358
|
id: bundle.splitGroupId,
|
|
@@ -5649,16 +6002,18 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5649
6002
|
}
|
|
5650
6003
|
/**
|
|
5651
6004
|
* Aggregate tokens by coinId with confirmed/unconfirmed breakdown.
|
|
5652
|
-
* Excludes tokens with status 'spent'
|
|
6005
|
+
* Excludes tokens with status 'spent' or 'invalid'.
|
|
6006
|
+
* Tokens with status 'transferring' are counted as unconfirmed (visible in UI as "Sending").
|
|
5653
6007
|
*/
|
|
5654
6008
|
aggregateTokens(coinId) {
|
|
5655
6009
|
const assetsMap = /* @__PURE__ */ new Map();
|
|
5656
6010
|
for (const token of this.tokens.values()) {
|
|
5657
|
-
if (token.status === "spent" || token.status === "invalid"
|
|
6011
|
+
if (token.status === "spent" || token.status === "invalid") continue;
|
|
5658
6012
|
if (coinId && token.coinId !== coinId) continue;
|
|
5659
6013
|
const key = token.coinId;
|
|
5660
6014
|
const amount = BigInt(token.amount);
|
|
5661
6015
|
const isConfirmed = token.status === "confirmed";
|
|
6016
|
+
const isTransferring = token.status === "transferring";
|
|
5662
6017
|
const existing = assetsMap.get(key);
|
|
5663
6018
|
if (existing) {
|
|
5664
6019
|
if (isConfirmed) {
|
|
@@ -5668,6 +6023,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5668
6023
|
existing.unconfirmedAmount += amount;
|
|
5669
6024
|
existing.unconfirmedTokenCount++;
|
|
5670
6025
|
}
|
|
6026
|
+
if (isTransferring) existing.transferringTokenCount++;
|
|
5671
6027
|
} else {
|
|
5672
6028
|
assetsMap.set(key, {
|
|
5673
6029
|
coinId: token.coinId,
|
|
@@ -5678,7 +6034,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5678
6034
|
confirmedAmount: isConfirmed ? amount : 0n,
|
|
5679
6035
|
unconfirmedAmount: isConfirmed ? 0n : amount,
|
|
5680
6036
|
confirmedTokenCount: isConfirmed ? 1 : 0,
|
|
5681
|
-
unconfirmedTokenCount: isConfirmed ? 0 : 1
|
|
6037
|
+
unconfirmedTokenCount: isConfirmed ? 0 : 1,
|
|
6038
|
+
transferringTokenCount: isTransferring ? 1 : 0
|
|
5682
6039
|
});
|
|
5683
6040
|
}
|
|
5684
6041
|
}
|
|
@@ -5696,6 +6053,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5696
6053
|
unconfirmedAmount: raw.unconfirmedAmount.toString(),
|
|
5697
6054
|
confirmedTokenCount: raw.confirmedTokenCount,
|
|
5698
6055
|
unconfirmedTokenCount: raw.unconfirmedTokenCount,
|
|
6056
|
+
transferringTokenCount: raw.transferringTokenCount,
|
|
5699
6057
|
priceUsd: null,
|
|
5700
6058
|
priceEur: null,
|
|
5701
6059
|
change24h: null,
|
|
@@ -7093,7 +7451,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7093
7451
|
/**
|
|
7094
7452
|
* Handle NOSTR-FIRST commitment-only transfer (recipient side)
|
|
7095
7453
|
* This is called when receiving a transfer with only commitmentData and no proof yet.
|
|
7096
|
-
*
|
|
7454
|
+
* Delegates to saveCommitmentOnlyToken() helper, then emits event + records history.
|
|
7097
7455
|
*/
|
|
7098
7456
|
async handleCommitmentOnlyTransfer(transfer, payload) {
|
|
7099
7457
|
try {
|
|
@@ -7103,41 +7461,22 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7103
7461
|
console.warn("[Payments] Invalid NOSTR-FIRST transfer format");
|
|
7104
7462
|
return;
|
|
7105
7463
|
}
|
|
7106
|
-
const
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
decimals: tokenInfo.decimals,
|
|
7113
|
-
iconUrl: tokenInfo.iconUrl,
|
|
7114
|
-
amount: tokenInfo.amount,
|
|
7115
|
-
status: "submitted",
|
|
7116
|
-
// NOSTR-FIRST: unconfirmed until proof
|
|
7117
|
-
createdAt: Date.now(),
|
|
7118
|
-
updatedAt: Date.now(),
|
|
7119
|
-
sdkData: typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput)
|
|
7120
|
-
};
|
|
7121
|
-
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7122
|
-
const nostrStateHash = extractStateHashFromSdkData(token.sdkData);
|
|
7123
|
-
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
7124
|
-
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
7125
|
-
return;
|
|
7126
|
-
}
|
|
7127
|
-
this.tokens.set(token.id, token);
|
|
7128
|
-
console.log(`[Payments][DEBUG] NOSTR-FIRST: saving token id=${token.id.slice(0, 16)} status=${token.status} sdkData.length=${token.sdkData?.length}`);
|
|
7129
|
-
await this.save();
|
|
7130
|
-
console.log(`[Payments][DEBUG] NOSTR-FIRST: save() completed, tokens.size=${this.tokens.size}`);
|
|
7464
|
+
const token = await this.saveCommitmentOnlyToken(
|
|
7465
|
+
sourceTokenInput,
|
|
7466
|
+
commitmentInput,
|
|
7467
|
+
transfer.senderTransportPubkey
|
|
7468
|
+
);
|
|
7469
|
+
if (!token) return;
|
|
7131
7470
|
const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
|
|
7132
|
-
|
|
7471
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
7133
7472
|
id: transfer.id,
|
|
7134
7473
|
senderPubkey: transfer.senderTransportPubkey,
|
|
7135
7474
|
senderNametag: senderInfo.senderNametag,
|
|
7136
7475
|
tokens: [token],
|
|
7137
7476
|
memo: payload.memo,
|
|
7138
7477
|
receivedAt: transfer.timestamp
|
|
7139
|
-
};
|
|
7140
|
-
|
|
7478
|
+
});
|
|
7479
|
+
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7141
7480
|
await this.addToHistory({
|
|
7142
7481
|
type: "RECEIVED",
|
|
7143
7482
|
amount: token.amount,
|
|
@@ -7149,29 +7488,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7149
7488
|
memo: payload.memo,
|
|
7150
7489
|
tokenId: nostrTokenId || token.id
|
|
7151
7490
|
});
|
|
7152
|
-
try {
|
|
7153
|
-
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
|
|
7154
|
-
const requestIdBytes = commitment.requestId;
|
|
7155
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
7156
|
-
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
7157
|
-
if (stClient) {
|
|
7158
|
-
const response = await stClient.submitTransferCommitment(commitment);
|
|
7159
|
-
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
7160
|
-
}
|
|
7161
|
-
this.addProofPollingJob({
|
|
7162
|
-
tokenId: token.id,
|
|
7163
|
-
requestIdHex,
|
|
7164
|
-
commitmentJson: JSON.stringify(commitmentInput),
|
|
7165
|
-
startedAt: Date.now(),
|
|
7166
|
-
attemptCount: 0,
|
|
7167
|
-
lastAttemptAt: 0,
|
|
7168
|
-
onProofReceived: async (tokenId) => {
|
|
7169
|
-
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
|
|
7170
|
-
}
|
|
7171
|
-
});
|
|
7172
|
-
} catch (err) {
|
|
7173
|
-
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
7174
|
-
}
|
|
7175
7491
|
} catch (error) {
|
|
7176
7492
|
console.error("[Payments] Failed to process NOSTR-FIRST transfer:", error);
|
|
7177
7493
|
}
|
|
@@ -7290,6 +7606,28 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7290
7606
|
try {
|
|
7291
7607
|
const payload = transfer.payload;
|
|
7292
7608
|
console.log("[Payments][DEBUG] handleIncomingTransfer: keys=", Object.keys(payload).join(","));
|
|
7609
|
+
let combinedBundle = null;
|
|
7610
|
+
if (isCombinedTransferBundleV6(payload)) {
|
|
7611
|
+
combinedBundle = payload;
|
|
7612
|
+
} else if (payload.token) {
|
|
7613
|
+
try {
|
|
7614
|
+
const inner = typeof payload.token === "string" ? JSON.parse(payload.token) : payload.token;
|
|
7615
|
+
if (isCombinedTransferBundleV6(inner)) {
|
|
7616
|
+
combinedBundle = inner;
|
|
7617
|
+
}
|
|
7618
|
+
} catch {
|
|
7619
|
+
}
|
|
7620
|
+
}
|
|
7621
|
+
if (combinedBundle) {
|
|
7622
|
+
this.log("Processing COMBINED_TRANSFER V6 bundle...");
|
|
7623
|
+
try {
|
|
7624
|
+
await this.processCombinedTransferBundle(combinedBundle, transfer.senderTransportPubkey);
|
|
7625
|
+
this.log("COMBINED_TRANSFER V6 processed successfully");
|
|
7626
|
+
} catch (err) {
|
|
7627
|
+
console.error("[Payments] COMBINED_TRANSFER V6 processing error:", err);
|
|
7628
|
+
}
|
|
7629
|
+
return;
|
|
7630
|
+
}
|
|
7293
7631
|
let instantBundle = null;
|
|
7294
7632
|
if (isInstantSplitBundle(payload)) {
|
|
7295
7633
|
instantBundle = payload;
|
|
@@ -7441,17 +7779,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7441
7779
|
memo: payload.memo,
|
|
7442
7780
|
tokenId: incomingTokenId || token.id
|
|
7443
7781
|
});
|
|
7782
|
+
const incomingTransfer = {
|
|
7783
|
+
id: transfer.id,
|
|
7784
|
+
senderPubkey: transfer.senderTransportPubkey,
|
|
7785
|
+
senderNametag: senderInfo.senderNametag,
|
|
7786
|
+
tokens: [token],
|
|
7787
|
+
memo: payload.memo,
|
|
7788
|
+
receivedAt: transfer.timestamp
|
|
7789
|
+
};
|
|
7790
|
+
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
7791
|
+
this.log(`Incoming transfer processed: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7792
|
+
} else {
|
|
7793
|
+
this.log(`Duplicate transfer ignored: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7444
7794
|
}
|
|
7445
|
-
const incomingTransfer = {
|
|
7446
|
-
id: transfer.id,
|
|
7447
|
-
senderPubkey: transfer.senderTransportPubkey,
|
|
7448
|
-
senderNametag: senderInfo.senderNametag,
|
|
7449
|
-
tokens: [token],
|
|
7450
|
-
memo: payload.memo,
|
|
7451
|
-
receivedAt: transfer.timestamp
|
|
7452
|
-
};
|
|
7453
|
-
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
7454
|
-
this.log(`Incoming transfer processed: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7455
7795
|
} catch (error) {
|
|
7456
7796
|
console.error("[Payments] Failed to process incoming transfer:", error);
|
|
7457
7797
|
}
|